swarm ui: a soft context limit field in an agent's inference settings
A whole percent from 5 to 100 of the model's context window, sent as soft_context_percent next to max_subagents. The inference row shows it, the edit dialog and the create form take it, and an archetype can carry it.
This commit is contained in:
parent
b19b730a3a
commit
e9af6cf2c7
6 changed files with 109 additions and 7 deletions
|
|
@ -714,11 +714,11 @@ text stays.
|
||||||
An opened agent card shows, in its **settings** section, its model and effort under **inference**, each
|
An opened agent card shows, in its **settings** section, its model and effort under **inference**, each
|
||||||
with where it comes from: the swarm, its agent.nix, its own pick, or the
|
with where it comes from: the swarm, its agent.nix, its own pick, or the
|
||||||
default. Below them are the subagent model list the swarm sets, or "its own"
|
default. Below them are the subagent model list the swarm sets, or "its own"
|
||||||
when it sets none, and the subagent cap, which reads "no cap" when unset.
|
when it sets none, the subagent cap, which reads "no cap" when unset, and the
|
||||||
**edit** opens a dialog with the four fields, filled with what the swarm
|
soft context limit, which reads "its own" when unset. **edit** opens a dialog with the five fields, filled with what the swarm
|
||||||
stores; saving does a `PUT /api/agents/{agent}/inference` of
|
stores; saving does a `PUT /api/agents/{agent}/inference` of
|
||||||
the whole value. A field you leave empty stays out of it, so the agent keeps
|
the whole value. A field you leave empty stays out of it, so the agent keeps
|
||||||
its own value for that field; an empty max subagents is no cap. **clear the swarm's setting** sends the `DELETE`.
|
its own value for that field; an empty max subagents is no cap, and an empty soft context limit leaves the agent's own. The soft context limit is a whole percent from 5 to 100 of the model's context window: the agent compacts at the end of a turn once its context passes it, and a turn can run past it. **clear the swarm's setting** sends the `DELETE`.
|
||||||
The card reads `GET /api/agents/{agent}/inference`; before the agent's first
|
The card reads `GET /api/agents/{agent}/inference`; before the agent's first
|
||||||
report reaches the controller, it shows the swarm's stored values only.
|
report reaches the controller, it shows the swarm's stored values only.
|
||||||
[inference](../swarm/inference.md) describes the settings themselves.
|
[inference](../swarm/inference.md) describes the settings themselves.
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ import { FormStack } from "../../ui/form-stack/FormStack.js";
|
||||||
import "../../ui/badge-row/BadgeRow.css";
|
import "../../ui/badge-row/BadgeRow.css";
|
||||||
import "./AgentInference.css";
|
import "./AgentInference.css";
|
||||||
import { MAX_SUBAGENTS, maxSubagentsValue } from "./max-subagents.js";
|
import { MAX_SUBAGENTS, maxSubagentsValue } from "./max-subagents.js";
|
||||||
|
import {
|
||||||
|
MAX_SOFT_CONTEXT_PERCENT,
|
||||||
|
MIN_SOFT_CONTEXT_PERCENT,
|
||||||
|
softContextValue,
|
||||||
|
} from "./soft-context.js";
|
||||||
|
|
||||||
type Source = "swarm" | "agent_nix" | "local_file" | "default";
|
type Source = "swarm" | "agent_nix" | "local_file" | "default";
|
||||||
|
|
||||||
|
|
@ -35,6 +40,7 @@ interface Settings {
|
||||||
effort?: string;
|
effort?: string;
|
||||||
subagent_models?: string[];
|
subagent_models?: string[];
|
||||||
max_subagents?: number;
|
max_subagents?: number;
|
||||||
|
soft_context_percent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors `EffectiveInference`.
|
// Mirrors `EffectiveInference`.
|
||||||
|
|
@ -81,13 +87,14 @@ function inferenceUrl(agent: string): string {
|
||||||
return `/api/agents/${encodeURIComponent(agent)}/inference`;
|
return `/api/agents/${encodeURIComponent(agent)}/inference`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The four inference boxes as typed. Shared by the editor here and the
|
// The inference boxes as typed. Shared by the editor here and the
|
||||||
// create-agent form, which send what `draftBody` makes of them.
|
// create-agent form, which send what `draftBody` makes of them.
|
||||||
export interface InferenceDraft {
|
export interface InferenceDraft {
|
||||||
model: string;
|
model: string;
|
||||||
effort: string;
|
effort: string;
|
||||||
subagents: string;
|
subagents: string;
|
||||||
maxSubagents: string;
|
maxSubagents: string;
|
||||||
|
softContext: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_INFERENCE_DRAFT: InferenceDraft = {
|
export const EMPTY_INFERENCE_DRAFT: InferenceDraft = {
|
||||||
|
|
@ -95,6 +102,7 @@ export const EMPTY_INFERENCE_DRAFT: InferenceDraft = {
|
||||||
effort: "",
|
effort: "",
|
||||||
subagents: "",
|
subagents: "",
|
||||||
maxSubagents: "",
|
maxSubagents: "",
|
||||||
|
softContext: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Why the draft would be refused, or null when it is fine to send.
|
// Why the draft would be refused, or null when it is fine to send.
|
||||||
|
|
@ -106,6 +114,9 @@ export function draftProblem(d: InferenceDraft): string | null {
|
||||||
.find((p) => p !== null) ??
|
.find((p) => p !== null) ??
|
||||||
(maxSubagentsValue(d.maxSubagents) === null
|
(maxSubagentsValue(d.maxSubagents) === null
|
||||||
? `max subagents is a whole number from 0 (no subagents) to ${MAX_SUBAGENTS}, or empty for no cap`
|
? `max subagents is a whole number from 0 (no subagents) to ${MAX_SUBAGENTS}, or empty for no cap`
|
||||||
|
: null) ??
|
||||||
|
(softContextValue(d.softContext) === null
|
||||||
|
? `soft context limit is a whole percent from ${MIN_SOFT_CONTEXT_PERCENT} to ${MAX_SOFT_CONTEXT_PERCENT}, or empty for the agent's own`
|
||||||
: null)
|
: null)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +126,8 @@ export function draftIsEmpty(d: InferenceDraft): boolean {
|
||||||
d.model.trim() === "" &&
|
d.model.trim() === "" &&
|
||||||
d.effort === "" &&
|
d.effort === "" &&
|
||||||
parseModels(d.subagents).length === 0 &&
|
parseModels(d.subagents).length === 0 &&
|
||||||
d.maxSubagents.trim() === ""
|
d.maxSubagents.trim() === "" &&
|
||||||
|
d.softContext.trim() === ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,6 +140,8 @@ export function draftBody(d: InferenceDraft): Settings {
|
||||||
if (models.length > 0) body.subagent_models = models;
|
if (models.length > 0) body.subagent_models = models;
|
||||||
const max = maxSubagentsValue(d.maxSubagents);
|
const max = maxSubagentsValue(d.maxSubagents);
|
||||||
if (typeof max === "number") body.max_subagents = max;
|
if (typeof max === "number") body.max_subagents = max;
|
||||||
|
const soft = softContextValue(d.softContext);
|
||||||
|
if (typeof soft === "number") body.soft_context_percent = soft;
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,6 +188,14 @@ export function InferenceFields({
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
onInput={(maxSubagents) => onChange({ ...draft, maxSubagents })}
|
onInput={(maxSubagents) => onChange({ ...draft, maxSubagents })}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
id={`${idPrefix}-soft-context`}
|
||||||
|
label="soft context limit (%)"
|
||||||
|
value={draft.softContext}
|
||||||
|
placeholder="empty for the agent's own"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
onInput={(softContext) => onChange({ ...draft, softContext })}
|
||||||
|
/>
|
||||||
{problem ? <Hint tone="error">{problem}</Hint> : null}
|
{problem ? <Hint tone="error">{problem}</Hint> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -278,11 +300,22 @@ export function AgentInference({ agent }: { agent: string }) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Badge
|
||||||
|
label="soft context limit"
|
||||||
|
value={
|
||||||
|
configured?.soft_context_percent === undefined
|
||||||
|
? "its own"
|
||||||
|
: `${configured.soft_context_percent}%`
|
||||||
|
}
|
||||||
|
title="the agent compacts at the end of a turn once its context passes this share of the window; a turn can run past it"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Badge
|
<Badge
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
value="edit"
|
value="edit"
|
||||||
title={`set the swarm's model, effort, subagent models and subagent cap for ${agent}`}
|
title={`set the swarm's model, effort, subagent models, subagent cap and soft context limit for ${agent}`}
|
||||||
onClick={() => setEditing(true)}
|
onClick={() => setEditing(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -325,6 +358,7 @@ function InferenceForm({
|
||||||
effort: configured?.effort ?? "",
|
effort: configured?.effort ?? "",
|
||||||
subagents: (configured?.subagent_models ?? []).join(", "),
|
subagents: (configured?.subagent_models ?? []).join(", "),
|
||||||
maxSubagents: configured?.max_subagents?.toString() ?? "",
|
maxSubagents: configured?.max_subagents?.toString() ?? "",
|
||||||
|
softContext: configured?.soft_context_percent?.toString() ?? "",
|
||||||
});
|
});
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [problem, setProblem] = useState<ProblemDetails | null>(null);
|
const [problem, setProblem] = useState<ProblemDetails | null>(null);
|
||||||
|
|
|
||||||
22
frontend/packages/swarm-ui/src/agent/detail/soft-context.ts
Normal file
22
frontend/packages/swarm-ui/src/agent/detail/soft-context.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// The create form's and the inference dialog's `soft_context_percent` field,
|
||||||
|
// kept out of the component so `npm test` can run it.
|
||||||
|
|
||||||
|
/** The lowest `soft_context_percent` the swarm accepts. */
|
||||||
|
export const MIN_SOFT_CONTEXT_PERCENT = 5;
|
||||||
|
/** The highest: the whole context window. */
|
||||||
|
export const MAX_SOFT_CONTEXT_PERCENT = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `soft_context_percent` as typed: `undefined` for empty (the agent's own
|
||||||
|
* default), the number for a whole number from [`MIN_SOFT_CONTEXT_PERCENT`]
|
||||||
|
* to [`MAX_SOFT_CONTEXT_PERCENT`], else `null`.
|
||||||
|
*/
|
||||||
|
export function softContextValue(text: string): number | null | undefined {
|
||||||
|
const t = text.trim();
|
||||||
|
if (t === "") return undefined;
|
||||||
|
if (!/^\d+$/.test(t)) return null;
|
||||||
|
const n = Number(t);
|
||||||
|
return n >= MIN_SOFT_CONTEXT_PERCENT && n <= MAX_SOFT_CONTEXT_PERCENT
|
||||||
|
? n
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ export interface Archetype {
|
||||||
effort?: string;
|
effort?: string;
|
||||||
subagent_models?: string[];
|
subagent_models?: string[];
|
||||||
max_subagents?: number;
|
max_subagents?: number;
|
||||||
|
soft_context_percent?: number;
|
||||||
};
|
};
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
/** The archetype's icon as SVG text. */
|
/** The archetype's icon as SVG text. */
|
||||||
|
|
@ -69,6 +70,10 @@ export function prefill(
|
||||||
inference.max_subagents === undefined
|
inference.max_subagents === undefined
|
||||||
? ""
|
? ""
|
||||||
: String(inference.max_subagents),
|
: String(inference.max_subagents),
|
||||||
|
softContext:
|
||||||
|
inference.soft_context_percent === undefined
|
||||||
|
? ""
|
||||||
|
: String(inference.soft_context_percent),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@ const empty = {
|
||||||
permissions: defaults,
|
permissions: defaults,
|
||||||
cpu: "",
|
cpu: "",
|
||||||
memory: "",
|
memory: "",
|
||||||
inference: { model: "", effort: "", subagents: "", maxSubagents: "" },
|
inference: {
|
||||||
|
model: "",
|
||||||
|
effort: "",
|
||||||
|
subagents: "",
|
||||||
|
maxSubagents: "",
|
||||||
|
softContext: "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
test("no archetype is the default permissions and empty settings", () => {
|
test("no archetype is the default permissions and empty settings", () => {
|
||||||
|
|
@ -34,6 +40,7 @@ test("an archetype's settings fill the form", () => {
|
||||||
effort: "high",
|
effort: "high",
|
||||||
subagent_models: ["haiku", "sonnet"],
|
subagent_models: ["haiku", "sonnet"],
|
||||||
max_subagents: 0,
|
max_subagents: 0,
|
||||||
|
soft_context_percent: 20,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert.deepEqual(prefill(reviewer, defaults), {
|
assert.deepEqual(prefill(reviewer, defaults), {
|
||||||
|
|
@ -45,6 +52,7 @@ test("an archetype's settings fill the form", () => {
|
||||||
effort: "high",
|
effort: "high",
|
||||||
subagents: "haiku, sonnet",
|
subagents: "haiku, sonnet",
|
||||||
maxSubagents: "0",
|
maxSubagents: "0",
|
||||||
|
softContext: "20",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
33
frontend/packages/swarm-ui/test/soft-context.test.mjs
Normal file
33
frontend/packages/swarm-ui/test/soft-context.test.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// The soft context limit field: what counts as empty, valid and refused. Run
|
||||||
|
// by `npm test`, which imports the `.ts` module as it is.
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
MAX_SOFT_CONTEXT_PERCENT,
|
||||||
|
MIN_SOFT_CONTEXT_PERCENT,
|
||||||
|
softContextValue,
|
||||||
|
} from "../src/agent/detail/soft-context.ts";
|
||||||
|
|
||||||
|
test("an empty box leaves the agent on its own limit", () => {
|
||||||
|
assert.equal(softContextValue(""), undefined);
|
||||||
|
assert.equal(softContextValue(" "), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a whole percent from 5 to 100 is taken", () => {
|
||||||
|
assert.equal(softContextValue("20"), 20);
|
||||||
|
assert.equal(softContextValue(" 75 "), 75);
|
||||||
|
assert.equal(
|
||||||
|
softContextValue(String(MIN_SOFT_CONTEXT_PERCENT)),
|
||||||
|
MIN_SOFT_CONTEXT_PERCENT,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
softContextValue(String(MAX_SOFT_CONTEXT_PERCENT)),
|
||||||
|
MAX_SOFT_CONTEXT_PERCENT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("anything else is refused", () => {
|
||||||
|
for (const text of ["0", "4", "101", "-20", "20.5", "abc", "1e1", "20%"]) {
|
||||||
|
assert.equal(softContextValue(text), null, text);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue