@intentic/sandbox-contract 1.223.0 → 1.225.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 +14 -14
- package/dist/agent-catalog.js +3 -3
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.js +43 -43
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/extension-update.js +2 -2
- package/dist/chores/extension-update.js.map +1 -1
- package/dist/chores/fix-deps.js +1 -1
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/chores/verdict.js +2 -2
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +41 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.d.ts +80 -0
- package/dist/contracts/exit.contract.d.ts.map +1 -0
- package/dist/contracts/exit.contract.js +13 -0
- package/dist/contracts/exit.contract.js.map +1 -0
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -5
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.d.ts +9 -0
- package/dist/fast-tier.d.ts.map +1 -0
- package/dist/fast-tier.js +19 -0
- package/dist/fast-tier.js.map +1 -0
- package/dist/history-state.js +3 -3
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +214 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +5 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts.map +1 -1
- package/dist/output-fields.js +2 -2
- package/dist/output-fields.js.map +1 -1
- package/dist/prompt-complexity.d.ts +19 -0
- package/dist/prompt-complexity.d.ts.map +1 -0
- package/dist/prompt-complexity.js +83 -0
- package/dist/prompt-complexity.js.map +1 -0
- package/dist/publish-drafts.js +2 -2
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +236 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +116 -2
- package/dist/schemas.js.map +1 -1
- package/dist/workflow-faults.js +3 -3
- package/dist/workflow-faults.js.map +1 -1
- package/dist/workspace-state.d.ts +20 -20
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +20 -20
- package/dist/workspace-state.js.map +1 -1
- package/package.json +5 -5
- package/src/agent-catalog.test.ts +25 -25
- package/src/agent-catalog.ts +81 -81
- package/src/agent-run-model.test.ts +3 -3
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-ledger.test.ts +13 -13
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/chores.ts +109 -109
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +5 -5
- package/src/chores/fix-deps.ts +4 -4
- package/src/chores/probes.test.ts +6 -6
- package/src/chores/probes.ts +17 -17
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.test.ts +3 -3
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.test.ts +20 -20
- package/src/chores/verdict.ts +25 -25
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/exit.contract.ts +42 -0
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +8 -8
- package/src/events.test.ts +3 -3
- package/src/events.ts +142 -142
- package/src/fast-tier.test.ts +88 -0
- package/src/fast-tier.ts +72 -0
- package/src/history-state.ts +15 -15
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.test.ts +1 -1
- package/src/hostnames.ts +15 -15
- package/src/index.ts +13 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.test.ts +11 -11
- package/src/model-order.ts +52 -30
- package/src/output-fields.ts +11 -11
- package/src/path-refs.test.ts +4 -4
- package/src/path-refs.ts +6 -6
- package/src/prompt-complexity.test.ts +160 -0
- package/src/prompt-complexity.ts +271 -0
- package/src/publish-drafts.ts +8 -8
- package/src/quick-model.test.ts +11 -11
- package/src/quick-model.ts +15 -15
- package/src/routes.test.ts +11 -5
- package/src/routes.ts +19 -19
- package/src/runtime-state.test.ts +1 -1
- package/src/runtime-state.ts +22 -22
- package/src/schemas.test.ts +8 -8
- package/src/schemas.ts +1238 -921
- package/src/search-globs.test.ts +2 -2
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.test.ts +1 -1
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.test.ts +9 -9
- package/src/title.ts +21 -21
- package/src/tunnel-ids.test.ts +3 -3
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.test.ts +3 -3
- package/src/versions.ts +6 -6
- package/src/workflow-faults.test.ts +6 -6
- package/src/workflow-faults.ts +14 -14
- package/src/workspace-state.test.ts +32 -32
- package/src/workspace-state.ts +139 -139
package/src/agent-catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type PermissionMode,
|
|
9
9
|
} from "./schemas.js";
|
|
10
10
|
|
|
11
|
-
/* The provider / harness / model catalog every picker shares (the chat menu, the automations dialog)
|
|
11
|
+
/* The provider / harness / model catalog every picker shares (the chat menu, the automations dialog), pure
|
|
12
12
|
* data keyed by the wire vocabulary in schemas.ts, so the surfaces can't drift. Live state stays with the
|
|
13
13
|
* consumer (native Grok's model list is the daemon's own catalog for it, layered on top of modelsFor by the
|
|
14
14
|
* web; ACP providers are merged in from the installed `agent` capabilities). */
|
|
@@ -26,13 +26,13 @@ export const PROVIDERS: readonly { label: string; value: NativeProvider }[] = [
|
|
|
26
26
|
{ label: "Codex", value: "codex" },
|
|
27
27
|
{ label: "Grok", value: "grok" },
|
|
28
28
|
{ label: "Kimi Code", value: "kimi" },
|
|
29
|
-
// Labelled for the ACCOUNT, not the model family: the `gemini` id names one channel
|
|
29
|
+
// Labelled for the ACCOUNT, not the model family: the `gemini` id names one channel. Google's Antigravity,
|
|
30
30
|
// and that channel vends Claude and GPT-OSS models alongside Gemini's own (see gemini-models.ts). A section
|
|
31
31
|
// headed "Gemini" holding Claude Opus would be a lie; "Google" is what the whole list has in common.
|
|
32
32
|
{ label: "Google", value: "gemini" },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
// What it COSTS to unlock a provider, and what the user connects to do it
|
|
35
|
+
// What it COSTS to unlock a provider, and what the user connects to do it, the axis the picker groups on, since
|
|
36
36
|
// "can this row actually run" is the first thing a model list has to answer. `free` is not a courtesy tier: the
|
|
37
37
|
// Google channel serves its models on an ordinary Google sign-in, at no subscription, which is the single most
|
|
38
38
|
// useful thing this catalog can tell a user who has connected nothing yet.
|
|
@@ -40,7 +40,7 @@ export type AccessKind = "free" | "subscription" | "key";
|
|
|
40
40
|
|
|
41
41
|
export interface ProviderAccess {
|
|
42
42
|
readonly kind: AccessKind;
|
|
43
|
-
// What the user connects, named the way its vendor names it
|
|
43
|
+
// What the user connects, named the way its vendor names it, this is the noun every connect prompt uses.
|
|
44
44
|
readonly requirement: string;
|
|
45
45
|
// What connecting it lets them run, for the connect gate's one-line pitch.
|
|
46
46
|
readonly runs: string;
|
|
@@ -54,7 +54,7 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
|
|
|
54
54
|
gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
/* THE PROVIDERS THAT COST NOTHING
|
|
57
|
+
/* THE PROVIDERS THAT COST NOTHING, derived from the table above rather than named a second time, and read by
|
|
58
58
|
* every surface that LEADS with a free option instead of merely labelling one.
|
|
59
59
|
*
|
|
60
60
|
* The distinction is worth the export. `accessBadge` answers "what does this row cost" for a row the user is
|
|
@@ -65,7 +65,7 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
|
|
|
65
65
|
export const FREE_PROVIDERS: readonly NativeProvider[] = NATIVE_PROVIDERS.filter((provider) => PROVIDER_ACCESS[provider].kind === "free");
|
|
66
66
|
export const isFreeProvider = (provider: AgentProvider): boolean => FREE_PROVIDERS.includes(provider as NativeProvider);
|
|
67
67
|
|
|
68
|
-
/* WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence
|
|
68
|
+
/* WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence, a third naming of the same
|
|
69
69
|
* five ids, and the third is not redundancy. PROVIDERS names the RUNTIME the user picks ("Claude Code", "Kimi
|
|
70
70
|
* Code") and PROVIDER_ACCESS.requirement names the thing they CONNECT ("Claude subscription", "Google sign-in");
|
|
71
71
|
* neither reads as English in "… usage limit reached", and neither is what a spent quota belongs to.
|
|
@@ -73,7 +73,7 @@ export const isFreeProvider = (provider: AgentProvider): boolean => FREE_PROVIDE
|
|
|
73
73
|
* The routed providers are why this can't be inferred from the harness: a `gemini` turn drives Claude Opus 4.6
|
|
74
74
|
* through Google's Antigravity channel on a plain Google sign-in, so the quota that refuses it is Google's and
|
|
75
75
|
* Anthropic has no part in it. Saying "Claude usage limit reached" there sends the user to check the wrong
|
|
76
|
-
* account
|
|
76
|
+
* account, and to a reset that is days out on a pool they never touched. */
|
|
77
77
|
export const PROVIDER_VENDOR: Record<NativeProvider, string> = {
|
|
78
78
|
claude: "Claude",
|
|
79
79
|
codex: "ChatGPT",
|
|
@@ -85,50 +85,50 @@ export const PROVIDER_VENDOR: Record<NativeProvider, string> = {
|
|
|
85
85
|
// What a turn on this provider costs at the MARGIN, ordering the same three kinds by the only question a
|
|
86
86
|
// helper spending the user's money on their behalf has to answer: free is free; a subscription is already paid
|
|
87
87
|
// but has a quota the user watches; a key is metered, so every call is real money. Deliberately not folded into
|
|
88
|
-
// AccessKind's declaration order
|
|
88
|
+
// AccessKind's declaration order, a union's order is not a runtime fact, and this one is relied on.
|
|
89
89
|
export const ACCESS_COST: Record<AccessKind, number> = { free: 0, subscription: 1, key: 2 };
|
|
90
90
|
|
|
91
|
-
/* THE PROVIDER ID OF AN `endpoint` CAPABILITY
|
|
91
|
+
/* THE PROVIDER ID OF AN `endpoint` CAPABILITY, a model API the user pointed us at, native or not, near or far.
|
|
92
92
|
*
|
|
93
93
|
* Namespaced rather than bare (which is what ACP agents are) because the two kinds mint providers with OPPOSITE
|
|
94
94
|
* ability records: an ACP agent brings its own loop and gets the documented ACP floor, while an endpoint is
|
|
95
95
|
* driven BY the Claude Code loop and gets its full ceiling. capabilitiesOf answers that from the id alone, so
|
|
96
96
|
* the prefix is what keeps it a pure function of (provider, harness) instead of a lookup against the installed
|
|
97
|
-
* manifest
|
|
97
|
+
* manifest, which the contract cannot see and the browser would have to pass in everywhere.
|
|
98
98
|
*
|
|
99
99
|
* A SLASH, never a colon: `${provider}:${model}` is the picker's own key shape, and quick-model.ts's parsePinned
|
|
100
100
|
* splits a pinned selection on the FIRST colon. `endpoint:ollama:qwen3` would parse as provider "endpoint" with
|
|
101
|
-
* model "ollama:qwen3"
|
|
101
|
+
* model "ollama:qwen3", a pin that silently resolves to nothing. The capability id (entryId) excludes both
|
|
102
102
|
* characters, so `endpoint/<id>` stays unambiguous in either direction. */
|
|
103
|
-
/* THE FREE TRIAL'S ENDPOINT ID IS RESERVED, the way `pi` is
|
|
103
|
+
/* THE FREE TRIAL'S ENDPOINT ID IS RESERVED, the way `pi` is, an `endpoint`-kind capability like any model API
|
|
104
104
|
* the user configured, except that this one is provisioned by the DAEMON rather than added by a person, and it
|
|
105
105
|
* points at intentic's own pool (see the sandbox's trial/ and the platform's /trial routes).
|
|
106
106
|
*
|
|
107
107
|
* Riding the endpoint kind is the entire reason the trial needed no new turn path, no new provider and no new
|
|
108
108
|
* adapter: the translator already re-serves an OpenAI-compatible upstream to the Claude Code loop, so a trial
|
|
109
|
-
* turn is an endpoint turn and everything downstream
|
|
109
|
+
* turn is an endpoint turn and everything downstream, catalog, picker, routing, works unchanged.
|
|
110
110
|
*
|
|
111
111
|
* What the reserved id buys is the part that must NOT look the same. A trial turn passes through intentic's
|
|
112
112
|
* servers, which no other provider in this product does, and a user cannot consent to something they were not
|
|
113
|
-
* told. So every surface that names a provider asks `isTrialProvider` and says so, and the id is here
|
|
114
|
-
* the vocabulary those surfaces already read
|
|
113
|
+
* told. So every surface that names a provider asks `isTrialProvider` and says so, and the id is here, beside
|
|
114
|
+
* the vocabulary those surfaces already read, rather than spelled out in each of them. */
|
|
115
115
|
export const TRIAL_ENDPOINT_ID = "free-trial";
|
|
116
116
|
export const TRIAL_PROVIDER = "endpoint/free-trial";
|
|
117
117
|
export const isTrialProvider = (provider: AgentProvider): boolean => provider === TRIAL_PROVIDER;
|
|
118
118
|
|
|
119
|
-
/* THE ONLY MODEL THE TRIAL PUBLISHES
|
|
119
|
+
/* THE ONLY MODEL THE TRIAL PUBLISHES, a synthetic id, not one of Google's, and that is the point.
|
|
120
120
|
*
|
|
121
121
|
* The trial used to publish whatever the upstream listed. Two things were wrong with that and neither could be
|
|
122
122
|
* fixed by filtering harder. Google lists ~54 models on a fresh key and declares `generateContent` for many that
|
|
123
|
-
* cannot serve an agent turn
|
|
124
|
-
* capability check and then fail the first message
|
|
123
|
+
* cannot serve an agent turn, deep-research, antigravity, gemma, robotics and computer-use previews all pass a
|
|
124
|
+
* capability check and then fail the first message, so the picker was full of rows whose only outcome was an
|
|
125
125
|
* error. And the list MOVED: the translator's routing table is written at boot and on capability edits, while
|
|
126
126
|
* the picker re-reads the catalog every minute, so a model discovered in between was pickable and unroutable,
|
|
127
127
|
* refused with "unknown provider for model".
|
|
128
128
|
*
|
|
129
129
|
* One id, never changing, ends both. There is nothing to filter because nothing is discovered, and the routing
|
|
130
130
|
* table cannot drift from a list of one constant. WHICH real model answers is decided per message by the
|
|
131
|
-
* platform, which is the only party that can see which of its keys still has quota on which model
|
|
131
|
+
* platform, which is the only party that can see which of its keys still has quota on which model, the sandbox
|
|
132
132
|
* cannot, and a user choosing blind between rows they know nothing about was never a choice worth offering. */
|
|
133
133
|
export const TRIAL_MODEL_ID = "auto";
|
|
134
134
|
// What the picker calls it, and the sentence the surfaces put underneath. One wording, so the composer's notice
|
|
@@ -143,28 +143,28 @@ export const isEndpointProvider = (provider: AgentProvider): boolean => provider
|
|
|
143
143
|
export const endpointIdOf = (provider: AgentProvider): string | undefined =>
|
|
144
144
|
isEndpointProvider(provider) ? provider.slice(ENDPOINT_PROVIDER_PREFIX.length) : undefined;
|
|
145
145
|
|
|
146
|
-
// An ACP provider carries its own credentials
|
|
146
|
+
// An ACP provider carries its own credentials, installed means runnable, so it has no access requirement at
|
|
147
147
|
// all; `undefined` is that state, and every surface reads it as "nothing to connect". An endpoint is the same
|
|
148
148
|
// answer for a different reason: its credential (if it even needs one) was configured with the endpoint itself,
|
|
149
|
-
// so there is likewise nothing left to connect. What a turn on it COSTS is deliberately not claimed here
|
|
149
|
+
// so there is likewise nothing left to connect. What a turn on it COSTS is deliberately not claimed here, a
|
|
150
150
|
// self-hosted model on the user's own GPU and a metered gateway key are the same shape to us, and inventing an
|
|
151
151
|
// AccessKind for them would have the picker assert a price the daemon has no way to know.
|
|
152
152
|
export const accessFor = (provider: AgentProvider): ProviderAccess | undefined => PROVIDER_ACCESS[provider as NativeProvider];
|
|
153
153
|
|
|
154
|
-
// An ACP provider's label is its capability's display name, which the web layers on top
|
|
154
|
+
// An ACP provider's label is its capability's display name, which the web layers on top, the raw id is the
|
|
155
155
|
// static fallback.
|
|
156
156
|
export const providerLabel = (provider: AgentProvider): string => PROVIDERS.find((p) => p.value === provider)?.label ?? provider;
|
|
157
157
|
|
|
158
|
-
/* Whether a plan-limit reading for this provider is OBTAINABLE at all
|
|
158
|
+
/* Whether a plan-limit reading for this provider is OBTAINABLE at all, one fact, on the wire, because both
|
|
159
159
|
* halves need it and they need the same answer. The daemon reads it to decide what to even ask upstream for
|
|
160
160
|
* (usage/translator-usage.ts); the browser reads it to say WHY an account shows no meter, which is the
|
|
161
|
-
* difference between "this plan publishes nothing" and "we haven't measured yet"
|
|
161
|
+
* difference between "this plan publishes nothing" and "we haven't measured yet", two states that look
|
|
162
162
|
* identical as a blank row and mean opposite things.
|
|
163
163
|
*
|
|
164
164
|
* Four can be read, by two mechanisms that stop at the daemon's readers: Claude's rides its own turn (the
|
|
165
165
|
* OAuth usage endpoint, agent.ts), ChatGPT's, Google's and Kimi's are pulled through the translator's
|
|
166
166
|
* credential-scoped api-call. Kimi's endpoint is the platform's own `/coding/v1/usages`, which the Kimi Code
|
|
167
|
-
* subscription's OAuth token reads directly
|
|
167
|
+
* subscription's OAuth token reads directly, the bundled translator does not route it, but it does not have
|
|
168
168
|
* to: the api-call substitutes that token server-side like it does for the other two.
|
|
169
169
|
*
|
|
170
170
|
* Grok is the one absence, because xAI's usable billing data needs a subject id CLIProxyAPI keeps out of its
|
|
@@ -176,28 +176,28 @@ export const reportsPlanLimits = (provider: AgentProvider): boolean => PLAN_LIMI
|
|
|
176
176
|
// The harness (agentic loop) a turn runs on, orthogonal to the provider. `native` = the provider's own runtime;
|
|
177
177
|
// `claude-code` = the Claude Code loop for any provider (codex/grok then route through the translator).
|
|
178
178
|
// Surfaced for codex/grok alone. Claude is always its own Claude Code loop; kimi has no native runtime to switch
|
|
179
|
-
// to (it only exists under this harness); and GEMINI IS THE MIRROR OF KIMI
|
|
179
|
+
// to (it only exists under this harness); and GEMINI IS THE MIRROR OF KIMI, it only exists under its native
|
|
180
180
|
// one, because Google refuses Claude Code's traffic outright (capabilitiesOf says why). See AgentHarness in
|
|
181
181
|
// schemas.ts.
|
|
182
182
|
//
|
|
183
183
|
// Gemini's `native` is OpenCode rather than a Google CLI: the image ships no Gemini binary, and OpenCode is
|
|
184
|
-
// already here driving Grok. It spends the same translator accounts a routed turn would have
|
|
184
|
+
// already here driving Grok. It spends the same translator accounts a routed turn would have, what Google
|
|
185
185
|
// refuses is the loop, never the credential.
|
|
186
186
|
export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
|
|
187
187
|
{ label: "Native", value: "native" },
|
|
188
188
|
{ label: "Claude Code", value: "claude-code" },
|
|
189
189
|
];
|
|
190
190
|
|
|
191
|
-
/* WHAT A PROVIDER/HARNESS PAIR CAN ACTUALLY DO
|
|
191
|
+
/* WHAT A PROVIDER/HARNESS PAIR CAN ACTUALLY DO, one declaration, read by both sides of the wire.
|
|
192
192
|
*
|
|
193
193
|
* Five runtimes serve turns behind one seam (AgentRequest in, AgentEvent frames out): the Claude Code Agent SDK
|
|
194
194
|
* loop, Codex app-server, OpenCode, any ACP agent, and Pi's RPC surface. They do NOT do the same things, and for a long time
|
|
195
|
-
* the only thing that said so was a comment inside each adapter
|
|
195
|
+
* the only thing that said so was a comment inside each adapter, "Ignores the Claude-only request fields",
|
|
196
196
|
* which no surface above it could read. So the composer offered "Ask before each file edit" on a runtime whose
|
|
197
197
|
* every tool call is pre-approved, and offered a reasoning-effort scale to a runtime that drops the field.
|
|
198
198
|
*
|
|
199
199
|
* A capability is listed here only if something READS it: the daemon gates a seam on it, the composer hides or
|
|
200
|
-
* clamps a control by it, or `limitationsOf` tells the user about it. That is the whole point
|
|
200
|
+
* clamps a control by it, or `limitationsOf` tells the user about it. That is the whole point, an ability the
|
|
201
201
|
* matrix claims and nothing consults is how the drift started.
|
|
202
202
|
*
|
|
203
203
|
* Adding a provider is a row here, not a hunt for literals; agent-catalog.test.ts walks PROVIDERS × HARNESSES
|
|
@@ -205,11 +205,11 @@ export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
|
|
|
205
205
|
|
|
206
206
|
// An execution backend: one way a turn runs work of its own, named for the AgentCapabilities.execution axis
|
|
207
207
|
// and for the persona switch that grants it. Adding a language is a member here and a backend in the daemon's
|
|
208
|
-
// execution/ module
|
|
208
|
+
// execution/ module, never a new one-off tool wired where nothing else can see it.
|
|
209
209
|
export type ExecutionBackend = "shell" | "js";
|
|
210
210
|
|
|
211
211
|
export interface AgentCapabilities {
|
|
212
|
-
// Which agentic loop actually serves the turn
|
|
212
|
+
// Which agentic loop actually serves the turn, the question "is the harness `claude-code`" only looks like.
|
|
213
213
|
// Claude is always its own Claude Code loop and Kimi has no native runtime, so both run it whatever harness
|
|
214
214
|
// the client sent; codex/grok/gemini each have a native runtime to switch away from. Names the session store
|
|
215
215
|
// a finished conversation's transcript is backfilled from, too.
|
|
@@ -222,7 +222,7 @@ export interface AgentCapabilities {
|
|
|
222
222
|
readonly steering: boolean;
|
|
223
223
|
// How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
|
|
224
224
|
// permission cards and `mode` frames when the agent moves itself; "plan" = propose-then-approve or run, and
|
|
225
|
-
// nothing in between
|
|
225
|
+
// nothing in between, the container is the isolation boundary and every tool call is pre-approved.
|
|
226
226
|
readonly permissions: "modes" | "plan";
|
|
227
227
|
// Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
|
|
228
228
|
readonly questions: boolean;
|
|
@@ -232,14 +232,14 @@ export interface AgentCapabilities {
|
|
|
232
232
|
// them at all. Keeping the partial answers distinct matters: a runtime that can drive a connected account
|
|
233
233
|
// must not be described as tool-less, and one that cannot host daemon-side SDK servers must not claim full.
|
|
234
234
|
readonly mcp: "full" | "browser" | "http" | "none";
|
|
235
|
-
/* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS
|
|
235
|
+
/* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS, the ways a turn RUNS things, as opposed to the tools it is
|
|
236
236
|
* handed. "shell" is the runtime's own command tool (Bash on the Claude Code loop, each foreign loop's
|
|
237
237
|
* equivalent); "js" is the sandbox's JavaScript backend (execution/ in the daemon): the model writes a
|
|
238
238
|
* script instead of a command line, and the daemon runs it in a permission-fenced Node subprocess.
|
|
239
239
|
*
|
|
240
240
|
* A first-class axis rather than a corollary of `mcp`, because the two answer different questions: `mcp`
|
|
241
241
|
* says which TOOLS reach the model's context, this says which ways of EXECUTING the daemon can stand
|
|
242
|
-
* behind for this runtime
|
|
242
|
+
* behind for this runtime, with the same guard, secret and persona seams the shell gets. A runtime that
|
|
243
243
|
* cannot host a backend simply never shows it, and the persona switch for it (PersonaPowersSchema.code)
|
|
244
244
|
* then has nothing to grant there. */
|
|
245
245
|
readonly execution: readonly ExecutionBackend[];
|
|
@@ -247,14 +247,14 @@ export interface AgentCapabilities {
|
|
|
247
247
|
readonly effort: boolean;
|
|
248
248
|
/* The runtime can serve a turn at fast speed when asked (AgentTurn.fast). A statement about the LOOP, not
|
|
249
249
|
* about the route: the Claude Code loop knows how to ask for it, which is why every provider this record
|
|
250
|
-
* hands the loop to reads true here
|
|
250
|
+
* hands the loop to reads true here, including the ones served through the translator, whose turns the
|
|
251
251
|
* harness will then refuse fast mode for because a translator endpoint is not first-party. That second
|
|
252
252
|
* question is answered where the endpoint is decided (planHarnessTurn), because it is a fact about the
|
|
253
253
|
* CREDENTIAL rather than about the runtime, and this record is a pure function of (provider, harness). */
|
|
254
254
|
readonly fastMode: boolean;
|
|
255
255
|
// How an isolated conversation's worktree is enforced. "namespace" = the worktree IS /work inside the turn's
|
|
256
256
|
// mount namespace (with the tool-input rewrite as the fallback when the container can't build one); "cwd" =
|
|
257
|
-
// the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout
|
|
257
|
+
// the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout,
|
|
258
258
|
// which is why those turns are told where their tree is (turn-preamble.ts).
|
|
259
259
|
readonly isolation: "namespace" | "cwd";
|
|
260
260
|
// Publishes its slash commands (`commands` frames) for the composer's `/` popover.
|
|
@@ -264,31 +264,31 @@ export interface AgentCapabilities {
|
|
|
264
264
|
// Fails with the coded frames the daemon's auto-resume keys off (rate_limit, provider-outage), so a turn the
|
|
265
265
|
// provider killed is re-run once the breaker says the provider is back (turn-resume.ts).
|
|
266
266
|
readonly recovery: boolean;
|
|
267
|
-
/* HOW MUCH OF ITS STANDING INSTRUCTIONS THIS RUNTIME WILL TAKE FROM US
|
|
267
|
+
/* HOW MUCH OF ITS STANDING INSTRUCTIONS THIS RUNTIME WILL TAKE FROM US, the axis behind the sandbox's
|
|
268
268
|
* system-prompt setting (SandboxSettings.systemPromptMode) and the persona's own override.
|
|
269
269
|
*
|
|
270
270
|
* It exists because that setting was silently a Claude Code setting. The composer offers Codex, Grok and
|
|
271
271
|
* Gemini on their own runtimes, and a turn on any of them ignored the prompt the owner had written without
|
|
272
|
-
* saying so anywhere
|
|
272
|
+
* saying so anywhere, the one failure mode a settings page cannot recover from, because nothing on screen
|
|
273
273
|
* is wrong. Naming it here means every surface reads the same answer and the daemon composes to it
|
|
274
274
|
* (agent/system-prompt.ts), rather than each learning the exception separately.
|
|
275
275
|
*
|
|
276
|
-
* "replace"
|
|
276
|
+
* "replace", the whole base prompt can be swapped for the owner's text, and extra guidance appended on
|
|
277
277
|
* top of whichever base is in force. The Claude Code loop (SDK `systemPrompt`) and native
|
|
278
278
|
* Codex (`model_instructions_file` replaces its base; `developer_instructions` adds a
|
|
279
|
-
* developer message
|
|
280
|
-
* "append"
|
|
279
|
+
* developer message, both verified on the wire against codex-cli 0.147).
|
|
280
|
+
* "append" , extra system text only; the runtime's own base prompt stands. OpenCode takes one per
|
|
281
281
|
* message (`system` on the prompt body), and there is no seam for replacing its base.
|
|
282
|
-
* "none"
|
|
282
|
+
* "none" , no system seam at all. What must still reach the model (the persona note) rides the user
|
|
283
283
|
* message instead, which is the door the delegation note already uses.
|
|
284
284
|
*
|
|
285
|
-
* The BASE CHOICE
|
|
285
|
+
* The BASE CHOICE. Intentic's prompt or Claude Code's, is a "replace" runtime's question and, of those,
|
|
286
286
|
* only the Claude Code loop's: Codex's own base describes Codex's own tools, so swapping it for a prompt
|
|
287
287
|
* written about another harness is the owner's deliberate act (their custom text), never ours. */
|
|
288
288
|
readonly instructions: "replace" | "append" | "none";
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
// The Claude Code Agent SDK loop
|
|
291
|
+
// The Claude Code Agent SDK loop, the ceiling every other runtime is measured against, and the only one that
|
|
292
292
|
// owns the whole request: permission callbacks, the ask tool, plugins, hooks, and the spawn seam a mount
|
|
293
293
|
// namespace needs.
|
|
294
294
|
const CLAUDE_CODE: AgentCapabilities = {
|
|
@@ -297,7 +297,7 @@ const CLAUDE_CODE: AgentCapabilities = {
|
|
|
297
297
|
permissions: "modes",
|
|
298
298
|
questions: true,
|
|
299
299
|
mcp: "full",
|
|
300
|
-
// The one loop with a seam the daemon can put its own backend through
|
|
300
|
+
// The one loop with a seam the daemon can put its own backend through, so it hosts the JS backend beside
|
|
301
301
|
// its Bash. Every other runtime below hosts only its own shell.
|
|
302
302
|
execution: ["shell", "js"],
|
|
303
303
|
effort: true,
|
|
@@ -310,13 +310,13 @@ const CLAUDE_CODE: AgentCapabilities = {
|
|
|
310
310
|
};
|
|
311
311
|
|
|
312
312
|
/* Codex app-server: item-level events, process-backed MCP servers, and the four interactive seams its protocol
|
|
313
|
-
* actually publishes
|
|
313
|
+
* actually publishes, `turn/steer` for mid-turn injection, the experimental `item/tool/requestUserInput` server
|
|
314
314
|
* request behind a question card, `skills/list` for the `/` popover (a picked command rides back as a structured
|
|
315
315
|
* skill input), and the same mount namespace the Claude Code loop gets, because app-server is a child process
|
|
316
316
|
* the adapter spawns and nsenter can put it in the turn's namespace like any other.
|
|
317
317
|
*
|
|
318
318
|
* Browser servers ride the per-thread config; daemon-side SDK servers, plugins and server-initiated APPROVALS
|
|
319
|
-
* stay unwired
|
|
319
|
+
* stay unwired, the container is the isolation boundary, so approvals are declined by design rather than
|
|
320
320
|
* missing (codex-app-server.ts refuses every server request but the question one). */
|
|
321
321
|
const CODEX: AgentCapabilities = {
|
|
322
322
|
runtime: "codex",
|
|
@@ -334,12 +334,12 @@ const CODEX: AgentCapabilities = {
|
|
|
334
334
|
/* Both halves, through the per-thread `config` block the adapter already sends: `model_instructions_file`
|
|
335
335
|
* takes the place of Codex's own base prompt, `developer_instructions` arrives as an extra developer
|
|
336
336
|
* message ahead of its skills and team blocks. Verified against codex-cli 0.147 by reading what actually
|
|
337
|
-
* reached the wire
|
|
337
|
+
* reached the wire, the keys are undocumented, and a strings dump proves only that they parse. */
|
|
338
338
|
instructions: "replace",
|
|
339
339
|
};
|
|
340
340
|
|
|
341
341
|
// OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id,
|
|
342
|
-
// a prompt and one system message of ours
|
|
342
|
+
// a prompt and one system message of ours, no effort scale, no tools of ours, no command list.
|
|
343
343
|
const OPENCODE: AgentCapabilities = {
|
|
344
344
|
runtime: "opencode",
|
|
345
345
|
steering: false,
|
|
@@ -353,19 +353,19 @@ const OPENCODE: AgentCapabilities = {
|
|
|
353
353
|
commands: false,
|
|
354
354
|
terminals: false,
|
|
355
355
|
recovery: false,
|
|
356
|
-
// `system` on the prompt body, per message. It ADDS to OpenCode's own prompt
|
|
357
|
-
// replacing that
|
|
356
|
+
// `system` on the prompt body, per message. It ADDS to OpenCode's own prompt, there is no seam for
|
|
357
|
+
// replacing that, so a custom prompt lands here as extra instructions, and the settings page says so
|
|
358
358
|
// rather than letting "replaces everything" quietly mean something else on two providers.
|
|
359
359
|
instructions: "append",
|
|
360
360
|
};
|
|
361
361
|
|
|
362
|
-
/* The same OpenCode loop, serving Gemini instead of xAI
|
|
362
|
+
/* The same OpenCode loop, serving Gemini instead of xAI, identical abilities, which is the point of giving it
|
|
363
363
|
* its own row rather than its own record shape.
|
|
364
364
|
*
|
|
365
365
|
* It exists because the alternative was Gemini's ONLY route being the Claude Code loop, and that loop announces
|
|
366
366
|
* itself to whatever it is pointed at: the CLI prepends its own "You are a Claude agent, built on Anthropic's
|
|
367
367
|
* Claude Agent SDK." to every request, baked into the binary with no option to suppress it. Google's Antigravity
|
|
368
|
-
* channel matches that exact sentence and refuses the request
|
|
368
|
+
* channel matches that exact sentence and refuses the request, reported as a quota error, which sent the
|
|
369
369
|
* translator walking all 31 connected accounts looking for one with room, ~60s per attempt, none of which could
|
|
370
370
|
* ever have answered. Under this runtime the request carries OpenCode's own prompt, so the turn is simply not
|
|
371
371
|
* Claude Code traffic and the block has nothing to match.
|
|
@@ -379,7 +379,7 @@ const OPENCODE_GEMINI: AgentCapabilities = {
|
|
|
379
379
|
|
|
380
380
|
// Any agent speaking the Agent Client Protocol: a documented floor rather than the native ceiling. It publishes
|
|
381
381
|
// commands, runs its terminals in the conversation's tmux session, and takes our http MCP tools when it says it
|
|
382
|
-
// can
|
|
382
|
+
// can, but it owns its own model, effort and permission posture.
|
|
383
383
|
const ACP: AgentCapabilities = {
|
|
384
384
|
runtime: "acp",
|
|
385
385
|
steering: false,
|
|
@@ -399,7 +399,7 @@ const ACP: AgentCapabilities = {
|
|
|
399
399
|
};
|
|
400
400
|
|
|
401
401
|
/* THE PI CAPABILITY ID IS RESERVED, the same way the five native ids are: an `agent`-kind capability installed
|
|
402
|
-
* under it is served over Pi's own RPC protocol rather than ACP
|
|
402
|
+
* under it is served over Pi's own RPC protocol rather than ACP. Pi closed ACP support deliberately (its RPC
|
|
403
403
|
* mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
|
|
404
404
|
* `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
|
|
405
405
|
* which is what keeps it a pure function of (provider, harness). */
|
|
@@ -408,7 +408,7 @@ export const PI_PROVIDER = "pi";
|
|
|
408
408
|
// Pi driven over its RPC mode (`pi --mode rpc`, strict-LF JSONL over stdio): above the ACP floor and below the
|
|
409
409
|
// Claude Code ceiling. Its `steer` command is real mid-turn injection; `set_thinking_level` takes the effort
|
|
410
410
|
// tiers; `get_commands` publishes its extension/skill commands. It has no MCP seam (Pi's own extensions are its
|
|
411
|
-
// tool surface), no approval channel (plan is the shared two-phase emulation), and runs bash in-process
|
|
411
|
+
// tool surface), no approval channel (plan is the shared two-phase emulation), and runs bash in-process, no
|
|
412
412
|
// tmux session for the terminal panel to attach to.
|
|
413
413
|
const PI: AgentCapabilities = {
|
|
414
414
|
runtime: "pi",
|
|
@@ -429,9 +429,9 @@ const PI: AgentCapabilities = {
|
|
|
429
429
|
};
|
|
430
430
|
|
|
431
431
|
// The pair → its record. An `endpoint/<id>` provider is a model API the user configured, driven BY the Claude
|
|
432
|
-
// Code loop on either harness
|
|
432
|
+
// Code loop on either harness, so it gets that loop's full ceiling, which is the entire point of routing a
|
|
433
433
|
// model through it rather than adopting a second runtime. The reserved `pi` id is the Pi coding agent on its
|
|
434
|
-
// own RPC runtime (harness doesn't apply
|
|
434
|
+
// own RPC runtime (harness doesn't apply. Pi is its own loop, like ACP). Any other id that names no native
|
|
435
435
|
// provider is an installed `agent`-kind capability, served over ACP.
|
|
436
436
|
export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness): AgentCapabilities => {
|
|
437
437
|
if (provider === "codex") {
|
|
@@ -442,12 +442,12 @@ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness):
|
|
|
442
442
|
}
|
|
443
443
|
/* GEMINI IGNORES THE HARNESS, and it is the only routed provider that does. The Claude Code loop announces
|
|
444
444
|
* itself in every request it sends and Google refuses on that announcement (see OPENCODE_GEMINI), so
|
|
445
|
-
* "Gemini under Claude Code" was never a slower or poorer option
|
|
445
|
+
* "Gemini under Claude Code" was never a slower or poorer option, it was one that could not complete a
|
|
446
446
|
* single turn, on any of the connected accounts, ever.
|
|
447
447
|
*
|
|
448
448
|
* Answering OPENCODE_GEMINI whatever the caller asked for is what makes that structural rather than a rule
|
|
449
|
-
* each surface has to remember. Everything downstream reads the runtime off this record
|
|
450
|
-
* serves a turn, the transcript store, the quick helper's choice of loop
|
|
449
|
+
* each surface has to remember. Everything downstream reads the runtime off this record, the adapter that
|
|
450
|
+
* serves a turn, the transcript store, the quick helper's choice of loop, so there is exactly one place
|
|
451
451
|
* where Gemini's loop is decided, and no way left to route Claude Code traffic at Google by asking for it. */
|
|
452
452
|
if (provider === "gemini") {
|
|
453
453
|
return OPENCODE_GEMINI;
|
|
@@ -466,18 +466,18 @@ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness):
|
|
|
466
466
|
export const modesFor = (capabilities: AgentCapabilities): readonly PermissionMode[] =>
|
|
467
467
|
capabilities.permissions === "modes" ? ["default", "acceptEdits", "plan", "bypassPermissions"] : ["plan", "bypassPermissions"];
|
|
468
468
|
|
|
469
|
-
// The mode a selection falls back to when the runtime can't hold it
|
|
469
|
+
// The mode a selection falls back to when the runtime can't hold it, the same shape as clampEffort, and for the
|
|
470
470
|
// same reason: a provider switch must not leave the composer showing a posture nothing applies.
|
|
471
471
|
export const clampMode = (mode: PermissionMode, capabilities: AgentCapabilities): PermissionMode =>
|
|
472
472
|
modesFor(capabilities).includes(mode) ? mode : "bypassPermissions";
|
|
473
473
|
|
|
474
|
-
/* What this pair does NOT do, phrased for the person about to send a message to it
|
|
474
|
+
/* What this pair does NOT do, phrased for the person about to send a message to it, the honest half of the
|
|
475
475
|
* picker, and the reason the record carries axes the daemon itself never branches on. Empty ⇒ the full ceiling.
|
|
476
476
|
*
|
|
477
477
|
* `fastMode` is deliberately NOT disclosed here, and it is the one axis that can't be: every other axis is fully
|
|
478
478
|
* determined by the record, while fast mode also depends on the route and the model. The record says true for
|
|
479
|
-
* every provider the Claude Code loop serves
|
|
480
|
-
* go fast
|
|
479
|
+
* every provider the Claude Code loop serves, including the ones routed through the translator, which can never
|
|
480
|
+
* go fast, so a sentence derived from it would stay silent for exactly the turns that most need to hear it.
|
|
481
481
|
* fastAllowed answers the real question, and the `fast_mode` frame reports what the turn actually got. */
|
|
482
482
|
export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
|
|
483
483
|
...(capabilities.permissions === "plan" ? ["no per-tool approvals"] : []),
|
|
@@ -486,11 +486,11 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
|
|
|
486
486
|
...(capabilities.mcp === "none"
|
|
487
487
|
? ["no MCP tools or plugins"]
|
|
488
488
|
: capabilities.mcp === "http"
|
|
489
|
-
? ["MCP tools only
|
|
489
|
+
? ["MCP tools only: no plugins or browser"]
|
|
490
490
|
: capabilities.mcp === "browser"
|
|
491
|
-
? ["browser tools only
|
|
491
|
+
? ["browser tools only: no plugins or other MCP tools"]
|
|
492
492
|
: []),
|
|
493
|
-
...(capabilities.execution.includes("js") ? [] : ["no code runs
|
|
493
|
+
...(capabilities.execution.includes("js") ? [] : ["no code runs, its shell is the one way to execute"]),
|
|
494
494
|
...(capabilities.effort ? [] : ["no effort control"]),
|
|
495
495
|
...(capabilities.commands ? [] : ["no slash commands"]),
|
|
496
496
|
...(capabilities.terminals ? [] : ["no terminal panel"]),
|
|
@@ -503,10 +503,10 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
|
|
|
503
503
|
...(capabilities.instructions === "none" ? ["your system prompt isn't applied"] : []),
|
|
504
504
|
];
|
|
505
505
|
|
|
506
|
-
// Claude's compile-time model floor, shared by the daemon's catalog (claude-models.ts
|
|
506
|
+
// Claude's compile-time model floor, shared by the daemon's catalog (claude-models.ts, its last rung, reached
|
|
507
507
|
// only before either live source has ever answered) and by the web's pre-load list, so the two can't name
|
|
508
508
|
// different models. VERSIONED ids only, never the tier aliases (`opus`, `sonnet`) that used to sit here: an
|
|
509
|
-
// alias names no version, so a turn running on one leaves the user unable to say what answered them
|
|
509
|
+
// alias names no version, so a turn running on one leaves the user unable to say what answered them, and it
|
|
510
510
|
// lags a release besides, resolving to the previous version for as long as the CLI keeps pointing it there.
|
|
511
511
|
// Going stale costs nothing: every rung above replaces the whole list, and a selection the live catalog no
|
|
512
512
|
// longer offers is repointed to its default (loadProviderModels web-side, routedModel daemon-side).
|
|
@@ -517,8 +517,8 @@ export const CLAUDE_SEED_MODELS: readonly Model[] = [
|
|
|
517
517
|
];
|
|
518
518
|
|
|
519
519
|
// The STATIC floor of the model catalog, harness-independent: every provider's real list is the daemon's live
|
|
520
|
-
// catalog (/providers/{provider}/models
|
|
521
|
-
// which consumers layer on top. Codex/grok are empty here (nothing sensible to offer before the live load
|
|
520
|
+
// catalog (/providers/{provider}/models, discovery with a persisted/seed floor, never empty),
|
|
521
|
+
// which consumers layer on top. Codex/grok are empty here (nothing sensible to offer before the live load, and
|
|
522
522
|
// under the Claude Code harness they route through the translator, which serves the SAME subscription model ids
|
|
523
523
|
// as the native catalog, so the harness no longer changes the list).
|
|
524
524
|
export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
|
|
@@ -532,9 +532,9 @@ export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
|
|
|
532
532
|
// Whether a reasoning-effort tier is actually sendable for this provider with this thinking setting. 'max' is
|
|
533
533
|
// the only constrained tier and it fails two ways: no non-Claude scale HAS it, and Claude's API rejects it
|
|
534
534
|
// outright when extended thinking is disabled ("effort 'max' is not supported when thinking is disabled on this
|
|
535
|
-
// model"
|
|
536
|
-
// category). It is the one rule a MODEL's published tier list can't express
|
|
537
|
-
// accepts without knowing this turn's thinking setting
|
|
535
|
+
// model", a 400 that kills the turn before the model sees it, surfacing only as the SDK's `unknown` error
|
|
536
|
+
// category). It is the one rule a MODEL's published tier list can't express, the daemon reports what a model
|
|
537
|
+
// accepts without knowing this turn's thinking setting, so the consumer that assembles the offered scale
|
|
538
538
|
// (effortsFor, web-side) filters through here, and the clamp over that scale makes the pair unreachable.
|
|
539
539
|
export const effortAllowed = (effort: string, provider: AgentProvider, thinking: boolean): boolean =>
|
|
540
540
|
effort !== "max" || (provider === "claude" && thinking);
|
|
@@ -543,7 +543,7 @@ export const effortAllowed = (effort: string, provider: AgentProvider, thinking:
|
|
|
543
543
|
*
|
|
544
544
|
* effortAllowed makes the pair unreachable in the picker, and the picker is not the only way a turn is
|
|
545
545
|
* assembled: a route, an extension, a restored tab or a settings-pinned model can all name an effort that no
|
|
546
|
-
* live scale filtered. One did
|
|
546
|
+
* live scale filtered. One did, a session ran `max` with thinking off, and every server-side tool call in it
|
|
547
547
|
* came back `400 output_config.effort 'max' is not supported when thinking is disabled`, which reads to the
|
|
548
548
|
* model as "web search is broken" and cost it the answer it was sent to find.
|
|
549
549
|
*
|
|
@@ -554,18 +554,18 @@ export const effortAllowed = (effort: string, provider: AgentProvider, thinking:
|
|
|
554
554
|
export const sendableEffort = (effort: string | undefined, thinking: boolean | undefined): string | undefined =>
|
|
555
555
|
effort === "max" && thinking !== true ? "high" : effort;
|
|
556
556
|
|
|
557
|
-
/* WHETHER FAST SPEED CAN BE OFFERED for a provider/harness/model triple
|
|
557
|
+
/* WHETHER FAST SPEED CAN BE OFFERED for a provider/harness/model triple, the picker-side filter, the same
|
|
558
558
|
* shape and the same reason as effortAllowed: the composer must not show a control that does nothing.
|
|
559
559
|
*
|
|
560
|
-
* Three conditions, each answering a different question, and all three are
|
|
560
|
+
* Three conditions, each answering a different question, and all three are required:
|
|
561
561
|
*
|
|
562
562
|
* - the RUNTIME has to know how to ask (capabilities.fastMode). Only the Claude Code loop does.
|
|
563
563
|
* - the ROUTE has to be first-party. Every non-Claude provider the Claude Code loop serves is served through
|
|
564
564
|
* the sandbox's translator, and the harness refuses fast mode on a non-Anthropic endpoint ("not_first_party")
|
|
565
|
-
*
|
|
565
|
+
* , so a `grok` turn on the claude-code harness reads true on the capability and still cannot go fast.
|
|
566
566
|
* - the MODEL has to publish it, which is the `fast` badge Anthropic's own catalog reports per model
|
|
567
567
|
* (claude-models.ts maps supportsFastMode onto it). Curating a list of ids here instead is what this repo
|
|
568
|
-
* deliberately does not do
|
|
568
|
+
* deliberately does not do, a model that gains or loses fast mode moves the badge, and this follows.
|
|
569
569
|
*
|
|
570
570
|
* `badges` absent ⇒ false. That is the honest reading: a catalog row that published no capabilities said
|
|
571
571
|
* nothing about fast mode, and the seed floor a picker shows before its first live load is exactly that row. */
|
|
@@ -3,7 +3,7 @@ import { resolveAgentRunModels } from "./agent-run-model.js";
|
|
|
3
3
|
import type { QuickModelSource } from "./quick-model.js";
|
|
4
4
|
|
|
5
5
|
/* Which model a run somebody's BUTTON started opens on. The rule answers the same two surfaces its quick-model
|
|
6
|
-
* sibling does
|
|
6
|
+
* sibling does: the daemon walks it, the settings row names it, so what these pin is the pair of properties
|
|
7
7
|
* that separate the two: an account this sandbox cannot reach never sits at the head of the chain, and an empty
|
|
8
8
|
* answer stays empty rather than being filled in with a tier nobody chose. */
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ const GOOGLE: QuickModelSource = { provider: `gemini`, ready: true, models: [`ge
|
|
|
13
13
|
|
|
14
14
|
const offline = (source: QuickModelSource): QuickModelSource => ({ ...source, ready: false });
|
|
15
15
|
|
|
16
|
-
test("keeps the user's own order
|
|
16
|
+
test("keeps the user's own order, this list is read, never ranked", () => {
|
|
17
17
|
// The opposite of the quick chain, which sorts by tier and cost. Here the order IS the setting: someone who
|
|
18
18
|
// put Opus above GPT wants Opus first, and a resolver that knew better would spend the wrong account.
|
|
19
19
|
expect(resolveAgentRunModels([CLAUDE, CODEX], [`codex:gpt-5.6`, `claude:claude-opus-5`])).toEqual([
|
|
@@ -30,7 +30,7 @@ test("steps over a provider this sandbox has no credential for", () => {
|
|
|
30
30
|
]);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test("resolves to nothing when no pin is reachable
|
|
33
|
+
test("resolves to nothing when no pin is reachable: it does NOT fall back to whatever is connected", () => {
|
|
34
34
|
// The deliberate difference from resolveQuickModels, which lands on its Auto ladder here. An agent run is
|
|
35
35
|
// billed in whole sessions, so an unreachable list hands the choice back to the caller's floor (the user's
|
|
36
36
|
// own composer pick) rather than spending an account they never pointed at.
|
package/src/agent-run-model.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parsePinned, type QuickModelChoice, quickModelKey, type QuickModelSource } from "./quick-model.js";
|
|
2
2
|
|
|
3
|
-
/* WHAT A SURFACE-STARTED AGENT RUN OPENS ON
|
|
3
|
+
/* WHAT A SURFACE-STARTED AGENT RUN OPENS ON, the resolver for `agentRunModels`, sibling to resolveQuickModels
|
|
4
4
|
* and deliberately not the same function.
|
|
5
5
|
*
|
|
6
6
|
* BOTH ARE ORDERED LISTS, FOR THE SAME REASON. One connected account whose allowance went on the chat this
|
|
@@ -13,33 +13,33 @@ import { parsePinned, type QuickModelChoice, quickModelKey, type QuickModelSourc
|
|
|
13
13
|
* is connected" is a good answer and quickModel's empty list resolves to a derived Auto ladder. An agent run is
|
|
14
14
|
* a full session with a worktree, billed whole: nothing here can judge whether a job is worth the frontier tier,
|
|
15
15
|
* so an empty list resolves to NOTHING and the caller falls back to the model the user picked for their own
|
|
16
|
-
* chat
|
|
16
|
+
* chat, a choice they made, rather than one this file guessed for them. For the same reason there is no Auto
|
|
17
17
|
* ladder underneath a list that has been emptied by disconnection: it would spend an account the user never
|
|
18
18
|
* pointed at, on the most expensive kind of run this app starts.
|
|
19
19
|
*
|
|
20
20
|
* WHAT "STEPPED OVER" MEANS HERE IS NARROWER than the quick chain's, and worth being exact about. The quick
|
|
21
21
|
* chain re-asks the next rung when a call comes back refused, because a one-shot that failed has cost nothing
|
|
22
|
-
* and can simply be run again. An agent session cannot be replayed that way
|
|
23
|
-
* mid-turn the agent may have already edited files
|
|
22
|
+
* and can simply be run again. An agent session cannot be replayed that way, by the time a provider refuses
|
|
23
|
+
* mid-turn the agent may have already edited files, so this list is read ONCE, at the moment the turn is
|
|
24
24
|
* composed, and steps over exactly one thing: an account that is not connected. A model that accepts the turn
|
|
25
25
|
* and fails later is a failed run the user reads on the card, like any other. */
|
|
26
26
|
|
|
27
27
|
/* The pins that could actually be started right now, in the user's own order.
|
|
28
28
|
*
|
|
29
29
|
* `sources` is the same readiness view resolveQuickModels takes, so both settings rows agree about which
|
|
30
|
-
* accounts this sandbox can send to
|
|
30
|
+
* accounts this sandbox can send to, a pin greyed as "Not connected" in one row and silently spent by the
|
|
31
31
|
* other would be the worst of both.
|
|
32
32
|
*
|
|
33
33
|
* A pin whose provider is gone is DROPPED rather than held: it would otherwise sit at the head of the chain
|
|
34
|
-
* failing every run, which is exactly what the list exists to prevent. It stays on SCREEN, greyed
|
|
35
|
-
* row renders the stored list, not this one
|
|
34
|
+
* failing every run, which is exactly what the list exists to prevent. It stays on SCREEN, greyed, the settings
|
|
35
|
+
* row renders the stored list, not this one, because a setting that vanished from view would look like the app
|
|
36
36
|
* had eaten it.
|
|
37
37
|
*
|
|
38
38
|
* Empty out means nobody has pinned anything this sandbox can reach, and the caller's floor takes over. */
|
|
39
39
|
export const resolveAgentRunModels = (sources: readonly QuickModelSource[], pinned: readonly string[]): readonly QuickModelChoice[] => {
|
|
40
40
|
const ready = new Set(sources.filter((source) => source.ready).map((source) => source.provider));
|
|
41
41
|
const requested = pinned.flatMap((key) => {
|
|
42
|
-
// Verbatim, unvalidated against the catalog
|
|
42
|
+
// Verbatim, unvalidated against the catalog, the same call resolveQuickModels makes and for the same
|
|
43
43
|
// reason: the picker offers a custom-id escape hatch for a model the static catalog has not caught up
|
|
44
44
|
// with, and second-guessing the id here would run a different model than the settings row names.
|
|
45
45
|
const choice = parsePinned(key);
|
package/src/capability-env.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/* HOW A CLI CAPABILITY'S ENV VARS ARE NAMED
|
|
1
|
+
/* HOW A CLI CAPABILITY'S ENV VARS ARE NAMED, the one rule both ends of that wire have to apply.
|
|
2
2
|
*
|
|
3
3
|
* The daemon writes the agent's shell env by suffixing every var a cli connector declares with the instance id
|
|
4
4
|
* (cli-env.ts), so two connections of the same provider coexist on one flat environment. A connector's own
|
|
5
|
-
* TOOL then has to read them back, and an extension may not import daemon internals
|
|
5
|
+
* TOOL then has to read them back, and an extension may not import daemon internals, so without this the
|
|
6
6
|
* rule would be spelled twice, and a change to it would silently split the writer from the reader. */
|
|
7
7
|
|
|
8
8
|
// `analytics` → `POSTGRES_URL_ANALYTICS`; the default-named `github` → `GITHUB_TOKEN_GITHUB`.
|
|
9
|
-
// ponytail: ids differing only by case or `-`/`_` (my-db vs my_db) map to the same suffix
|
|
9
|
+
// ponytail: ids differing only by case or `-`/`_` (my-db vs my_db) map to the same suffix, last wins.
|
|
10
10
|
export const envSuffix = (id: string): string => id.toUpperCase().replaceAll("-", "_");
|