From e9af6cf2c7259ea433e1747555c6e94aaf89119a Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 15 Nov 2026 15:16:59 +0100 Subject: [PATCH] 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. --- docs/4-implementation/web-ui/swarm-ui.md | 6 +-- .../src/agent/detail/AgentInference.tsx | 40 +++++++++++++++++-- .../swarm-ui/src/agent/detail/soft-context.ts | 22 ++++++++++ .../src/pages/CreateAgentArchetype.ts | 5 +++ .../test/create-agent-archetype.test.mjs | 10 ++++- .../swarm-ui/test/soft-context.test.mjs | 33 +++++++++++++++ 6 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/agent/detail/soft-context.ts create mode 100644 frontend/packages/swarm-ui/test/soft-context.test.mjs diff --git a/docs/4-implementation/web-ui/swarm-ui.md b/docs/4-implementation/web-ui/swarm-ui.md index 182e22bb4..022132aac 100644 --- a/docs/4-implementation/web-ui/swarm-ui.md +++ b/docs/4-implementation/web-ui/swarm-ui.md @@ -714,11 +714,11 @@ text stays. 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 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. -**edit** opens a dialog with the four fields, filled with what the swarm +when it sets none, the subagent cap, which reads "no cap" when unset, and the +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 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 report reaches the controller, it shows the swarm's stored values only. [inference](../swarm/inference.md) describes the settings themselves. diff --git a/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx b/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx index 6eace45b2..263ed1e3d 100644 --- a/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx +++ b/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx @@ -26,6 +26,11 @@ import { FormStack } from "../../ui/form-stack/FormStack.js"; import "../../ui/badge-row/BadgeRow.css"; import "./AgentInference.css"; 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"; @@ -35,6 +40,7 @@ interface Settings { effort?: string; subagent_models?: string[]; max_subagents?: number; + soft_context_percent?: number; } // Mirrors `EffectiveInference`. @@ -81,13 +87,14 @@ function inferenceUrl(agent: string): string { 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. export interface InferenceDraft { model: string; effort: string; subagents: string; maxSubagents: string; + softContext: string; } export const EMPTY_INFERENCE_DRAFT: InferenceDraft = { @@ -95,6 +102,7 @@ export const EMPTY_INFERENCE_DRAFT: InferenceDraft = { effort: "", subagents: "", maxSubagents: "", + softContext: "", }; // 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) ?? (maxSubagentsValue(d.maxSubagents) === null ? `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) ); } @@ -115,7 +126,8 @@ export function draftIsEmpty(d: InferenceDraft): boolean { d.model.trim() === "" && d.effort === "" && 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; const max = maxSubagentsValue(d.maxSubagents); if (typeof max === "number") body.max_subagents = max; + const soft = softContextValue(d.softContext); + if (typeof soft === "number") body.soft_context_percent = soft; return body; } @@ -174,6 +188,14 @@ export function InferenceFields({ pattern="[0-9]*" onInput={(maxSubagents) => onChange({ ...draft, maxSubagents })} /> + onChange({ ...draft, softContext })} + /> {problem ? {problem} : null} ); @@ -278,11 +300,22 @@ export function AgentInference({ agent }: { agent: string }) { } /> +
  • + +
  • setEditing(true)} /> @@ -325,6 +358,7 @@ function InferenceForm({ effort: configured?.effort ?? "", subagents: (configured?.subagent_models ?? []).join(", "), maxSubagents: configured?.max_subagents?.toString() ?? "", + softContext: configured?.soft_context_percent?.toString() ?? "", }); const [busy, setBusy] = useState(false); const [problem, setProblem] = useState(null); diff --git a/frontend/packages/swarm-ui/src/agent/detail/soft-context.ts b/frontend/packages/swarm-ui/src/agent/detail/soft-context.ts new file mode 100644 index 000000000..b5abf3218 --- /dev/null +++ b/frontend/packages/swarm-ui/src/agent/detail/soft-context.ts @@ -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; +} diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentArchetype.ts b/frontend/packages/swarm-ui/src/pages/CreateAgentArchetype.ts index c4220b6d3..aeb52b60a 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentArchetype.ts +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentArchetype.ts @@ -14,6 +14,7 @@ export interface Archetype { effort?: string; subagent_models?: string[]; max_subagents?: number; + soft_context_percent?: number; }; prompt?: string; /** The archetype's icon as SVG text. */ @@ -69,6 +70,10 @@ export function prefill( inference.max_subagents === undefined ? "" : String(inference.max_subagents), + softContext: + inference.soft_context_percent === undefined + ? "" + : String(inference.soft_context_percent), }, }; } diff --git a/frontend/packages/swarm-ui/test/create-agent-archetype.test.mjs b/frontend/packages/swarm-ui/test/create-agent-archetype.test.mjs index 0284db724..ad1d59a17 100644 --- a/frontend/packages/swarm-ui/test/create-agent-archetype.test.mjs +++ b/frontend/packages/swarm-ui/test/create-agent-archetype.test.mjs @@ -15,7 +15,13 @@ const empty = { permissions: defaults, cpu: "", memory: "", - inference: { model: "", effort: "", subagents: "", maxSubagents: "" }, + inference: { + model: "", + effort: "", + subagents: "", + maxSubagents: "", + softContext: "", + }, }; test("no archetype is the default permissions and empty settings", () => { @@ -34,6 +40,7 @@ test("an archetype's settings fill the form", () => { effort: "high", subagent_models: ["haiku", "sonnet"], max_subagents: 0, + soft_context_percent: 20, }, }; assert.deepEqual(prefill(reviewer, defaults), { @@ -45,6 +52,7 @@ test("an archetype's settings fill the form", () => { effort: "high", subagents: "haiku, sonnet", maxSubagents: "0", + softContext: "20", }, }); }); diff --git a/frontend/packages/swarm-ui/test/soft-context.test.mjs b/frontend/packages/swarm-ui/test/soft-context.test.mjs new file mode 100644 index 000000000..8c5fd3f2f --- /dev/null +++ b/frontend/packages/swarm-ui/test/soft-context.test.mjs @@ -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); + } +});