Watch
0
0
Fork
You've already forked hyperhive
0

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:
iris 2026-11-15 15:16:59 +01:00 • committed by mara
commit e9af6cf2c7
5 changed files with 109 additions and 7 deletions

View file

@ -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 })}
/>
<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}
</>
);
@ -278,11 +300,22 @@ export function AgentInference({ agent }: { agent: string }) {
}
/>
</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>
<Badge
variant="quiet"
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)}
/>
</div>
@ -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<ProblemDetails | null>(null);

View 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;
}

View file

@ -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),
},
};
}

View file

@ -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",
},
});
});

View 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);
}
});