@intentic/sandbox-contract 1.331.0 → 1.332.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/chores/probes.d.ts.map +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/contracts/accounts.contract.d.ts +11 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +37 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +14 -3
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +29 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +2 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +7 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +2 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts +10 -0
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +7 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.d.ts +70 -0
- package/dist/contracts/offload.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.js +10 -1
- package/dist/contracts/offload.contract.js.map +1 -1
- package/dist/contracts/privacy.contract.d.ts +14 -0
- package/dist/contracts/privacy.contract.d.ts.map +1 -1
- package/dist/contracts/privacy.contract.js +11 -1
- package/dist/contracts/privacy.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +3 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +103 -2
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +72 -94
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +19 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +17 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +4 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +46 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +6 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +1 -1
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/resume.d.ts +3 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +17 -2
- package/dist/events/resume.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +18 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +27 -1
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +2 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +64 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +17 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +313 -124
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +8 -7
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts +3 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +10 -0
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/model-order.d.ts +1 -0
- package/dist/models/model-order.d.ts.map +1 -1
- package/dist/models/model-order.js +9 -0
- package/dist/models/model-order.js.map +1 -1
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/model-roles.d.ts +25 -18
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +23 -27
- package/dist/models/model-roles.js.map +1 -1
- package/dist/models/model-setups.d.ts +46 -0
- package/dist/models/model-setups.d.ts.map +1 -0
- package/dist/models/model-setups.js +86 -0
- package/dist/models/model-setups.js.map +1 -0
- package/dist/models/plan-pools.d.ts +3 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +18 -6
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/models/provider-specs.d.ts +20 -0
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js +13 -1
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +26 -3
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/credential-material.d.ts +6 -0
- package/dist/policy/credential-material.d.ts.map +1 -1
- package/dist/policy/credential-material.js +37 -15
- package/dist/policy/credential-material.js.map +1 -1
- package/dist/policy/search-globs.d.ts +3 -0
- package/dist/policy/search-globs.d.ts.map +1 -1
- package/dist/policy/search-globs.js +118 -0
- package/dist/policy/search-globs.js.map +1 -1
- package/dist/policy/secret-hosts.d.ts +1 -0
- package/dist/policy/secret-hosts.d.ts.map +1 -1
- package/dist/policy/secret-hosts.js +1 -0
- package/dist/policy/secret-hosts.js.map +1 -1
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +1 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +9 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +5 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/pairing-redeem.d.ts +23 -0
- package/dist/protocol/pairing-redeem.d.ts.map +1 -0
- package/dist/protocol/pairing-redeem.js +44 -0
- package/dist/protocol/pairing-redeem.js.map +1 -0
- package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
- package/dist/protocol/peer-mcp-server.js +1 -1
- package/dist/protocol/peer-mcp-server.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +16 -0
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +2 -0
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +8 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +2 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/protocol/sse.d.ts +20 -1
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +112 -17
- package/dist/protocol/sse.js.map +1 -1
- package/dist/schemas/agent.d.ts +32 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +12 -1
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/automations.d.ts +6 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +3 -1
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/devices.d.ts +17 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +2 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/engines.d.ts +8 -1
- package/dist/schemas/engines.d.ts.map +1 -1
- package/dist/schemas/engines.js +1 -1
- package/dist/schemas/engines.js.map +1 -1
- package/dist/schemas/git/git-scope.d.ts +20 -0
- package/dist/schemas/git/git-scope.d.ts.map +1 -0
- package/dist/schemas/git/git-scope.js +37 -0
- package/dist/schemas/git/git-scope.js.map +1 -0
- package/dist/schemas/git/git.d.ts +1 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +9 -1
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +42 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +3 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +1 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -1
- package/dist/schemas/keep-warm.js +1 -1
- package/dist/schemas/keep-warm.js.map +1 -1
- package/dist/schemas/logs.d.ts +21 -0
- package/dist/schemas/logs.d.ts.map +1 -1
- package/dist/schemas/logs.js +19 -0
- package/dist/schemas/logs.js.map +1 -1
- package/dist/schemas/metrics.d.ts +12 -0
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +26 -8
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/needs.d.ts +19 -0
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +17 -3
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/offload.d.ts +154 -0
- package/dist/schemas/offload.d.ts.map +1 -1
- package/dist/schemas/offload.js +34 -0
- package/dist/schemas/offload.js.map +1 -1
- package/dist/schemas/privacy.d.ts +19 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +17 -2
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/project-bring-back.d.ts +172 -0
- package/dist/schemas/project-bring-back.d.ts.map +1 -0
- package/dist/schemas/project-bring-back.js +39 -0
- package/dist/schemas/project-bring-back.js.map +1 -0
- package/dist/schemas/providers/handoff.d.ts +77 -0
- package/dist/schemas/providers/handoff.d.ts.map +1 -0
- package/dist/schemas/providers/handoff.js +85 -0
- package/dist/schemas/providers/handoff.js.map +1 -0
- package/dist/schemas/providers/plan-limits.d.ts +27 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +8 -1
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +4 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/usage.d.ts +3 -1
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +7 -2
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/run-targets.d.ts.map +1 -1
- package/dist/schemas/run-targets.js +5 -2
- package/dist/schemas/run-targets.js.map +1 -1
- package/dist/schemas/settings-history.d.ts +1 -1
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +21 -0
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +67 -100
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +41 -24
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/short-codes.d.ts +31 -0
- package/dist/schemas/short-codes.d.ts.map +1 -0
- package/dist/schemas/short-codes.js +28 -0
- package/dist/schemas/short-codes.js.map +1 -0
- package/dist/schemas/system.d.ts +130 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +37 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/state/definition.d.ts +64 -40
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +1 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/versions.d.ts +6 -0
- package/dist/state/versions.d.ts.map +1 -1
- package/dist/state/versions.js +47 -0
- package/dist/state/versions.js.map +1 -1
- package/dist/text/embed.d.ts +2 -1
- package/dist/text/embed.d.ts.map +1 -1
- package/dist/text/embed.js +10 -5
- package/dist/text/embed.js.map +1 -1
- package/dist/text/title.d.ts.map +1 -1
- package/dist/text/title.js +4 -7
- package/dist/text/title.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +2 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -6
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +28 -6
- package/src/chores/probes.ts +3 -1
- package/src/contracts/agent.contract.ts +24 -3
- package/src/contracts/offload.contract.ts +21 -1
- package/src/contracts/privacy.contract.ts +13 -0
- package/src/contracts/settings.contract.ts +23 -0
- package/src/events/agent-events.ts +13 -0
- package/src/events/agent-words.ts +2 -3
- package/src/events/resume.test.ts +14 -2
- package/src/events/resume.ts +32 -3
- package/src/events/sandbox-notice.ts +31 -1
- package/src/events/transcript.ts +27 -2
- package/src/index.ts +5 -0
- package/src/models/agent-catalog.test.ts +4 -3
- package/src/models/agent-catalog.ts +10 -7
- package/src/models/agent-runtimes.ts +30 -1
- package/src/models/capability-ledger.test.ts +4 -0
- package/src/models/model-order.ts +13 -0
- package/src/models/model-pins.ts +5 -4
- package/src/models/model-roles.test.ts +9 -31
- package/src/models/model-roles.ts +35 -50
- package/src/models/model-setups.test.ts +100 -0
- package/src/models/model-setups.ts +151 -0
- package/src/models/plan-pools.test.ts +58 -0
- package/src/models/plan-pools.ts +38 -7
- package/src/models/provider-specs.ts +20 -2
- package/src/policy/command-classes.ts +34 -3
- package/src/policy/credential-material.ts +58 -16
- package/src/policy/search-globs.ts +135 -0
- package/src/policy/secret-hosts.ts +7 -0
- package/src/policy/tests/command-classes.test.ts +35 -0
- package/src/policy/tests/credential-material.test.ts +20 -0
- package/src/policy/tests/search-globs.test.ts +46 -1
- package/src/policy/tests/secret-hosts.test.ts +17 -1
- package/src/policy/turned-away.ts +2 -0
- package/src/protocol/doors/runner-protocol.ts +14 -1
- package/src/protocol/pairing-redeem.test.ts +86 -0
- package/src/protocol/pairing-redeem.ts +79 -0
- package/src/protocol/peer-mcp-server.ts +1 -2
- package/src/protocol/raw/raw-json-routes.ts +3 -0
- package/src/protocol/raw/raw-routes.ts +5 -0
- package/src/protocol/sse.test.ts +50 -1
- package/src/protocol/sse.ts +172 -26
- package/src/schemas/agent.ts +23 -2
- package/src/schemas/automations.ts +6 -2
- package/src/schemas/daemon-health.test.ts +28 -0
- package/src/schemas/devices.ts +11 -2
- package/src/schemas/engines.ts +1 -1
- package/src/schemas/git/git-scope.test.ts +91 -0
- package/src/schemas/git/git-scope.ts +71 -0
- package/src/schemas/git/git.ts +15 -1
- package/src/schemas/hosts.ts +7 -0
- package/src/schemas/keep-warm.ts +2 -1
- package/src/schemas/logs.ts +25 -0
- package/src/schemas/metrics.test.ts +80 -0
- package/src/schemas/metrics.ts +31 -9
- package/src/schemas/needs.ts +29 -3
- package/src/schemas/offload.ts +76 -0
- package/src/schemas/privacy.ts +27 -3
- package/src/schemas/project-bring-back.test.ts +15 -0
- package/src/schemas/project-bring-back.ts +78 -0
- package/src/schemas/providers/handoff.test.ts +42 -0
- package/src/schemas/providers/handoff.ts +128 -0
- package/src/schemas/providers/plan-limits.ts +14 -1
- package/src/schemas/providers/usage.ts +13 -7
- package/src/schemas/run-targets.ts +5 -2
- package/src/schemas/settings-history.ts +28 -0
- package/src/schemas/settings.test.ts +74 -21
- package/src/schemas/settings.ts +74 -44
- package/src/schemas/short-codes.ts +48 -0
- package/src/schemas/system.ts +63 -1
- package/src/state/history-state.ts +4 -0
- package/src/state/runtime-state.test.ts +4 -0
- package/src/state/runtime-state.ts +4 -0
- package/src/state/versions.test.ts +30 -1
- package/src/state/versions.ts +68 -0
- package/src/text/embed.test.ts +14 -0
- package/src/text/embed.ts +19 -10
- package/src/text/title.ts +5 -9
- package/src/text/transcript-fold.test.ts +61 -0
- package/src/text/transcript-fold.ts +31 -6
|
@@ -8,7 +8,7 @@ export type ExecutionBackend = "shell" | "js";
|
|
|
8
8
|
|
|
9
9
|
export interface AgentCapabilities {
|
|
10
10
|
// Which loop serves the turn; `opencode-gemini` stays separate since adapter health keys off this field.
|
|
11
|
-
readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi" | "cursor";
|
|
11
|
+
readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "devin" | "pi" | "cursor";
|
|
12
12
|
// Mid-turn injection (the SteeringQueue behind /agent/steer); needs the SDK's streaming-input mode.
|
|
13
13
|
readonly steering: boolean;
|
|
14
14
|
// How much of the permission-mode axis the runtime honours: every PermissionMode, or just propose-then-approve.
|
|
@@ -59,6 +59,10 @@ export interface AgentCapabilities {
|
|
|
59
59
|
// configuration the daemon does not own and reads files it never sees. Only a `none` loop on an untrusted provider is
|
|
60
60
|
// refused while the shield is on.
|
|
61
61
|
readonly privacy: "gateway" | "hooks" | "none";
|
|
62
|
+
// Whether a turn's tool list (TurnPolicy.allowedTools, an automation's `allowedTools`) is a real boundary here:
|
|
63
|
+
// every tool call the loop makes passes a check the daemon answers, or the loop is built with only the listed tools.
|
|
64
|
+
// A runtime that runs some tool without asking anyone cannot keep it, so a turn carrying a list never starts on one.
|
|
65
|
+
readonly toolAllowlist: boolean;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
// The Claude Code Agent SDK loop, the ceiling every other runtime is measured against: the only one that owns the whole
|
|
@@ -85,6 +89,9 @@ export const CLAUDE_CODE: AgentCapabilities = {
|
|
|
85
89
|
secrets: "masked",
|
|
86
90
|
// Every request goes to ANTHROPIC_BASE_URL, which the daemon sets per turn.
|
|
87
91
|
privacy: "gateway",
|
|
92
|
+
// A PreToolUse hook refuses every call off the list, under bypassPermissions and in subagents too
|
|
93
|
+
// (agent/tools/tool-allowlist.ts).
|
|
94
|
+
toolAllowlist: true,
|
|
88
95
|
};
|
|
89
96
|
|
|
90
97
|
// Codex app-server: item-level events, the turn's remote MCP servers over http, and four seams (steer, a question
|
|
@@ -112,6 +119,9 @@ export const CODEX: AgentCapabilities = {
|
|
|
112
119
|
secrets: "none",
|
|
113
120
|
// Its model provider's base_url is the daemon's, written into the per-thread config.
|
|
114
121
|
privacy: "gateway",
|
|
122
|
+
// Codex runs the commands it judges read-only without asking (its known-safe list), and every tool it has is the
|
|
123
|
+
// shell, so no answer the daemon gives can keep a call off a list.
|
|
124
|
+
toolAllowlist: false,
|
|
115
125
|
};
|
|
116
126
|
|
|
117
127
|
// OpenCode (the Grok runtime): its own agentic loop and tools, with the turn's remote MCP servers mounted on the shared
|
|
@@ -140,6 +150,10 @@ export const OPENCODE: AgentCapabilities = {
|
|
|
140
150
|
secrets: "none",
|
|
141
151
|
// The shared server's provider baseURLs are written by the daemon at spawn.
|
|
142
152
|
privacy: "gateway",
|
|
153
|
+
// Its session rules could deny every action off the list, but they apply after the server's and the last match wins,
|
|
154
|
+
// so allowing a listed shell command there would override the owner's rulebook asking about it. Not kept until the
|
|
155
|
+
// permission judge (opencode-permissions.ts) checks the list on every call instead.
|
|
156
|
+
toolAllowlist: false,
|
|
143
157
|
};
|
|
144
158
|
|
|
145
159
|
// The same OpenCode loop, serving Gemini instead of xAI, identical abilities. The Claude Code loop announces itself in
|
|
@@ -173,6 +187,16 @@ export const ACP: AgentCapabilities = {
|
|
|
173
187
|
secrets: "none",
|
|
174
188
|
// The agent picks its own provider and endpoint from its own login and environment.
|
|
175
189
|
privacy: "none",
|
|
190
|
+
// Which calls the agent asks permission for is its own choice, and most run reads without asking.
|
|
191
|
+
toolAllowlist: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Devin's own CLI (`devin acp`), the ACP floor with one seam an installed agent lacks: the daemon picks the model, from
|
|
195
|
+
// the account's own list, through the session's model option. Its own runtime name, as OpenCode's Gemini twin has, since
|
|
196
|
+
// adapter health keys off this field and Devin's readiness is an account and an engine, not an installed card.
|
|
197
|
+
export const DEVIN: AgentCapabilities = {
|
|
198
|
+
...ACP,
|
|
199
|
+
runtime: "devin",
|
|
176
200
|
};
|
|
177
201
|
|
|
178
202
|
// Pi driven over its RPC mode: above the ACP floor and below the Claude Code ceiling. Real mid-turn steering, an effort
|
|
@@ -199,6 +223,8 @@ export const PI: AgentCapabilities = {
|
|
|
199
223
|
secrets: "none",
|
|
200
224
|
// Pi calls its provider from its own login; the daemon names no endpoint.
|
|
201
225
|
privacy: "none",
|
|
226
|
+
// No seam before a tool runs.
|
|
227
|
+
toolAllowlist: false,
|
|
202
228
|
};
|
|
203
229
|
|
|
204
230
|
// Cursor's own runtime, driven through `@cursor/sdk` from this daemon's process; an embedding surface, so most seams
|
|
@@ -242,4 +268,7 @@ export const CURSOR: AgentCapabilities = {
|
|
|
242
268
|
// to mask the output, the tools that read past both (grep, ls, glob, semantic search, fetch, lints) are withheld,
|
|
243
269
|
// and MCP goes through the masking proxy (runtimes/cursor/cursor-shield.ts).
|
|
244
270
|
privacy: "hooks",
|
|
271
|
+
// Its hooks are installed only for a shielded turn, and a hook that cannot tell which turn is asking allows; the
|
|
272
|
+
// built-in tool set is withheld by name, not granted. None of that is a list nothing gets past.
|
|
273
|
+
toolAllowlist: false,
|
|
245
274
|
};
|
|
@@ -37,6 +37,10 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
|
|
|
37
37
|
// agent/run/decide/turn-decision.ts refuses a "none" runtime on an untrusted provider while the privacy shield is
|
|
38
38
|
// on; "gateway" runtimes get the daemon's gateway as their base URL (privacy/gateway).
|
|
39
39
|
privacy: "enforced",
|
|
40
|
+
// agent/run/decide/turn-decision.ts refuses a turn carrying a tool list on a runtime that cannot keep one, the
|
|
41
|
+
// automation ladder passes such rungs over, and agent/tools/tool-allowlist.ts refuses every off-list call on the
|
|
42
|
+
// Claude Code loop.
|
|
43
|
+
toolAllowlist: "enforced",
|
|
40
44
|
|
|
41
45
|
// Descriptive: true of the runtime but nothing consults these; wiring one up moves it to enforced.
|
|
42
46
|
questions: "descriptive",
|
|
@@ -147,3 +147,16 @@ export const compareUnrankedModelIds = (left: string, right: string): number =>
|
|
|
147
147
|
// True only for an id that spells out a level above the quiet end, so an unannotated id is never accused of it.
|
|
148
148
|
// Exported so a settings row can label a pinned thinking variant.
|
|
149
149
|
export const namesThinking = (id: string): boolean => thinkingRankOf(familyOf(id)) > UNSTATED_THINKING;
|
|
150
|
+
|
|
151
|
+
// Which of three rungs an id stands on: 0 the frontier, 1 the everyday workhorse, 2 the cheap end. A tier word decides
|
|
152
|
+
// where there is one, a release-local ladder (sol, terra, luna) where there is not, and an id with neither is read as
|
|
153
|
+
// the frontier, as the catalog order already leads with it. What the preset setups are worked out from.
|
|
154
|
+
export const modelRungOf = (id: string): number => {
|
|
155
|
+
const family = familyOf(id);
|
|
156
|
+
const tier = tierRankOf(family);
|
|
157
|
+
if (tier !== UNRANKED) {
|
|
158
|
+
return tier;
|
|
159
|
+
}
|
|
160
|
+
const release = releaseTierRankOf(family);
|
|
161
|
+
return release === UNRANKED ? 0 : release;
|
|
162
|
+
};
|
package/src/models/model-pins.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AgentProvider, ModelPin } from "../schemas/agent.js";
|
|
2
2
|
|
|
3
|
-
// The order to try models in for a
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// The order to try models in for a job's setup (model-setups.ts), shared by daemon and browser so both agree what a job
|
|
4
|
+
// will spend before it runs. An empty list resolves to nothing: the caller decides what that means (a helper is off; a
|
|
5
|
+
// run falls back to the chat's own pick).
|
|
6
6
|
|
|
7
7
|
// One provider's standing in the decision: can a turn go, and what its catalog holds. ACP agents have no rung here (an
|
|
8
8
|
// empty model id), but endpoint/<id> providers must be included, matching the picker's own options.
|
|
@@ -11,7 +11,8 @@ export interface ModelSource {
|
|
|
11
11
|
readonly provider: AgentProvider;
|
|
12
12
|
// Same connection predicate every surface gates on; a nonempty catalog does not imply ready to send.
|
|
13
13
|
readonly ready: boolean;
|
|
14
|
-
//
|
|
14
|
+
// The models it can run a turn on, newest-first order not assumed. readyChain never reads it (a pin is verbatim);
|
|
15
|
+
// a preset setup is worked out from it (model-setups.ts `presetPins`), so a helper-only model is left out.
|
|
15
16
|
readonly models: readonly string[];
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -1,37 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MODEL_ROLES, PRESET_SETUPS } from "./model-roles.js";
|
|
2
2
|
|
|
3
|
-
// The catalog draws the settings page directly, so its properties
|
|
4
|
-
//
|
|
3
|
+
// The catalog draws the settings page directly, so its properties must hold of the table itself, not be remembered by
|
|
4
|
+
// whoever adds a row.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
6
|
+
test("the page reads one-shots first, then whole sessions, in the table's own order", () => {
|
|
7
|
+
const kinds = MODEL_ROLES.map((role) => role.kind);
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const blockOf = new Map(MODEL_ROLE_BLOCKS.flatMap((block) => block.roles.map((role) => [role.id, block.id] as const)));
|
|
11
|
-
|
|
12
|
-
for (const role of roles) {
|
|
13
|
-
expect(role.kind === `run` ? role.trigger : role.kind, role.id).toBe(blockOf.get(role.id));
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("a one-shot declares no trigger at all: nothing presses a commit subject into being", () => {
|
|
18
|
-
for (const helper of roles.filter((role) => role.kind === `helper`)) {
|
|
19
|
-
expect(Object.keys(helper), helper.id).not.toContain(`trigger`);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("the blocks hold every role once, in the table's own order", () => {
|
|
24
|
-
const blocked = MODEL_ROLE_BLOCKS.flatMap((block) => block.roles.map((role) => role.id));
|
|
25
|
-
|
|
26
|
-
expect(blocked.toSorted()).toEqual(MODEL_ROLES.map((role) => role.id).toSorted());
|
|
27
|
-
// The block order mirrors reach ordering (unpicked jobs, then click-started sessions, then auto-started ones).
|
|
28
|
-
expect(blocked).toEqual(MODEL_ROLES.map((role) => role.id));
|
|
9
|
+
expect(kinds.indexOf(`run`)).toBe(kinds.lastIndexOf(`helper`) + 1);
|
|
29
10
|
});
|
|
30
11
|
|
|
31
|
-
test("
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// A block with nothing in it would draw a heading over an empty surface.
|
|
35
|
-
expect(block.roles.length, block.id).toBeGreaterThan(1);
|
|
36
|
-
}
|
|
12
|
+
test("every job is suggested a preset, and the cheap one is never suggested for a whole session", () => {
|
|
13
|
+
expect(MODEL_ROLES.every((role) => PRESET_SETUPS.includes(role.setup))).toBe(true);
|
|
14
|
+
expect(new Set<string>(MODEL_ROLES.filter((role) => role.kind === `run`).map((role) => role.setup)).has(`low`)).toBe(false);
|
|
37
15
|
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
// Every job that picks a model, one row per job; the unit is the role, not a difficulty tier.
|
|
4
|
-
//
|
|
5
|
-
// role is one row here, settings, resolver
|
|
3
|
+
// Every job that picks a model, one row per job; the unit is the role, not a difficulty tier. A job runs on the setup it
|
|
4
|
+
// was put on (model-setups.ts), an ordered fallback ladder it shares with every other job put there; a job on none, or
|
|
5
|
+
// on an empty one, is off (helper) or falls back to the caller's own chat model (run). Adding a role is one row here, settings, resolver
|
|
6
|
+
// and daemon all read this table.
|
|
6
7
|
|
|
7
8
|
export const ModelRoleKindSchema = z.enum(["helper", "run"]);
|
|
8
9
|
export type ModelRoleKind = z.infer<typeof ModelRoleKindSchema>;
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
export
|
|
11
|
+
// The three setups every sandbox has, cheapest first: worked out from what is connected until the owner edits one
|
|
12
|
+
// (model-setups.ts `presetPins`). Ids are stored, so they are wire-stable.
|
|
13
|
+
export const PRESET_SETUPS = ["low", "medium", "high"] as const;
|
|
14
|
+
export type PresetSetup = (typeof PRESET_SETUPS)[number];
|
|
13
15
|
|
|
14
16
|
// `id` is a bare string here; narrowed to the role union on `ModelRoleSpec` below, since that union derives from this
|
|
15
17
|
// table via `satisfies`.
|
|
@@ -20,13 +22,14 @@ interface ModelRoleRow {
|
|
|
20
22
|
// Must add information beyond the label, not restate it.
|
|
21
23
|
readonly blurb: string;
|
|
22
24
|
readonly kind: ModelRoleKind;
|
|
23
|
-
/** Required when `kind` is "run"; unset for helpers. */
|
|
24
|
-
readonly trigger?: ModelRoleTrigger;
|
|
25
25
|
// Icon name from the shared icon set; kept here, not in a separate web-side map keyed by id.
|
|
26
26
|
readonly icon: string;
|
|
27
|
+
// The preset suggested for this job, offered when it is placed: what the work needs, not what it costs today. Only
|
|
28
|
+
// a suggestion: a job runs on nothing until someone puts it on a setup.
|
|
29
|
+
readonly setup: PresetSetup;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
// Ids are wire-stable (`AgentTurn.runRole`); order
|
|
32
|
+
// Ids are wire-stable (`AgentTurn.runRole`); order is the settings page's: one-shot helpers first, then whole sessions.
|
|
30
33
|
export const MODEL_ROLES = [
|
|
31
34
|
{
|
|
32
35
|
id: "commit-message",
|
|
@@ -34,6 +37,7 @@ export const MODEL_ROLES = [
|
|
|
34
37
|
blurb: "The subject written when an agent's work lands, and the release note under it.",
|
|
35
38
|
kind: "helper",
|
|
36
39
|
icon: "file-edit",
|
|
40
|
+
setup: "low",
|
|
37
41
|
},
|
|
38
42
|
{
|
|
39
43
|
id: "session-title",
|
|
@@ -41,6 +45,7 @@ export const MODEL_ROLES = [
|
|
|
41
45
|
blurb: "The name a conversation wears on the board, written a second into its first turn.",
|
|
42
46
|
kind: "helper",
|
|
43
47
|
icon: "pencil",
|
|
48
|
+
setup: "low",
|
|
44
49
|
},
|
|
45
50
|
{
|
|
46
51
|
// Prompt includes the command about to run, which may be attacker-controlled; a small model can be swayed.
|
|
@@ -49,6 +54,7 @@ export const MODEL_ROLES = [
|
|
|
49
54
|
blurb: "Which model reads your safety policy before a flagged command runs.",
|
|
50
55
|
kind: "helper",
|
|
51
56
|
icon: "shield",
|
|
57
|
+
setup: "medium",
|
|
52
58
|
},
|
|
53
59
|
{
|
|
54
60
|
id: "loop-verdict",
|
|
@@ -56,6 +62,16 @@ export const MODEL_ROLES = [
|
|
|
56
62
|
blurb: "Whether a loop's iteration met the goal, or the loop goes round again.",
|
|
57
63
|
kind: "helper",
|
|
58
64
|
icon: "check-square",
|
|
65
|
+
setup: "medium",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
// Reads a whole conversation (capped tool output) once, so a small model with a large window suits it best.
|
|
69
|
+
id: "handoff-summary",
|
|
70
|
+
label: "Hand-off summaries",
|
|
71
|
+
blurb: "The summary a fresh session opens with when you choose to summarise a long conversation that hit a usage limit, instead of re-reading it all.",
|
|
72
|
+
kind: "helper",
|
|
73
|
+
icon: "align-left",
|
|
74
|
+
setup: "low",
|
|
59
75
|
},
|
|
60
76
|
{
|
|
61
77
|
// One reading answering both halves of "what does this chat open on": an offered `provider:model` line with an
|
|
@@ -67,79 +83,79 @@ export const MODEL_ROLES = [
|
|
|
67
83
|
blurb: "Which model reads a new chat's first message and picks what it opens on: the model, effort and account, and the persona.",
|
|
68
84
|
kind: "helper",
|
|
69
85
|
icon: "sparkles",
|
|
86
|
+
setup: "low",
|
|
70
87
|
},
|
|
71
88
|
{
|
|
72
89
|
id: "pipeline-fix",
|
|
73
90
|
label: "Pipeline fixes",
|
|
74
91
|
blurb: "The agent started by Fix on a failed pipeline.",
|
|
75
92
|
kind: "run",
|
|
76
|
-
trigger: "pressed",
|
|
77
93
|
icon: "wave-pulse",
|
|
94
|
+
setup: "high",
|
|
78
95
|
},
|
|
79
96
|
{
|
|
80
97
|
id: "deployment-fix",
|
|
81
98
|
label: "Deployment fixes",
|
|
82
99
|
blurb: "The agent started by Fix on a deployment that is down.",
|
|
83
100
|
kind: "run",
|
|
84
|
-
trigger: "pressed",
|
|
85
101
|
icon: "server",
|
|
102
|
+
setup: "high",
|
|
86
103
|
},
|
|
87
104
|
{
|
|
88
105
|
id: "maintenance-chore",
|
|
89
106
|
label: "Maintenance chores",
|
|
90
107
|
blurb: "A chore run started from the Maintenance board.",
|
|
91
108
|
kind: "run",
|
|
92
|
-
trigger: "pressed",
|
|
93
109
|
icon: "wrench",
|
|
110
|
+
setup: "medium",
|
|
94
111
|
},
|
|
95
112
|
{
|
|
96
113
|
id: "refactor-run",
|
|
97
114
|
label: "Refactors",
|
|
98
115
|
blurb: "The agent started from a hotspot or key module on a repository's Health tab.",
|
|
99
116
|
kind: "run",
|
|
100
|
-
trigger: "pressed",
|
|
101
117
|
icon: "hammer",
|
|
118
|
+
setup: "high",
|
|
102
119
|
},
|
|
103
120
|
{
|
|
104
121
|
id: "documentation-run",
|
|
105
122
|
label: "Documentation runs",
|
|
106
123
|
blurb: "A pass over a repo's own documentation.",
|
|
107
124
|
kind: "run",
|
|
108
|
-
trigger: "pressed",
|
|
109
125
|
icon: "book",
|
|
126
|
+
setup: "medium",
|
|
110
127
|
},
|
|
111
128
|
{
|
|
112
129
|
id: "acceptance-run",
|
|
113
130
|
label: "Acceptance runs",
|
|
114
131
|
blurb: "One session per story in an acceptance fan-out.",
|
|
115
132
|
kind: "run",
|
|
116
|
-
trigger: "pressed",
|
|
117
133
|
icon: "list-check",
|
|
134
|
+
setup: "high",
|
|
118
135
|
},
|
|
119
136
|
{
|
|
120
|
-
// Pressed trigger: the owner's approval click starts this turn; the queue between is just machinery.
|
|
121
137
|
id: "approval-queue",
|
|
122
138
|
label: "Approvals queue",
|
|
123
139
|
blurb: "The turn that publishes or acts on what you approved.",
|
|
124
140
|
kind: "run",
|
|
125
|
-
trigger: "pressed",
|
|
126
141
|
icon: "check-circle",
|
|
142
|
+
setup: "medium",
|
|
127
143
|
},
|
|
128
144
|
{
|
|
129
145
|
id: "extension-review",
|
|
130
146
|
label: "Extension update reviews",
|
|
131
147
|
blurb: "The agent that reads an extension update before it is applied.",
|
|
132
148
|
kind: "run",
|
|
133
|
-
trigger: "unprompted",
|
|
134
149
|
icon: "box",
|
|
150
|
+
setup: "medium",
|
|
135
151
|
},
|
|
136
152
|
{
|
|
137
153
|
id: "loop-iteration",
|
|
138
154
|
label: "Loop iterations",
|
|
139
155
|
blurb: "Each round of a loop working towards its goal.",
|
|
140
156
|
kind: "run",
|
|
141
|
-
trigger: "unprompted",
|
|
142
157
|
icon: "repeat",
|
|
158
|
+
setup: "high",
|
|
143
159
|
},
|
|
144
160
|
] as const satisfies readonly ModelRoleRow[];
|
|
145
161
|
|
|
@@ -150,34 +166,3 @@ export const MODEL_ROLE_IDS = MODEL_ROLES.map((role) => role.id) as readonly Mod
|
|
|
150
166
|
|
|
151
167
|
// Enum, not a string: an id outside this table names nothing, so it should fail loudly rather than be ignored.
|
|
152
168
|
export const ModelRoleSchema = z.enum(MODEL_ROLE_IDS as [ModelRole, ...ModelRole[]]);
|
|
153
|
-
|
|
154
|
-
// Groups MODEL_ROLES for the settings page by kind and trigger; each role belongs to exactly one block, in table order.
|
|
155
|
-
export type ModelRoleBlockId = "helper" | "pressed" | "unprompted";
|
|
156
|
-
|
|
157
|
-
export interface ModelRoleBlock {
|
|
158
|
-
readonly id: ModelRoleBlockId;
|
|
159
|
-
/** Heading shown on the settings page. */
|
|
160
|
-
readonly label: string;
|
|
161
|
-
/** Its roles, in MODEL_ROLES order. */
|
|
162
|
-
readonly roles: readonly ModelRoleSpec[];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const rolesWhere = (match: (role: ModelRoleSpec) => boolean): readonly ModelRoleSpec[] => MODEL_ROLES.filter((role) => match(role));
|
|
166
|
-
|
|
167
|
-
export const MODEL_ROLE_BLOCKS: readonly ModelRoleBlock[] = [
|
|
168
|
-
{
|
|
169
|
-
id: "helper",
|
|
170
|
-
label: "Automatic helpers",
|
|
171
|
-
roles: rolesWhere((role) => role.kind === "helper"),
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
id: "pressed",
|
|
175
|
-
label: "Runs you start",
|
|
176
|
-
roles: rolesWhere((role) => role.kind === "run" && role.trigger === "pressed"),
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
id: "unprompted",
|
|
180
|
-
label: "Runs that start themselves",
|
|
181
|
-
roles: rolesWhere((role) => role.kind === "run" && role.trigger === "unprompted"),
|
|
182
|
-
},
|
|
183
|
-
];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ModelPin } from "../schemas/agent.js";
|
|
2
|
+
import type { ModelSource } from "./model-pins.js";
|
|
3
|
+
import { endpointProvider } from "./agent-catalog.js";
|
|
4
|
+
import type { ModelRole } from "./model-roles.js";
|
|
5
|
+
import { ladderIsEmpty, presetPins, roleLadder, rolePins, roleSetupId, setupsFromRoleLists } from "./model-setups.js";
|
|
6
|
+
|
|
7
|
+
// What a job runs on is read here by the daemon and the browser alike, so the rules are pinned once: where a job starts,
|
|
8
|
+
// what a preset is worked out from, and what a written setup overrides.
|
|
9
|
+
|
|
10
|
+
const opus: ModelPin = { provider: `claude`, model: `claude-opus-5-5`, effort: `high` };
|
|
11
|
+
const haiku: ModelPin = { provider: `claude`, model: `claude-haiku-4-5` };
|
|
12
|
+
|
|
13
|
+
const CLAUDE: ModelSource = { provider: `claude`, ready: true, models: [`claude-haiku-4-5-20251001`, `claude-opus-4-8`, `claude-opus-5`, `claude-sonnet-5`] };
|
|
14
|
+
const CODEX: ModelSource = { provider: `codex`, ready: true, models: [`gpt-5.2-codex`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5-mini`] };
|
|
15
|
+
const GEMINI: ModelSource = { provider: `gemini`, ready: true, models: [`gemini-3-flash`, `gemini-3-flash-lite`, `gemini-3-pro`] };
|
|
16
|
+
|
|
17
|
+
const NOTHING_SET = { modelSetups: [], roleSetups: {} };
|
|
18
|
+
|
|
19
|
+
describe(`a preset worked out from what is connected`, () => {
|
|
20
|
+
test(`takes each connected provider's newest model on the preset's rung, in the order they come`, () => {
|
|
21
|
+
expect(presetPins(`high`, [CLAUDE, CODEX, GEMINI]).map((pin) => pin.model)).toEqual([`claude-opus-5`, `gpt-5.6-sol`, `gemini-3-pro`]);
|
|
22
|
+
expect(presetPins(`medium`, [CLAUDE, CODEX, GEMINI]).map((pin) => pin.model)).toEqual([`claude-sonnet-5`, `gpt-5.6-terra`, `gemini-3-flash`]);
|
|
23
|
+
expect(presetPins(`low`, [CLAUDE, CODEX, GEMINI]).map((pin) => pin.model)).toEqual([`claude-haiku-4-5-20251001`, `gpt-5.6-luna`, `gemini-3-flash-lite`]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test(`leaves out a provider that is not connected, a local endpoint, and anything past three`, () => {
|
|
27
|
+
const local: ModelSource = { provider: endpointProvider(`ollama`), ready: true, models: [`qwen-coder`] };
|
|
28
|
+
const extra: ModelSource = { provider: `grok`, ready: true, models: [`grok-5-heavy`] };
|
|
29
|
+
expect(presetPins(`high`, [{ ...CLAUDE, ready: false }, local, CODEX, GEMINI, extra]).map((pin) => pin.provider)).toEqual([`codex`, `gemini`, `grok`]);
|
|
30
|
+
expect(presetPins(`high`, [CLAUDE, CODEX, GEMINI, extra])).toHaveLength(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test(`takes a provider's own model over one it serves from another maker`, () => {
|
|
34
|
+
const google: ModelSource = { provider: `gemini`, ready: true, models: [`claude-opus-4-6-thinking`, `gemini-3-pro`, `gemini-3-flash`] };
|
|
35
|
+
expect(presetPins(`high`, [google])).toEqual([{ provider: `gemini`, model: `gemini-3-pro` }]);
|
|
36
|
+
// With none of its own on the rung, the borrowed one still serves.
|
|
37
|
+
expect(presetPins(`high`, [{ ...google, models: [`claude-opus-4-6-thinking`, `gemini-3-flash`, `gemini-3-flash-lite`] }])).toEqual([
|
|
38
|
+
{ provider: `gemini`, model: `claude-opus-4-6-thinking` },
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test(`is empty while nothing is connected`, () => {
|
|
43
|
+
expect(presetPins(`low`, [])).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe(`which setup a job runs on`, () => {
|
|
48
|
+
test(`a job nobody placed runs on nothing, however much is connected`, () => {
|
|
49
|
+
expect(roleSetupId(NOTHING_SET, `commit-message`)).toBeUndefined();
|
|
50
|
+
expect(ladderIsEmpty(roleLadder(NOTHING_SET, `safety-judge`))).toBe(true);
|
|
51
|
+
expect(rolePins(NOTHING_SET, `pipeline-fix`, [CLAUDE, CODEX, GEMINI])).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test(`a job put on a preset nobody edited runs what is connected, and follows it`, () => {
|
|
55
|
+
const settings = { modelSetups: [], roleSetups: { "commit-message": `low` } };
|
|
56
|
+
expect(roleLadder(settings, `commit-message`)).toEqual({ kind: `preset`, preset: `low` });
|
|
57
|
+
expect(rolePins(settings, `commit-message`, [CLAUDE])).toEqual([{ provider: `claude`, model: `claude-haiku-4-5-20251001` }]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test(`a written preset is run as written, and stops following what is connected`, () => {
|
|
61
|
+
const settings = { modelSetups: [{ id: `low`, pins: [opus] }], roleSetups: { "commit-message": `low` } };
|
|
62
|
+
expect(rolePins(settings, `commit-message`, [CLAUDE, CODEX])).toEqual([opus]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test(`a preset that was only renamed still follows what is connected`, () => {
|
|
66
|
+
const settings = { modelSetups: [{ id: `low`, name: `Cheap` }], roleSetups: { "commit-message": `low` } };
|
|
67
|
+
expect(roleLadder(settings, `commit-message`)).toEqual({ kind: `preset`, preset: `low` });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test(`a job put on a setup of the owner's runs that setup's models`, () => {
|
|
71
|
+
const settings = { modelSetups: [{ id: `setup-1`, name: `Reviews`, pins: [opus, haiku] }], roleSetups: { "commit-message": `setup-1` } };
|
|
72
|
+
expect(rolePins(settings, `commit-message`, [CLAUDE])).toEqual([opus, haiku]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test(`a job put on a setup that is gone is on none`, () => {
|
|
76
|
+
const settings = { modelSetups: [], roleSetups: { "commit-message": `setup-9` } };
|
|
77
|
+
expect(roleSetupId(settings, `commit-message`)).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test(`a setup of the owner's with no models is empty, never a preset`, () => {
|
|
81
|
+
const settings = { modelSetups: [{ id: `setup-1`, name: `Later` }], roleSetups: { "pipeline-fix": `setup-1` } };
|
|
82
|
+
expect(ladderIsEmpty(roleLadder(settings, `pipeline-fix`))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test(`per-job lists become setups every job runs exactly as before on`, () => {
|
|
87
|
+
const cases = [
|
|
88
|
+
[`commit-message`, [haiku]],
|
|
89
|
+
[`session-title`, [haiku]],
|
|
90
|
+
[`pipeline-fix`, [opus]],
|
|
91
|
+
[`safety-judge`, []],
|
|
92
|
+
] as const satisfies readonly (readonly [ModelRole, readonly ModelPin[]])[];
|
|
93
|
+
const settings = setupsFromRoleLists(Object.fromEntries(cases));
|
|
94
|
+
|
|
95
|
+
for (const [role, pins] of cases) {
|
|
96
|
+
expect(rolePins(settings, role, [CLAUDE, CODEX])).toEqual(pins);
|
|
97
|
+
}
|
|
98
|
+
// A job that never had a list, or had an empty one, ran on nothing and is on none.
|
|
99
|
+
expect([settings.roleSetups[`loop-iteration`], settings.roleSetups[`safety-judge`]]).toEqual([undefined, undefined]);
|
|
100
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ModelPin, ModelPinSchema } from "../schemas/agent.js";
|
|
3
|
+
import { isEndpointProvider } from "./agent-catalog.js";
|
|
4
|
+
import { compareModelIds, modelRungOf } from "./model-order.js";
|
|
5
|
+
import type { ModelSource } from "./model-pins.js";
|
|
6
|
+
import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRole, PRESET_SETUPS, type PresetSetup } from "./model-roles.js";
|
|
7
|
+
|
|
8
|
+
// A setup is a named, ordered list of models that any number of jobs run on: edited once, used by every job put there.
|
|
9
|
+
// Every sandbox has three, Low, Medium and High, worked out from the accounts it has connected until the owner edits
|
|
10
|
+
// one; the owner adds any others. A job runs on nothing until someone puts it on a setup: nothing spends an account, and
|
|
11
|
+
// no judge starts holding commands, because an account was connected. Each job's catalog row suggests a preset
|
|
12
|
+
// (model-roles.ts `setup`) for when it is placed. Every reader resolves a job through here, daemon and browser alike, so
|
|
13
|
+
// the settings page shows what will run.
|
|
14
|
+
|
|
15
|
+
export const MODEL_SETUP_ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
16
|
+
|
|
17
|
+
export const ModelSetupSchema = z.object({
|
|
18
|
+
id: z.string().regex(MODEL_SETUP_ID).describe("Which setup this is. `low`, `medium` and `high` are the three every sandbox has; any other is one you made."),
|
|
19
|
+
name: z.string().trim().min(1).max(40).optional().describe("What it is called on the settings page. Left out, a preset keeps its own name."),
|
|
20
|
+
pins: z
|
|
21
|
+
.array(ModelPinSchema)
|
|
22
|
+
.max(10)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(
|
|
25
|
+
"The models it runs on, tried in order, so one spent account does not take its jobs down. Left out on a preset, it is worked out from the accounts this sandbox has connected and follows them as they change; left out on one you made, it has none.",
|
|
26
|
+
),
|
|
27
|
+
});
|
|
28
|
+
export type ModelSetup = z.infer<typeof ModelSetupSchema>;
|
|
29
|
+
|
|
30
|
+
export const isPresetSetup = (id: string): id is PresetSetup => PRESET_SETUPS.some((preset) => preset === id);
|
|
31
|
+
|
|
32
|
+
// The settings a job's models are read from; a slice, so a caller holding only these two can resolve.
|
|
33
|
+
export interface SetupSettings {
|
|
34
|
+
readonly modelSetups: readonly ModelSetup[];
|
|
35
|
+
readonly roleSetups: Partial<Record<ModelRole, string>>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SUGGESTED = new Map<ModelRole, PresetSetup>(MODEL_ROLES.map((role) => [role.id, role.setup]));
|
|
39
|
+
|
|
40
|
+
/** The preset suggested for a job when it is placed. */
|
|
41
|
+
export const suggestedSetup = (role: ModelRole): PresetSetup => SUGGESTED.get(role) ?? `medium`;
|
|
42
|
+
|
|
43
|
+
// First entry wins: a hand-edited file holding one id twice means the earlier, as everywhere else a list is read.
|
|
44
|
+
export const storedSetup = (settings: Pick<SetupSettings, "modelSetups">, id: string): ModelSetup | undefined =>
|
|
45
|
+
settings.modelSetups.find((setup) => setup.id === id);
|
|
46
|
+
|
|
47
|
+
/** The setup a job runs on, or undefined while it is on none (never placed, or placed on a setup that is gone). */
|
|
48
|
+
export const roleSetupId = (settings: SetupSettings, role: ModelRole): string | undefined => {
|
|
49
|
+
const assigned = settings.roleSetups[role];
|
|
50
|
+
return assigned !== undefined && (isPresetSetup(assigned) || storedSetup(settings, assigned) !== undefined) ? assigned : undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// What a setup runs on before anything is connected or resolved: a written list, or a preset still to be worked out.
|
|
54
|
+
// A snapshot of this is what a turn carries, so it is judged by one setup however its accounts change mid-turn.
|
|
55
|
+
export type SetupLadder = { readonly kind: `pins`; readonly pins: readonly ModelPin[] } | { readonly kind: `preset`; readonly preset: PresetSetup };
|
|
56
|
+
|
|
57
|
+
const NO_MODELS: SetupLadder = { kind: `pins`, pins: [] };
|
|
58
|
+
|
|
59
|
+
export const setupLadder = (settings: Pick<SetupSettings, "modelSetups">, id: string | undefined): SetupLadder => {
|
|
60
|
+
if (id === undefined) {
|
|
61
|
+
return NO_MODELS;
|
|
62
|
+
}
|
|
63
|
+
const pins = storedSetup(settings, id)?.pins;
|
|
64
|
+
if (pins !== undefined) {
|
|
65
|
+
return { kind: `pins`, pins };
|
|
66
|
+
}
|
|
67
|
+
return isPresetSetup(id) ? { kind: `preset`, preset: id } : NO_MODELS;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const roleLadder = (settings: SetupSettings, role: ModelRole): SetupLadder => setupLadder(settings, roleSetupId(settings, role));
|
|
71
|
+
|
|
72
|
+
/** True only for a ladder that can never name a model, whatever is connected: a written list left empty. */
|
|
73
|
+
export const ladderIsEmpty = (ladder: SetupLadder): boolean => ladder.kind === `pins` && ladder.pins.length === 0;
|
|
74
|
+
|
|
75
|
+
const PRESET_RUNG = { high: 0, medium: 1, low: 2 } as const satisfies Record<PresetSetup, number>;
|
|
76
|
+
|
|
77
|
+
// Enough to step over one spent account and then another; a preset longer than this would mostly be read, not tried.
|
|
78
|
+
const PRESET_LENGTH = 3;
|
|
79
|
+
|
|
80
|
+
// The first segment most of a catalog's ids share (`gemini`, `gpt`, `claude`): the provider's own family, as opposed to
|
|
81
|
+
// a model it also serves from someone else, which Google's catalog does with Claude's.
|
|
82
|
+
const ownPrefix = (models: readonly string[]): string | undefined => {
|
|
83
|
+
const counts = new Map<string, number>();
|
|
84
|
+
for (const model of models) {
|
|
85
|
+
const prefix = model.split(/[-_]/)[0] ?? model;
|
|
86
|
+
counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
|
|
87
|
+
}
|
|
88
|
+
return [...counts].toSorted(([, a], [, b]) => b - a)[0]?.[0];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// A provider's pick for a rung: its own newest there, or a borrowed one only when it has none of its own.
|
|
92
|
+
const newestOnRung = (models: readonly string[], rung: number): string | undefined => {
|
|
93
|
+
const own = ownPrefix(models);
|
|
94
|
+
const onRung = models.filter((model) => modelRungOf(model) === rung).toSorted(compareModelIds);
|
|
95
|
+
return onRung.find((model) => model.split(/[-_]/)[0] === own) ?? onRung[0];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A preset's models, from what is connected: each connected provider's newest model of its own on the preset's rung, in
|
|
100
|
+
* the order the sources come in, at most three. `models` must hold only models that can run a turn, since a preset feeds whole
|
|
101
|
+
* sessions as well as one-shot jobs. Local endpoints are left out: what someone installed for themselves is theirs to
|
|
102
|
+
* put on a setup, not a suggestion.
|
|
103
|
+
*/
|
|
104
|
+
export const presetPins = (preset: PresetSetup, sources: readonly ModelSource[]): readonly ModelPin[] =>
|
|
105
|
+
sources
|
|
106
|
+
.filter((source) => source.ready && !isEndpointProvider(source.provider))
|
|
107
|
+
.flatMap((source) => {
|
|
108
|
+
const newest = newestOnRung(source.models, PRESET_RUNG[preset]);
|
|
109
|
+
return newest === undefined ? [] : [{ provider: source.provider, model: newest }];
|
|
110
|
+
})
|
|
111
|
+
.slice(0, PRESET_LENGTH);
|
|
112
|
+
|
|
113
|
+
export const ladderPins = (ladder: SetupLadder, sources: readonly ModelSource[]): readonly ModelPin[] =>
|
|
114
|
+
ladder.kind === `pins` ? ladder.pins : presetPins(ladder.preset, sources);
|
|
115
|
+
|
|
116
|
+
/** The models a job runs on, in try order, before readiness filters them (model-pins.ts `readyChain`). */
|
|
117
|
+
export const rolePins = (settings: SetupSettings, role: ModelRole, sources: readonly ModelSource[]): readonly ModelPin[] =>
|
|
118
|
+
ladderPins(roleLadder(settings, role), sources);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Setups for a set of per-job lists, the shape settings had before setups (2026-10-12): jobs holding the same list share
|
|
122
|
+
* one setup, a list most of whose jobs are suggested one preset becomes that preset while it is free, and the rest are
|
|
123
|
+
* new setups. A job with no list, or an empty one, ran on nothing and is left on none. Exported for the settings
|
|
124
|
+
* history and for tests that want a job on exact models.
|
|
125
|
+
*/
|
|
126
|
+
export const setupsFromRoleLists = (lists: Partial<Record<ModelRole, readonly ModelPin[]>>): SetupSettings & { modelSetups: ModelSetup[] } => {
|
|
127
|
+
const modelSetups: ModelSetup[] = [];
|
|
128
|
+
const roleSetups: Partial<Record<ModelRole, string>> = {};
|
|
129
|
+
const groups = new Map<string, ModelRole[]>();
|
|
130
|
+
for (const role of MODEL_ROLE_IDS) {
|
|
131
|
+
const list = lists[role];
|
|
132
|
+
if (list !== undefined && list.length > 0) {
|
|
133
|
+
const key = JSON.stringify(list);
|
|
134
|
+
groups.set(key, [...(groups.get(key) ?? []), role]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let made = 0;
|
|
138
|
+
for (const roles of groups.values()) {
|
|
139
|
+
const pins = lists[roles[0]!] ?? [];
|
|
140
|
+
// The preset most of these jobs are suggested; a tie goes to the first job's, the table's own order.
|
|
141
|
+
const votes = roles.map(suggestedSetup);
|
|
142
|
+
const preset = votes.toSorted((a, b) => votes.filter((vote) => vote === b).length - votes.filter((vote) => vote === a).length)[0]!;
|
|
143
|
+
const free = storedSetup({ modelSetups }, preset) === undefined;
|
|
144
|
+
const id = free ? preset : `setup-${++made}`;
|
|
145
|
+
modelSetups.push(free ? { id, pins: [...pins] } : { id, name: `Setup ${made}`, pins: [...pins] });
|
|
146
|
+
for (const role of roles) {
|
|
147
|
+
roleSetups[role] = id;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { modelSetups, roleSetups };
|
|
151
|
+
};
|