@intentic/sandbox-contract 1.299.0 → 1.300.1
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/dist/contracts/agent.contract.d.ts +771 -8
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +3 -2
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +0 -81
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +6 -1
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +3 -2
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/exit.contract.d.ts +15 -3
- package/dist/contracts/exit.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.js +5 -4
- package/dist/contracts/exit.contract.js.map +1 -1
- package/dist/contracts/host.contract.d.ts +24 -2
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.js +4 -3
- package/dist/contracts/host.contract.js.map +1 -1
- package/dist/contracts/intentic.contract.d.ts +10 -2
- package/dist/contracts/intentic.contract.d.ts.map +1 -1
- package/dist/contracts/intentic.contract.js +4 -3
- package/dist/contracts/intentic.contract.js.map +1 -1
- package/dist/contracts/netdisk.contract.d.ts +5 -1
- package/dist/contracts/netdisk.contract.d.ts.map +1 -1
- package/dist/contracts/netdisk.contract.js +3 -2
- package/dist/contracts/netdisk.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +505 -18
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.js +4 -3
- package/dist/contracts/runner.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +0 -1
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +0 -33
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +333 -8
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +5 -4
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/vpn.contract.d.ts +5 -1
- package/dist/contracts/vpn.contract.d.ts.map +1 -1
- package/dist/contracts/vpn.contract.js +3 -2
- package/dist/contracts/vpn.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +1 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +1 -15
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +0 -10
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/system-events.d.ts +0 -10
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +0 -6
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +1 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +1176 -171
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/models/model-order.d.ts +0 -2
- package/dist/models/model-order.d.ts.map +1 -1
- package/dist/models/model-order.js +0 -13
- package/dist/models/model-order.js.map +1 -1
- package/dist/protocol/routes.d.ts +6 -0
- package/dist/protocol/routes.d.ts.map +1 -1
- package/dist/protocol/routes.js +12 -4
- package/dist/protocol/routes.js.map +1 -1
- package/dist/schemas/agent.d.ts +0 -1
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +0 -4
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +0 -15
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +0 -8
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +0 -5
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +1 -0
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +1 -0
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/devices.d.ts +1 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +43 -24
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/providers/usage.d.ts +0 -6
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +0 -6
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/settings.d.ts +0 -31
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +0 -23
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/workspace/workspace-search.d.ts +1 -0
- package/dist/schemas/workspace/workspace-search.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-search.js +5 -0
- package/dist/schemas/workspace/workspace-search.js.map +1 -1
- package/dist/state/definition.d.ts +0 -12
- 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/workspace-state.d.ts +6 -0
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +7 -0
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +0 -4
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +5 -5
- package/src/contracts/agent.contract.ts +3 -2
- package/src/contracts/capabilities.contract.ts +3 -2
- package/src/contracts/exit.contract.ts +5 -4
- package/src/contracts/host.contract.ts +4 -3
- package/src/contracts/intentic.contract.ts +4 -3
- package/src/contracts/netdisk.contract.ts +3 -2
- package/src/contracts/runner.contract.ts +4 -3
- package/src/contracts/system.contract.ts +5 -4
- package/src/contracts/vpn.contract.ts +3 -2
- package/src/events/agent-events.ts +0 -14
- package/src/events/transcript.ts +1 -1
- package/src/index.ts +0 -2
- package/src/models/model-order.test.ts +1 -51
- package/src/models/model-order.ts +1 -23
- package/src/models/model-pins.ts +2 -2
- package/src/protocol/routes.test.ts +51 -18
- package/src/protocol/routes.ts +26 -6
- package/src/schemas/agent.ts +0 -8
- package/src/schemas/agents.ts +0 -11
- package/src/schemas/capabilities.ts +3 -0
- package/src/schemas/devices.ts +75 -29
- package/src/schemas/model-route.ts +1 -1
- package/src/schemas/providers/usage.ts +0 -11
- package/src/schemas/settings.ts +0 -52
- package/src/schemas/workspace/workspace-search.ts +7 -0
- package/src/state/history-state.ts +3 -0
- package/src/state/workspace-state.test.ts +13 -5
- package/src/state/workspace-state.ts +11 -0
- package/src/text/transcript-fold.test.ts +0 -17
- package/src/text/transcript-fold.ts +0 -5
- package/dist/models/fast-tier.d.ts +0 -9
- package/dist/models/fast-tier.d.ts.map +0 -1
- package/dist/models/fast-tier.js +0 -19
- package/dist/models/fast-tier.js.map +0 -1
- package/dist/models/prompt-complexity.d.ts +0 -27
- package/dist/models/prompt-complexity.d.ts.map +0 -1
- package/dist/models/prompt-complexity.js +0 -91
- package/dist/models/prompt-complexity.js.map +0 -1
- package/src/models/fast-tier.test.ts +0 -77
- package/src/models/fast-tier.ts +0 -36
- package/src/models/prompt-complexity.test.ts +0 -177
- package/src/models/prompt-complexity.ts +0 -215
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { compareModelIds, compareUnrankedModelIds, familyOf, namesThinking, releaseOf, tierRankOf } from "./model-order.js";
|
|
3
3
|
|
|
4
4
|
// The order every provider's catalog is served and browsed in; only Anthropic's catalog arrives ranked, the rest hand
|
|
5
5
|
// back sets, so their registry order is not a preference.
|
|
@@ -174,51 +174,6 @@ test("sorts an unversioned rolling alias under the releases that name their vers
|
|
|
174
174
|
expect(["kimi-latest", "kimi-k2-0711-preview"].toSorted(compareModelIds)).toEqual(["kimi-k2-0711-preview", "kimi-latest"]);
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// compareCheapestFirst: what the quick model's one-click helper resolves against, the same tier scale read
|
|
178
|
-
// weakest-first.
|
|
179
|
-
|
|
180
|
-
test("opens on the efficient rung and buries the frontier one: the exact inverse of the picker's order", () => {
|
|
181
|
-
const claude = ["claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001"];
|
|
182
|
-
|
|
183
|
-
expect(claude.toSorted(compareCheapestFirst)).toEqual(["claude-haiku-4-5-20251001", "claude-sonnet-5", "claude-opus-5"]);
|
|
184
|
-
expect(claude.toSorted(compareModelIds)).toEqual(["claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001"]);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("keeps an UNRANKED family off the cheap end, where a plain reversal would have seated it first", () => {
|
|
188
|
-
// An id with no tier word is the provider's baseline, likelier the next flagship than a budget tier.
|
|
189
|
-
expect(["gpt-5.6", "gpt-5.4-mini"].toSorted(compareCheapestFirst)).toEqual(["gpt-5.4-mini", "gpt-5.6"]);
|
|
190
|
-
expect(["claude-mythos-1", "claude-haiku-4-5", "claude-sonnet-5"].toSorted(compareCheapestFirst).at(-1)).toBe("claude-mythos-1");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("takes the NEWEST build of the cheap rung, not merely any of them", () => {
|
|
194
|
-
// Within one tier the release rule runs unchanged: cheap is a tier, not an excuse to serve a stale model.
|
|
195
|
-
expect(["claude-haiku-4-5-20251001", "claude-haiku-4-5-20260210"].toSorted(compareCheapestFirst)[0]).toBe("claude-haiku-4-5-20260210");
|
|
196
|
-
expect(["gemini-3-flash-lite", "gemini-2-flash-lite"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3-flash-lite");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test("finds each vendor's own cheap rung, including a re-served open-weights row", () => {
|
|
200
|
-
expect(["gemini-3-pro", "gemini-3-flash", "gemini-3-flash-lite"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3-flash-lite");
|
|
201
|
-
// gpt-oss is Google's cheap option; `oss` is its only tier signal, without it the id would sink instead of lead.
|
|
202
|
-
expect(["claude-opus-4-6-thinking", "gpt-oss-120b-medium"].toSorted(compareCheapestFirst)[0]).toBe("gpt-oss-120b-medium");
|
|
203
|
-
expect(["grok-4", "grok-4-fast"].toSorted(compareCheapestFirst)[0]).toBe("grok-4-fast");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("reads a release-local tier ladder from the cheap end too", () => {
|
|
207
|
-
expect(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].toSorted(compareCheapestFirst)[0]).toBe("gpt-5.6-luna");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("refuses the thinking variant of a model, however new it is", () => {
|
|
211
|
-
// A routed channel vends one row per thinking level; the cheapest model's top row must not win here.
|
|
212
|
-
expect(["gemini-3.6-flash-high", "gemini-3.5-flash-extra-low"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.5-flash-extra-low");
|
|
213
|
-
// …and it is the level that decides, not the release: same model, quieter row wins.
|
|
214
|
-
expect(["gemini-3.5-flash-high", "gemini-3.5-flash-minimal"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.5-flash-minimal");
|
|
215
|
-
// Tier still outranks thinking: a cheap thinking model beats a silent expensive one.
|
|
216
|
-
expect(["gemini-3-pro", "gemini-3.6-flash-high"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.6-flash-high");
|
|
217
|
-
// An unannotated id is neither accused of thinking nor credited with silence: it sits between the stated ends.
|
|
218
|
-
expect(["gemini-3-flash", "gemini-3.5-flash-low"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.5-flash-low");
|
|
219
|
-
expect(["gemini-3-flash", "gemini-3.6-flash-high"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3-flash");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
177
|
test("names the thinking rows, and only those", () => {
|
|
223
178
|
// What a settings row shows beside a pin.
|
|
224
179
|
expect(namesThinking("gemini-3.6-flash-high")).toBe(true);
|
|
@@ -231,8 +186,3 @@ test("names the thinking rows, and only those", () => {
|
|
|
231
186
|
expect(namesThinking("kimi-k2-thinking")).toBe(true);
|
|
232
187
|
expect(namesThinking("kimi-k2")).toBe(false);
|
|
233
188
|
});
|
|
234
|
-
|
|
235
|
-
test("falls back on the newest release for a catalog that publishes no cheap tier at all", () => {
|
|
236
|
-
// Kimi names no tier word anywhere; with tier cancelled out, newest is the honest fallback.
|
|
237
|
-
expect(["kimi-k2-0711-preview", "kimi-k2-0905-preview"].toSorted(compareCheapestFirst)[0]).toBe("kimi-k2-0905-preview");
|
|
238
|
-
});
|
|
@@ -97,7 +97,7 @@ const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): nu
|
|
|
97
97
|
|
|
98
98
|
const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
|
|
99
99
|
|
|
100
|
-
// How hard an id says it will think;
|
|
100
|
+
// How hard an id says it will think; only the quiet-rung rule below reads it, and `namesThinking` for a label.
|
|
101
101
|
const THINKING_RANK: Readonly<Record<string, number>> = {
|
|
102
102
|
minimal: 0,
|
|
103
103
|
none: 0,
|
|
@@ -144,28 +144,6 @@ export const compareModelIds = (left: string, right: string): number => {
|
|
|
144
144
|
// arrival order, since a registry can reorder a tied set between refreshes.
|
|
145
145
|
export const compareUnrankedModelIds = (left: string, right: string): number => compareModelIds(left, right) || left.localeCompare(right);
|
|
146
146
|
|
|
147
|
-
// The same tier scale read from the weakest end, for the caller that wants the cheapest model, not the strongest. An
|
|
148
|
-
// unranked family still sinks last on both orders; a tier-less catalog (Kimi) falls back to its newest release.
|
|
149
|
-
export const compareCheapestFirst = (left: string, right: string): number => {
|
|
150
|
-
const leftFamily = familyOf(left);
|
|
151
|
-
const rightFamily = familyOf(right);
|
|
152
|
-
return (
|
|
153
|
-
tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
|
|
154
|
-
// Before release: two rows differing only by thinking are the same model; recency must not pick between them.
|
|
155
|
-
thinkingRankOf(leftFamily) - thinkingRankOf(rightFamily) ||
|
|
156
|
-
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
157
|
-
releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
|
|
158
|
-
);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Tier only, not release or thinking level, so a downgrade stays legible as one. An unranked family is false on either
|
|
162
|
-
// side: an unranked candidate might be the next flagship, and an unranked pick's tier is unknown to have been beaten.
|
|
163
|
-
export const isCheaperRung = (candidate: string, pick: string): boolean => {
|
|
164
|
-
const candidateRank = tierRankOf(familyOf(candidate));
|
|
165
|
-
const pickRank = tierRankOf(familyOf(pick));
|
|
166
|
-
return candidateRank !== UNRANKED && pickRank !== UNRANKED && candidateRank > pickRank;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
147
|
// True only for an id that spells out a level above the quiet end, so an unannotated id is never accused of it.
|
|
170
148
|
// Exported so a settings row can label a pinned thinking variant.
|
|
171
149
|
export const namesThinking = (id: string): boolean => thinkingRankOf(familyOf(id)) > UNSTATED_THINKING;
|
package/src/models/model-pins.ts
CHANGED
|
@@ -22,10 +22,10 @@ export interface ModelChoice {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// A (provider, model) pair on the wire: `${provider}:${modelId}`, the same key shape the picker mints. Used to compare
|
|
25
|
-
// entries
|
|
25
|
+
// entries and dedupe a role list.
|
|
26
26
|
export const modelPinKey = (choice: ModelChoice): string => `${choice.provider}:${choice.model}`;
|
|
27
27
|
|
|
28
|
-
// Splits on the first colon only: a provider id never contains one, a model id might. Exported since
|
|
28
|
+
// Splits on the first colon only: a provider id never contains one, a model id might. Exported since a stored key
|
|
29
29
|
// stores these keys.
|
|
30
30
|
export const parsePinned = (pinned: string): ModelChoice | undefined => {
|
|
31
31
|
const separator = pinned.indexOf(`:`);
|
|
@@ -2,7 +2,7 @@ import { eventIterator, oc } from "@orpc/contract";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { SANDBOX_ROUTE_NAMES, SANDBOX_ROUTE_SHAPES, SANDBOX_ROUTES, sandboxRouteName } from "../index.js";
|
|
5
|
-
import { contractRoutes, routeNameForRequest, routeShapes } from "./routes.js";
|
|
5
|
+
import { contractRoutes, routeNameForRequest, routeShapes, streamOf } from "./routes.js";
|
|
6
6
|
|
|
7
7
|
const fixture = {
|
|
8
8
|
vpn: {
|
|
@@ -111,18 +111,38 @@ describe(`routeShapes`, () => {
|
|
|
111
111
|
expect(routeShapes(shaped(z.object({ a: z.string() })))[`vpn.list`]).not.toBe(shapes[`vpn.list`]);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it(`
|
|
115
|
-
// An oRPC event iterator wraps its output in an opaque type with no schema, so `watch` here keeps its name but
|
|
116
|
-
// no shape.
|
|
114
|
+
it(`fingerprints a stream through the frames it declares`, () => {
|
|
117
115
|
const withStream = {
|
|
116
|
+
vpn: {
|
|
117
|
+
list: fixture.vpn.list,
|
|
118
|
+
watch: oc.route({ method: "GET", path: "/vpn/watch" }).output(streamOf(z.object({ a: z.string() }))),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
expect(Object.keys(routeShapes(withStream)).toSorted()).toEqual([`vpn.list`, `vpn.watch`]);
|
|
122
|
+
// A changed frame is a changed route, which is the whole point of reaching past the iterator.
|
|
123
|
+
const reframed = { vpn: { watch: oc.route({ method: "GET", path: "/vpn/watch" }).output(streamOf(z.object({ a: z.number() }))) } };
|
|
124
|
+
expect(routeShapes(reframed)[`vpn.watch`]).not.toBe(routeShapes(withStream)[`vpn.watch`]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it(`tells a stream of X apart from a route that answers X`, () => {
|
|
128
|
+
const frame = z.object({ a: z.string() });
|
|
129
|
+
const streamed = { vpn: { watch: oc.route({ method: "GET", path: "/vpn/watch" }).output(streamOf(frame)) } };
|
|
130
|
+
const answered = { vpn: { watch: oc.route({ method: "GET", path: "/vpn/watch" }).output(frame) } };
|
|
131
|
+
expect(routeShapes(streamed)[`vpn.watch`]).not.toBe(routeShapes(answered)[`vpn.watch`]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it(`omits a route whose shape cannot be expressed rather than failing the walk`, () => {
|
|
135
|
+
// oRPC's own eventIterator hides its frames, which is why contracts declare streams with streamOf; one that
|
|
136
|
+
// slipped through keeps its name and loses only its shape, instead of breaking the walk for every other route.
|
|
137
|
+
const withRawIterator = {
|
|
118
138
|
vpn: {
|
|
119
139
|
list: fixture.vpn.list,
|
|
120
140
|
watch: oc.route({ method: "GET", path: "/vpn/watch" }).output(eventIterator(z.object({ a: z.string() }))),
|
|
121
141
|
},
|
|
122
142
|
};
|
|
123
|
-
expect(Object.keys(routeShapes(
|
|
143
|
+
expect(Object.keys(routeShapes(withRawIterator)).toSorted()).toEqual([`vpn.list`]);
|
|
124
144
|
expect(
|
|
125
|
-
contractRoutes(
|
|
145
|
+
contractRoutes(withRawIterator)
|
|
126
146
|
.map((route) => route.name)
|
|
127
147
|
.toSorted(),
|
|
128
148
|
).toEqual([`vpn.list`, `vpn.watch`]);
|
|
@@ -130,34 +150,47 @@ describe(`routeShapes`, () => {
|
|
|
130
150
|
});
|
|
131
151
|
|
|
132
152
|
describe(`the real sandbox contract`, () => {
|
|
133
|
-
it(`fingerprints
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
it(`fingerprints every route, streams included`, () => {
|
|
154
|
+
// Nothing may opt out: a route with no fingerprint is a route the browser can never tell has drifted, and the
|
|
155
|
+
// streaming ones (system.events, agent.attach, the exit dials) carry the frames a stale daemon breaks first.
|
|
156
|
+
expect(SANDBOX_ROUTE_NAMES.filter((name) => !(name in SANDBOX_ROUTE_SHAPES))).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it(`fingerprints the streaming routes eventIterator used to hide`, () => {
|
|
160
|
+
expect(
|
|
161
|
+
[
|
|
162
|
+
`agent.attach`,
|
|
163
|
+
`capabilities.add`,
|
|
164
|
+
`exit.rotate`,
|
|
165
|
+
`exit.start`,
|
|
166
|
+
`exit.use`,
|
|
167
|
+
`intentic.applyEvents`,
|
|
168
|
+
`intentic.run`,
|
|
169
|
+
`netdisk.mount`,
|
|
170
|
+
`system.events`,
|
|
171
|
+
`system.manageDeviceSandbox`,
|
|
172
|
+
`system.runDeviceAgentFlow`,
|
|
173
|
+
`vpn.connect`,
|
|
174
|
+
].filter((name) => name in SANDBOX_ROUTE_SHAPES).toSorted(),
|
|
175
|
+
).toEqual([
|
|
138
176
|
`agent.attach`,
|
|
139
177
|
`capabilities.add`,
|
|
140
|
-
// The three exit moves stream since bringing an exit up dials and verifies over tens of seconds, failing at
|
|
141
|
-
// any step.
|
|
142
178
|
`exit.rotate`,
|
|
143
179
|
`exit.start`,
|
|
144
180
|
`exit.use`,
|
|
145
181
|
`intentic.applyEvents`,
|
|
146
182
|
`intentic.run`,
|
|
147
|
-
// A mount dials a file server and can fail at any step with something to read, like a vpn dial.
|
|
148
183
|
`netdisk.mount`,
|
|
149
184
|
`system.events`,
|
|
150
185
|
`system.manageDeviceSandbox`,
|
|
151
|
-
// The device agent updates or restarts itself; the stream dies with the process, so lines arrive as they
|
|
152
|
-
// happen.
|
|
153
186
|
`system.runDeviceAgentFlow`,
|
|
154
187
|
`vpn.connect`,
|
|
155
188
|
]);
|
|
156
189
|
});
|
|
157
190
|
|
|
158
|
-
it(`fingerprints every
|
|
191
|
+
it(`fingerprints every route exactly once`, () => {
|
|
159
192
|
expect(Object.keys(SANDBOX_ROUTE_SHAPES).every((name) => SANDBOX_ROUTE_NAMES.includes(name))).toBe(true);
|
|
160
|
-
expect(Object.keys(SANDBOX_ROUTE_SHAPES).length).toBe(SANDBOX_ROUTE_NAMES.length
|
|
193
|
+
expect(Object.keys(SANDBOX_ROUTE_SHAPES).length).toBe(SANDBOX_ROUTE_NAMES.length);
|
|
161
194
|
});
|
|
162
195
|
|
|
163
196
|
it(`derives a route table with no duplicate names`, () => {
|
package/src/protocol/routes.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import { eventIterator } from "@orpc/contract";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
// Named route surface of the daemon's contract (`<group>.<route>`), derived automatically so nothing here is
|
|
4
5
|
// hand-maintained. The daemon advertises which routes it implements (the /events hello frame); the browser diffs that
|
|
5
6
|
// against its own contract so an old daemon's gap is a named feature check, not a silent 404.
|
|
6
7
|
|
|
8
|
+
// Every streamed route declares its frames through this, never oRPC's `eventIterator` directly: that returns an opaque
|
|
9
|
+
// standard-schema validator holding no reachable inner schema, so a stream's payload would be unfingerprintable and its
|
|
10
|
+
// drift invisible. The frame schema rides along under a symbol, which oRPC never reads and JSON never serializes.
|
|
11
|
+
// Typed as a plain `symbol`, not the inferred unique one: a unique symbol would ride into every contract's inferred
|
|
12
|
+
// type and break declaration emit on a local name nothing outside can refer to.
|
|
13
|
+
const FRAME: symbol = Symbol.for("intentic.contract.frame");
|
|
14
|
+
|
|
15
|
+
export const streamOf = <T extends z.ZodType>(frame: T) => Object.assign(eventIterator(frame), { [FRAME]: frame });
|
|
16
|
+
|
|
17
|
+
// The frame schema behind a streamed route's validator, or undefined for an ordinary request/response schema.
|
|
18
|
+
const frameOf = (schema: unknown): z.ZodType | undefined =>
|
|
19
|
+
typeof schema === "object" && schema !== null && FRAME in schema ? ((schema as Record<symbol, unknown>)[FRAME] as z.ZodType) : undefined;
|
|
20
|
+
|
|
7
21
|
// Structural shape of `~orpc.route`, the metadata oRPC attaches to every `oc.route(...)` procedure; read this way since
|
|
8
22
|
// oRPC's internal types aren't public.
|
|
9
23
|
interface ContractProcedureLike {
|
|
@@ -85,18 +99,24 @@ const fingerprint = (value: unknown): string => {
|
|
|
85
99
|
return hash.toString(36);
|
|
86
100
|
};
|
|
87
101
|
|
|
88
|
-
// One route
|
|
89
|
-
// matters: a `.default()` field is optional in and required out
|
|
102
|
+
// One side of a route as JSON Schema. A streamed side is read through its frame and tagged, so "answers X" and "streams
|
|
103
|
+
// frames of X" can never fingerprint alike. `io` matters: a `.default()` field is optional in and required out.
|
|
104
|
+
const wireShape = (schema: unknown, io: "input" | "output"): unknown => {
|
|
105
|
+
if (schema === undefined) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const frame = frameOf(schema);
|
|
109
|
+
return frame === undefined ? z.toJSONSchema(schema as z.ZodType, { io }) : { stream: z.toJSONSchema(frame, { io }) };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// One route's wire shape, or undefined if it can't be expressed at all.
|
|
90
113
|
const procedureShape = (value: unknown): string | undefined => {
|
|
91
114
|
if (typeof value !== "object" || value === null || !("~orpc" in value)) {
|
|
92
115
|
return undefined;
|
|
93
116
|
}
|
|
94
117
|
const { inputSchema, outputSchema } = (value as ContractSchemasLike)["~orpc"];
|
|
95
118
|
try {
|
|
96
|
-
return fingerprint({
|
|
97
|
-
in: inputSchema === undefined ? undefined : z.toJSONSchema(inputSchema as z.ZodType, { io: "input" }),
|
|
98
|
-
out: outputSchema === undefined ? undefined : z.toJSONSchema(outputSchema as z.ZodType, { io: "output" }),
|
|
99
|
-
});
|
|
119
|
+
return fingerprint({ in: wireShape(inputSchema, "input"), out: wireShape(outputSchema, "output") });
|
|
100
120
|
} catch {
|
|
101
121
|
return undefined;
|
|
102
122
|
}
|
package/src/schemas/agent.ts
CHANGED
|
@@ -260,8 +260,6 @@ export const AgentTurnSchema = z
|
|
|
260
260
|
.describe(
|
|
261
261
|
"Ask for the same work at a higher rate for a higher price. A request rather than a promise: the answer says what actually happened.",
|
|
262
262
|
),
|
|
263
|
-
// Vetoes automatic tier selection for this turn; the judge still runs and records its verdict, but nothing is
|
|
264
|
-
// substituted.
|
|
265
263
|
// Set by the composer on the one turn whose model the Auto judge chose; the daemon only records it.
|
|
266
264
|
autoPicked: z
|
|
267
265
|
.boolean()
|
|
@@ -269,12 +267,6 @@ export const AgentTurnSchema = z
|
|
|
269
267
|
.describe(
|
|
270
268
|
"Whether this turn's model was chosen for you by reading the conversation's opening message, rather than picked by hand. Recorded so the choice can be judged later against what you did next.",
|
|
271
269
|
),
|
|
272
|
-
tierHold: z
|
|
273
|
-
.boolean()
|
|
274
|
-
.optional()
|
|
275
|
-
.describe(
|
|
276
|
-
"Run exactly the model that was picked, even when the turn looks simple enough for a cheaper one. The judgement is still recorded; nothing is substituted.",
|
|
277
|
-
),
|
|
278
270
|
// The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
|
|
279
271
|
editorContext: EditorContextSchema.optional().describe(
|
|
280
272
|
'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
|
package/src/schemas/agents.ts
CHANGED
|
@@ -241,17 +241,6 @@ export const AgentSummarySchema = z.object({
|
|
|
241
241
|
effort: z.string().optional().describe("How hard that turn was told to think."),
|
|
242
242
|
thinking: z.boolean().optional().describe("Whether that turn showed its reasoning."),
|
|
243
243
|
fast: z.boolean().optional().describe("Whether that turn asked for higher speed. What was asked for, not what was served."),
|
|
244
|
-
// The judge's verdict on the last turn, not what actually ran: seeds tomorrow's composer preview, absent means
|
|
245
|
-
// nothing judged yet.
|
|
246
|
-
tier: z
|
|
247
|
-
.enum(["fast", "standard"])
|
|
248
|
-
.optional()
|
|
249
|
-
.describe("How hard its last turn looked to the complexity judge. What the next turn's preview needs, not what actually ran."),
|
|
250
|
-
// Standing per-conversation choice, restored into the composer on open and sent back every turn, like `fast` above.
|
|
251
|
-
tierHold: z
|
|
252
|
-
.boolean()
|
|
253
|
-
.optional()
|
|
254
|
-
.describe("Whether this conversation is pinned to the picked model, so a turn that looks simple is never moved to a cheaper one."),
|
|
255
244
|
account: z.string().optional().describe("Which connected account paid for it."),
|
|
256
245
|
// The worktree branch (agent/<id>); absent for a non-isolated (main-tree) conversation.
|
|
257
246
|
branch: z.string().optional().describe("The branch its private copy works on. Absent for a conversation that works directly in the shared tree."),
|
|
@@ -420,5 +420,8 @@ export const CapabilityProbeSchema = z.object({
|
|
|
420
420
|
message: z
|
|
421
421
|
.string()
|
|
422
422
|
.describe("What happened, in the words a person standing in front of the form needs: the service's own answer, or its refusal."),
|
|
423
|
+
// The account the credential turned out to be: a login, an org slug, a project name. Only when the card declares
|
|
424
|
+
// where to read it and the service answered with one; a form offers it as the connection's name.
|
|
425
|
+
who: z.string().optional().describe("Who the service said the credential belongs to, when it said."),
|
|
423
426
|
});
|
|
424
427
|
export type CapabilityProbe = z.infer<typeof CapabilityProbeSchema>;
|
package/src/schemas/devices.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What one of the user's own machines is running.
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { type HostFacts, HostFactsSchema, WslEnvironmentSchema } from "./hosts.js";
|
|
3
|
+
import { hostCardOf, hostEnvironmentOf, type HostFacts, HostFactsSchema, WslEnvironmentSchema } from "./hosts.js";
|
|
4
4
|
import { DEV_VERSION } from "../state/versions.js";
|
|
5
5
|
// Desktop-sync report shape shared by the agent, daemon and browser, produced only by `intentic-machine status --json`.
|
|
6
6
|
// The agent never reports `sandboxes`; the docker half is filled in by whoever reads the report, scoped to the reader's
|
|
@@ -451,54 +451,100 @@ export type DevicesList = z.infer<typeof DevicesListSchema>;
|
|
|
451
451
|
// The physical computer a device is an environment of. Windows and every WSL distro on it are one machine with one
|
|
452
452
|
// Docker engine, one screen and one set of disks, each environment holding its own agent and its own door.
|
|
453
453
|
export interface Machine {
|
|
454
|
-
/**
|
|
454
|
+
/** The card its doors hang off, else the hostname they share, else a lone uncarded device's own key. */
|
|
455
455
|
readonly key: string;
|
|
456
456
|
readonly label: string;
|
|
457
|
-
/** Windows first, then distros by
|
|
457
|
+
/** Windows first, then distros by the name WSL registered: the side that owns the screen leads. */
|
|
458
458
|
readonly environments: readonly Device[];
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
export const deviceHostname = (device: Device): string | undefined => device.facts?.hostname ?? device.report?.hostname;
|
|
462
|
-
export const deviceEnvironment = (device: Device): string | undefined => environmentOf(device.facts, device.report);
|
|
463
|
-
export const isWslDevice = (device: Device): boolean => deviceEnvironment(device)?.startsWith("wsl:") === true;
|
|
464
462
|
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
463
|
+
// Which side of a machine a device is. Its door id names the environment it connected under (`<card>::wsl:<distro>`),
|
|
464
|
+
// and that is all an environment nobody has reached since this daemon booted can say about itself: liveness resets on
|
|
465
|
+
// restart, so facts and reports are absent on a side that is merely asleep.
|
|
466
|
+
export const deviceEnvironment = (device: Device): string | undefined =>
|
|
467
|
+
environmentOf(device.facts, device.report) ?? (device.hostId === undefined ? undefined : hostEnvironmentOf(device.hostId));
|
|
468
|
+
|
|
469
|
+
const WSL_PREFIX = "wsl:";
|
|
470
|
+
export const isWslDevice = (device: Device): boolean => deviceEnvironment(device)?.startsWith(WSL_PREFIX) === true;
|
|
471
|
+
// The distro a device runs, by whatever evidence it holds; absent on a native install and on one that has said nothing.
|
|
472
|
+
export const deviceDistro = (device: Device): string | undefined => {
|
|
473
|
+
const environment = deviceEnvironment(device);
|
|
474
|
+
return environment?.startsWith(WSL_PREFIX) === true ? environment.slice(WSL_PREFIX.length) : undefined;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// Native first, then distros by the name WSL registered them under rather than by their door id, which is that same
|
|
478
|
+
// name behind a card's and so would order a PC's distros by which card each was connected through.
|
|
479
|
+
const byEnvironment = (a: Device, b: Device): number =>
|
|
480
|
+
Number(isWslDevice(a)) - Number(isWslDevice(b)) || (deviceDistro(a) ?? a.label).localeCompare(deviceDistro(b) ?? b.label);
|
|
468
481
|
|
|
469
482
|
const hostnameKey = (device: Device): string | undefined => deviceHostname(device)?.toLowerCase();
|
|
470
483
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
484
|
+
// The card a device's door hangs off: one card is one computer, so two doors naming the same card are one machine
|
|
485
|
+
// whatever either has said about itself — which is the whole of what an environment that has never connected says.
|
|
486
|
+
const cardKey = (device: Device): string | undefined => (device.hostId === undefined ? undefined : hostCardOf(device.hostId));
|
|
487
|
+
|
|
488
|
+
// What makes two devices one computer: a shared card, or a shared hostname where a distro answers to it, since WSL
|
|
489
|
+
// hands a distro the Windows machine's name. Two native installs that merely share a name stay two machines.
|
|
490
|
+
const tokensOf = (device: Device, distroNames: ReadonlySet<string>): string[] => {
|
|
491
|
+
const card = cardKey(device);
|
|
492
|
+
const hostname = hostnameKey(device);
|
|
493
|
+
return [
|
|
494
|
+
...(card === undefined ? [] : [`card:${card.toLowerCase()}`]),
|
|
495
|
+
...(hostname !== undefined && distroNames.has(hostname) ? [`host:${hostname}`] : []),
|
|
496
|
+
];
|
|
475
497
|
};
|
|
476
498
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return { key: hostname, label: hostname, environments };
|
|
484
|
-
};
|
|
499
|
+
// Components over both joins at once: a device holding a card token and a hostname token is the evidence that merges
|
|
500
|
+
// the machines those tokens named, so a distro connected under a card of its own still lands on the PC it runs on.
|
|
501
|
+
interface Component {
|
|
502
|
+
readonly tokens: Set<string>;
|
|
503
|
+
readonly devices: Device[];
|
|
504
|
+
}
|
|
485
505
|
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
const
|
|
506
|
+
const componentsOf = (devices: readonly Device[]): Component[] => {
|
|
507
|
+
const distroNames = new Set(devices.filter(isWslDevice).map(hostnameKey).filter((name) => name !== undefined));
|
|
508
|
+
const components: Component[] = [];
|
|
489
509
|
for (const device of devices) {
|
|
490
|
-
|
|
510
|
+
const tokens = tokensOf(device, distroNames);
|
|
511
|
+
const [head, ...merged] = components.filter((component) => tokens.some((token) => component.tokens.has(token)));
|
|
512
|
+
if (head === undefined) {
|
|
513
|
+
components.push({ tokens: new Set(tokens), devices: [device] });
|
|
491
514
|
continue;
|
|
492
515
|
}
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
516
|
+
for (const other of merged) {
|
|
517
|
+
other.tokens.forEach((token) => head.tokens.add(token));
|
|
518
|
+
head.devices.push(...other.devices);
|
|
519
|
+
components.splice(components.indexOf(other), 1);
|
|
496
520
|
}
|
|
497
|
-
|
|
521
|
+
for (const token of tokens) {
|
|
522
|
+
head.tokens.add(token);
|
|
523
|
+
}
|
|
524
|
+
head.devices.push(device);
|
|
498
525
|
}
|
|
499
|
-
return
|
|
526
|
+
return components;
|
|
500
527
|
};
|
|
501
528
|
|
|
529
|
+
// Addressed by the card of the side that owns the screen, an address that does not change when a second environment
|
|
530
|
+
// connects — unlike a row key, which is a hostname another environment can take first. An uncarded fold has only the
|
|
531
|
+
// hostname its doors share, and an uncarded lone device its own key. Called what the owner calls it: the name the
|
|
532
|
+
// leading environment carries, unless that is nothing but its door id, which reads as a PC named after one of its
|
|
533
|
+
// own sides.
|
|
534
|
+
const machineOf = (environments: readonly Device[]): Machine => {
|
|
535
|
+
const [first, ...rest] = environments;
|
|
536
|
+
// A component holds the device that made it; the empty case only keeps the type honest.
|
|
537
|
+
if (first === undefined) {
|
|
538
|
+
return { key: "", label: "", environments };
|
|
539
|
+
}
|
|
540
|
+
const named = environments.map(cardKey).find((key) => key !== undefined) ?? (rest.length === 0 ? undefined : deviceHostname(first));
|
|
541
|
+
const label = first.label === first.hostId ? (cardKey(first) ?? first.label) : first.label;
|
|
542
|
+
return { key: named ?? first.key, label, environments };
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export const machinesOf = (devices: readonly Device[]): Machine[] =>
|
|
546
|
+
componentsOf(devices).map((component) => machineOf(component.devices.toSorted(byEnvironment)));
|
|
547
|
+
|
|
502
548
|
// Every door whose docker reports a given sandbox slug, in list order. More than one is the ordinary case, not a
|
|
503
549
|
// conflict: Windows and the WSL distros on it share one engine, so each door answers for the same containers.
|
|
504
550
|
const doorsRunningSandbox = (devices: readonly Device[], slug: string | undefined): Device[] =>
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { AgentProviderSchema } from "./agent.js";
|
|
3
3
|
|
|
4
4
|
// Asked once per chat, on the message it opens with; answers with the model, effort and account that whole
|
|
5
|
-
// conversation runs on.
|
|
5
|
+
// conversation runs on. The one place a model is chosen for a chat, and it runs once: from turn two the pick is an
|
|
6
6
|
// cheaper rung of the provider already picked: this one chooses freely, and only before anything has run.
|
|
7
7
|
|
|
8
8
|
export const ModelRouteAskSchema = z.object({
|
|
@@ -91,17 +91,6 @@ export const UsageTurnSchema = z.object({
|
|
|
91
91
|
compactions: z.number().optional(),
|
|
92
92
|
contextTokens: z.number().optional(),
|
|
93
93
|
contextWindow: z.number().optional(),
|
|
94
|
-
// tierScore (0..1 from judgeComplexity, vs FAST_CEILING) absent means the judge did not run. tierRules is which
|
|
95
|
-
// named features fired. tierRouted is whether it actually ran on the cheap rung, not implied by the score.
|
|
96
|
-
tierScore: z.number().optional(),
|
|
97
|
-
tierRules: z.array(z.string()).optional(),
|
|
98
|
-
tierRouted: z.boolean().optional(),
|
|
99
|
-
// The verdict and the cutoff it was judged against (settings.autoTierEagerness); absent rows predate the knob and
|
|
100
|
-
// fall back to FAST_CEILING.
|
|
101
|
-
tierFast: z.boolean().optional(),
|
|
102
|
-
tierCeiling: z.number().optional(),
|
|
103
|
-
// The turn carried AgentTurn.tierHold: the user vetoed a fast verdict. Absent means no veto.
|
|
104
|
-
tierDenied: z.boolean().optional(),
|
|
105
94
|
// This turn's model was chosen by the Auto judge reading the conversation's opening message, not picked by hand.
|
|
106
95
|
// Marked on that one turn only; a later row of the same conversation naming a different model is the user
|
|
107
96
|
// overruling it, which is the escalation rate this feature has to be able to answer for.
|
package/src/schemas/settings.ts
CHANGED
|
@@ -314,35 +314,6 @@ export const SandboxSettingsSchema = z.object({
|
|
|
314
314
|
.describe(
|
|
315
315
|
"Which repositories keep a changelog, and so get a user-facing note written alongside each merge. A list rather than a switch, and empty by default, because the commit writer's standing rule is to copy the house style rather than impose one, and a repository that has never written such a note gives it nothing to copy.",
|
|
316
316
|
),
|
|
317
|
-
// off: the judge never runs.
|
|
318
|
-
// shadow (default): the judge scores every turn to the ledger; nothing is routed.
|
|
319
|
-
// on: a turn judged fast runs on the cheap rung, where the provider publishes one.
|
|
320
|
-
// judge: the deterministic scorer keeps writing shadow rows but routes nothing; the Auto picker row is offered
|
|
321
|
-
// instead, and a model reads a new chat's opening message once to choose what the whole conversation runs on.
|
|
322
|
-
// Exclusive with `on` because the two answer the same question at different moments, and a per-turn downgrade
|
|
323
|
-
// underneath a picked-by-model conversation would be overruling a choice already made about this chat.
|
|
324
|
-
autoTier: z
|
|
325
|
-
.enum(["off", "shadow", "on", "judge"])
|
|
326
|
-
.default("shadow")
|
|
327
|
-
.describe(
|
|
328
|
-
"Whether an easy-looking turn may run on a cheaper model from the same provider. Three states rather than a switch, because the middle one is the only honest road to the third: it scores every turn and routes nothing, so the guess can become a measurement before it changes anything. It can only ever route down, so the worst case is one turn's quality rather than a bill nobody asked for. The fourth, Auto, answers a different question: rather than downgrading turns one by one, it offers an Auto row in the model picker and has a model read a new chat's first message to choose what that whole conversation runs on.",
|
|
329
|
-
),
|
|
330
|
-
// `balanced` is what every verdict recorded before this setting existed was judged against, so shadow history stays
|
|
331
|
-
// comparable.
|
|
332
|
-
autoTierEagerness: z
|
|
333
|
-
.enum(["cautious", "balanced", "eager"])
|
|
334
|
-
.default("balanced")
|
|
335
|
-
.describe(
|
|
336
|
-
"How readily a turn counts as simple enough for the cheaper model. It moves only the cutoff: at every setting a turn still has to say something positively easy, so nothing here can downgrade a short vague request.",
|
|
337
|
-
),
|
|
338
|
-
// Auto uses the same cheap-end order as this list (`compareCheapestFirst`), so the two can never disagree.
|
|
339
|
-
autoFastModels: z
|
|
340
|
-
.array(z.string())
|
|
341
|
-
.max(10)
|
|
342
|
-
.default([])
|
|
343
|
-
.describe(
|
|
344
|
-
"Which cheaper model a downgraded turn lands on. A list so a sandbox spanning providers can name a rung on each, but not a fallback ladder: an entry naming a different provider than the turn is on is skipped rather than tried, because switching provider retires the conversation and starting over to save a fraction of a penny is not a saving. Empty picks the cheapest the turn's own provider publishes.",
|
|
345
|
-
),
|
|
346
317
|
agentRetentionDays: z
|
|
347
318
|
.number()
|
|
348
319
|
.min(0)
|
|
@@ -522,27 +493,6 @@ export const TurnExperimentSchema = z.object({
|
|
|
522
493
|
cohort: z.string().optional(),
|
|
523
494
|
});
|
|
524
495
|
export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
|
|
525
|
-
// Absent when the experiment isn't running (flag off, or no holdout set); absence reads as "not measured", not as zero.
|
|
526
|
-
// Read off the spend ledger's tier fields over the window; no counterfactual $ saved, since the ledger holds only what
|
|
527
|
-
// turns cost. Not a `TurnExperiment`: there's no randomized control, routing follows the settings mode over time, not a
|
|
528
|
-
// coin flip. Absent (no turn judged, autoTier off) reads as "not measured", not "measured, found nothing".
|
|
529
|
-
export const TierReportSchema = z.object({
|
|
530
|
-
// Turns the judge ran on in the window, the denominator under everything below.
|
|
531
|
-
judged: z.number(),
|
|
532
|
-
// …of which landed at or below FAST_CEILING: the turns that looked simple. fast ÷ judged is the fast share.
|
|
533
|
-
fast: z.number(),
|
|
534
|
-
// Upper bound on any saving, never an estimate: moving these turns to the cheap rung would have cost something too.
|
|
535
|
-
atStakeUsd: z.number(),
|
|
536
|
-
// Turns that actually ran the cheap rung, and what they cost there. Realized, not projected.
|
|
537
|
-
routed: z.number(),
|
|
538
|
-
routedUsd: z.number(),
|
|
539
|
-
// The guardrail: fast-judged turns whose very next ledger row asked for a dearer model. Past a few percent of
|
|
540
|
-
// `fast`, the judge costs more in trust than it saves.
|
|
541
|
-
escalated: z.number(),
|
|
542
|
-
// Fast-judged turns the user vetoed outright (UsageTurn.tierDenied): the same signal, said even louder.
|
|
543
|
-
denied: z.number(),
|
|
544
|
-
});
|
|
545
|
-
export type TierReport = z.infer<typeof TierReportSchema>;
|
|
546
496
|
// One dependency version or library improvement suggested or pinned.
|
|
547
497
|
export const DependencyImprovementSchema = z.object({
|
|
548
498
|
prevented: z.string(),
|
|
@@ -564,8 +514,6 @@ export const SavingsReportSchema = z.object({
|
|
|
564
514
|
search: TurnExperimentSchema.optional(),
|
|
565
515
|
// Same absence rule as `search`: not measured, never zero.
|
|
566
516
|
map: TurnExperimentSchema.optional(),
|
|
567
|
-
// Automatic tier selection's readout, see TierReportSchema. Absent ⇒ nothing was judged in the window.
|
|
568
|
-
tier: TierReportSchema.optional(),
|
|
569
517
|
dependencies: DependencySavingsSchema.optional(),
|
|
570
518
|
});
|
|
571
519
|
export type SavingsReport = z.infer<typeof SavingsReportSchema>;
|
|
@@ -11,6 +11,13 @@ export const WorkspaceSearchQuerySchema = z.object({
|
|
|
11
11
|
"Narrow the search to one kind: plain text, filenames, definitions, references, symbols, or code structure. Leave it out to blend them, which also answers a question asked in words.",
|
|
12
12
|
),
|
|
13
13
|
includeIgnored: z.stringbool().optional().describe("Search inside installed packages and other ignored folders too."),
|
|
14
|
+
// One subtree, root-relative, no trailing slash: the caller's own scope (a UI narrowed to one project) rather than a
|
|
15
|
+
// pattern. Unlike `include` it is a prefix, so it composes with whatever globs the user typed.
|
|
16
|
+
dir: z
|
|
17
|
+
.string()
|
|
18
|
+
.max(512)
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Only look inside this folder, given as a path from the workspace root. Leave it out to search everything."),
|
|
14
21
|
// The three switches every search box has; caseSensitive off means case-insensitive, not ripgrep's smart case.
|
|
15
22
|
literal: z.stringbool().optional().describe("Treat the query as fixed text rather than a pattern."),
|
|
16
23
|
word: z.stringbool().optional().describe("Match whole words only."),
|
|
@@ -32,6 +32,9 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
32
32
|
{ path: "provider-refusals.json", portability: "carry" },
|
|
33
33
|
// Models refused to this sandbox's credentials; expires daily, same subscriptions apply elsewhere.
|
|
34
34
|
{ path: "model-refusals.json", portability: "carry" },
|
|
35
|
+
// What each account has run out of, for a plan publishing no allowance to poll — the refusal is the reading.
|
|
36
|
+
// Carried like the refusals above: an allowance belongs to the account, not to the machine that spent it.
|
|
37
|
+
{ path: "observed-limits.json", portability: "carry" },
|
|
35
38
|
// First-time dependency setup requests; carried so an interrupted queue survives export, entries self-remove.
|
|
36
39
|
{ path: "dependency-requests.json", portability: "carry" },
|
|
37
40
|
// Which device cards were already offered; carry, not identity, since it holds ids, never a token.
|