@intentic/sandbox-contract 1.345.0 → 1.347.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 +26 -14
- package/dist/contracts/accounts.contract.d.ts +0 -80
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +33 -2
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +910 -29
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +43 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +2 -1
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +0 -1
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.d.ts +0 -1
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +134 -123
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +4 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +102 -1
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +0 -80
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +43 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +42 -37
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/failure-rows.d.ts +14 -0
- package/dist/events/failure-rows.d.ts.map +1 -0
- package/dist/events/failure-rows.js +109 -0
- package/dist/events/failure-rows.js.map +1 -0
- package/dist/events/sandbox-notice.d.ts +16 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +1 -0
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +68 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +7 -1
- package/dist/events/system-events.js.map +1 -1
- package/dist/index.d.ts +1158 -303
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent-lanes.d.ts.map +1 -1
- package/dist/models/agent-lanes.js +3 -5
- package/dist/models/agent-lanes.js.map +1 -1
- package/dist/models/agent-standing.d.ts +3 -20
- package/dist/models/agent-standing.d.ts.map +1 -1
- package/dist/models/agent-standing.js +12 -37
- package/dist/models/agent-standing.js.map +1 -1
- package/dist/models/model-pins.d.ts +3 -0
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js +2 -0
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/plan-pools.d.ts +23 -5
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +116 -33
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +1 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +1 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +3 -1
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +4 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +1 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/agent-next.d.ts +21 -0
- package/dist/schemas/agent-next.d.ts.map +1 -0
- package/dist/schemas/agent-next.js +21 -0
- package/dist/schemas/agent-next.js.map +1 -0
- package/dist/schemas/agent-search.d.ts +45 -0
- package/dist/schemas/agent-search.d.ts.map +1 -0
- package/dist/schemas/agent-search.js +28 -0
- package/dist/schemas/agent-search.js.map +1 -0
- package/dist/schemas/agent.d.ts +9 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +14 -10
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +16 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +48 -43
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +10 -33
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/compose-overlay.d.ts +9 -0
- package/dist/schemas/compose-overlay.d.ts.map +1 -0
- package/dist/schemas/compose-overlay.js +11 -0
- package/dist/schemas/compose-overlay.js.map +1 -0
- package/dist/schemas/composer-drafts.d.ts +169 -0
- package/dist/schemas/composer-drafts.d.ts.map +1 -0
- package/dist/schemas/composer-drafts.js +60 -0
- package/dist/schemas/composer-drafts.js.map +1 -0
- package/dist/schemas/device-commands.d.ts +63 -0
- package/dist/schemas/device-commands.d.ts.map +1 -0
- package/dist/schemas/device-commands.js +42 -0
- package/dist/schemas/device-commands.js.map +1 -0
- package/dist/schemas/device-folder.d.ts +11 -0
- package/dist/schemas/device-folder.d.ts.map +1 -1
- package/dist/schemas/device-folder.js +1 -0
- package/dist/schemas/device-folder.js.map +1 -1
- package/dist/schemas/devices.d.ts +14 -67
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +7 -45
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/hosts.d.ts +27 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +21 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/privacy-egress.d.ts +41 -0
- package/dist/schemas/privacy-egress.d.ts.map +1 -0
- package/dist/schemas/privacy-egress.js +30 -0
- package/dist/schemas/privacy-egress.js.map +1 -0
- package/dist/schemas/privacy.d.ts +1 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +1 -0
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +2 -130
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +5 -23
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +0 -82
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +1 -6
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/refusals.d.ts +64 -0
- package/dist/schemas/providers/refusals.d.ts.map +1 -0
- package/dist/schemas/providers/refusals.js +33 -0
- package/dist/schemas/providers/refusals.js.map +1 -0
- package/dist/schemas/sessions.js +1 -1
- package/dist/schemas/sessions.js.map +1 -1
- package/dist/schemas/settings.d.ts +4 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +2 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +16 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +11 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/state/definition.d.ts +8 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/sandbox-build.d.ts +20 -0
- package/dist/state/sandbox-build.d.ts.map +1 -0
- package/dist/state/sandbox-build.js +43 -0
- package/dist/state/sandbox-build.js.map +1 -0
- package/dist/state/ways-back.d.ts +4 -0
- package/dist/state/ways-back.d.ts.map +1 -0
- package/dist/state/ways-back.js +8 -0
- package/dist/state/ways-back.js.map +1 -0
- package/dist/text/land-rows.d.ts +14 -0
- package/dist/text/land-rows.d.ts.map +1 -0
- package/dist/text/land-rows.js +86 -0
- package/dist/text/land-rows.js.map +1 -0
- package/dist/text/transcript-fold.d.ts +4 -2
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -187
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +6 -6
- package/src/contracts/agents.contract.ts +49 -25
- package/src/events/agent-events.ts +95 -78
- package/src/events/failure-rows.ts +148 -0
- package/src/events/sandbox-notice.ts +11 -2
- package/src/events/system-events.ts +14 -4
- package/src/ids/project-dir.test.ts +2 -2
- package/src/ids/project-dir.ts +4 -4
- package/src/models/agent-lanes.ts +7 -21
- package/src/models/agent-runtimes.ts +2 -2
- package/src/models/agent-standing.ts +31 -96
- package/src/models/model-pins.test.ts +31 -1
- package/src/models/model-pins.ts +17 -0
- package/src/models/plan-pools.test.ts +159 -24
- package/src/models/plan-pools.ts +215 -83
- package/src/protocol/doors/runner-protocol.ts +4 -1
- package/src/protocol/raw/raw-routes.ts +1 -0
- package/src/schemas/agent-next.ts +34 -0
- package/src/schemas/agent-search.ts +51 -0
- package/src/schemas/agent.ts +21 -12
- package/src/schemas/agents.ts +24 -63
- package/src/schemas/compose-overlay.test.ts +35 -0
- package/src/schemas/compose-overlay.ts +19 -0
- package/src/schemas/composer-drafts.ts +86 -0
- package/src/schemas/device-commands.ts +89 -0
- package/src/schemas/device-folder.ts +9 -0
- package/src/schemas/devices.ts +24 -106
- package/src/schemas/environment.ts +8 -9
- package/src/schemas/hosts.ts +56 -0
- package/src/schemas/machine-presence.test.ts +33 -0
- package/src/schemas/privacy-egress.test.ts +83 -0
- package/src/schemas/privacy-egress.ts +86 -0
- package/src/schemas/privacy.ts +4 -0
- package/src/schemas/project-delivery.ts +1 -1
- package/src/schemas/providers/plan-limits.test.ts +16 -0
- package/src/schemas/providers/plan-limits.ts +8 -49
- package/src/schemas/providers/provider-oauth.ts +1 -13
- package/src/schemas/providers/refusals.ts +62 -0
- package/src/schemas/sessions.ts +1 -1
- package/src/schemas/settings.ts +2 -0
- package/src/schemas/system.ts +15 -0
- package/src/state/runtime-state.ts +5 -0
- package/src/state/sandbox-build.test.ts +95 -0
- package/src/state/sandbox-build.ts +96 -0
- package/src/state/ways-back.test.ts +36 -0
- package/src/state/ways-back.ts +19 -0
- package/src/state/workspace-state.ts +1 -1
- package/src/text/land-rows.ts +111 -0
- package/src/text/transcript-fold.test.ts +38 -9
- package/src/text/transcript-fold.ts +30 -232
- package/dist/events/undelivered.d.ts +0 -10
- package/dist/events/undelivered.d.ts.map +0 -1
- package/dist/events/undelivered.js +0 -22
- package/dist/events/undelivered.js.map +0 -1
- package/dist/state/history-state.d.ts +0 -3
- package/dist/state/history-state.d.ts.map +0 -1
- package/dist/state/history-state.js +0 -153
- package/dist/state/history-state.js.map +0 -1
- package/src/events/undelivered.ts +0 -33
- package/src/state/history-state.ts +0 -313
package/src/models/plan-pools.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AccountState, AccountUsage, ProviderRefusal, UsageWindow, WindowGates } from "../schemas/providers/plan-limits.js";
|
|
2
|
+
import { humanizeModelId } from "./model-label.js";
|
|
2
3
|
|
|
3
4
|
// Single reading of which account pool gates a given model (or, with none named, the account's tightest pool), shared
|
|
4
5
|
// by the daemon's picker/refusal logic and the browser's rings and rail. A pool gated `none` is excluded from the
|
|
@@ -191,8 +192,9 @@ export const currentWindows = (usage: AccountUsage, now: number): UsageWindow[]
|
|
|
191
192
|
});
|
|
192
193
|
};
|
|
193
194
|
|
|
194
|
-
// Whether an account can serve a turn
|
|
195
|
-
//
|
|
195
|
+
// Whether an account can serve a turn, and what one provider's accounts can serve together: the one rule every picker,
|
|
196
|
+
// gate, listing and surface reads. The daemon and the editor run it on the same facts (the account rows and every
|
|
197
|
+
// refusal on file); neither keeps a judged copy, so the two cannot disagree.
|
|
196
198
|
|
|
197
199
|
/** Everything the serviceability rule reads of one account, as the account rows already carry it. */
|
|
198
200
|
export interface ServiceFacts {
|
|
@@ -209,6 +211,9 @@ export interface ServiceFacts {
|
|
|
209
211
|
readonly label?: string | undefined;
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
/** Every refusal on file for one provider: what its accounts are judged against, each standing until something answers it. */
|
|
215
|
+
export type RefusalsOnFile = readonly ProviderRefusal[];
|
|
216
|
+
|
|
212
217
|
// A spent allowance is over once a reading with room lands after it; a refused credential once any reading does (the
|
|
213
218
|
// token worked). Nothing a reading says answers an entitlement refusal: a seatless account authenticates and reads fine.
|
|
214
219
|
// Nor does the refused account's own reading answer an allowance it does not measure (`unmetered`: an admin's credit).
|
|
@@ -227,10 +232,39 @@ const answers = (refusal: ProviderRefusal, facts: ServiceFacts, measured: boolea
|
|
|
227
232
|
};
|
|
228
233
|
|
|
229
234
|
/**
|
|
230
|
-
*
|
|
231
|
-
* the
|
|
232
|
-
*
|
|
233
|
-
*
|
|
235
|
+
* When the pool a spent allowance that named no instant was refused in reopens, in epoch seconds: the soonest reset of
|
|
236
|
+
* the binding pool for the model it named (the pool judgedReading pins full) on the accounts it may have refused, its own
|
|
237
|
+
* where it named one, any where it named none, as any account's room answers a nameless one. The daemon's ledger stamps
|
|
238
|
+
* it on the refusal as it files it (`poolResetsAt`), while the readings still show that pool open: once it reopens the
|
|
239
|
+
* reading lapses it to an empty pool and a different one binds, so it cannot be read back off the reading later.
|
|
240
|
+
* Undefined for a refusal that named its instant, one no reading measures (`unmetered`), a person's mark (lifted by a
|
|
241
|
+
* person or a turn, not a window), and where no reading publishes a reset still ahead of the refusal.
|
|
242
|
+
*/
|
|
243
|
+
export const refusedPoolReset = (refusal: ProviderRefusal, accounts: readonly ServiceFacts[]): number | undefined => {
|
|
244
|
+
if (refusal.kind !== "limit" || refusal.resetsAt !== undefined || refusal.unmetered === true || refusal.byHand === true) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const model = refusal.model === undefined ? undefined : { id: refusal.model };
|
|
248
|
+
const resets = accounts
|
|
249
|
+
.filter((facts) => refusal.account === undefined || facts.account === refusal.account)
|
|
250
|
+
.flatMap((facts) => {
|
|
251
|
+
const reset = bindingWindow(facts.usage, model)?.resetsAt;
|
|
252
|
+
return reset !== undefined && reset * 1000 > refusal.at ? [reset] : [];
|
|
253
|
+
});
|
|
254
|
+
return resets.length === 0 ? undefined : Math.min(...resets);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* How a refusal on file stands against everything read since, over every account the provider holds: `gone` once the
|
|
259
|
+
* account it named is disconnected, `answered` once the instant it named has passed (or, where it named none, the
|
|
260
|
+
* reset of the pool it was refused in, stamped as it was filed: refusedPoolReset) or once that account (or, for a refusal
|
|
261
|
+
* naming none, any account) has a later reading that contradicts it, else `standing`. An empty list is one not loaded
|
|
262
|
+
* yet, so the refusal stands. Judged by the daemon and every open window at the same instant from what the refusal
|
|
263
|
+
* carries: nothing waits for a fresh reading or a ledger write to flip it. 2026-10-20: a refusal naming no instant stood
|
|
264
|
+
* past its pool's reset until the next reading landed, up to the 15-minute idle sweep, while the picker already said
|
|
265
|
+
* "back at" that reset. A stamp is this sandbox's guess, not the provider's word, so a reading taken after it outranks
|
|
266
|
+
* it: 2026-10-21, a weekly pool read at a rounded 100 once the five-hour one it was stamped with reopened, and the
|
|
267
|
+
* account read ready with no room, so every turn let through was refused again.
|
|
234
268
|
*/
|
|
235
269
|
export const refusalVerdict = (
|
|
236
270
|
refusal: ProviderRefusal,
|
|
@@ -241,8 +275,12 @@ export const refusalVerdict = (
|
|
|
241
275
|
if (refusal.account !== undefined && named.length === 0) {
|
|
242
276
|
return accounts.length === 0 ? "standing" : "gone";
|
|
243
277
|
}
|
|
244
|
-
|
|
245
|
-
|
|
278
|
+
const whose = refusal.account === undefined ? accounts : named;
|
|
279
|
+
// The provider's own "try again at" is over at that instant, whichever account it refused; so, where it named none,
|
|
280
|
+
// is the refused pool's reset the ledger stamped, unless a reading since says otherwise (read below).
|
|
281
|
+
const over = refusal.resetsAt ?? refusal.poolResetsAt;
|
|
282
|
+
const readSince = (instant: number): boolean => whose.some((facts) => (facts.usage?.measuredAt ?? 0) > instant * 1000);
|
|
283
|
+
if (refusal.kind === "limit" && over !== undefined && over * 1000 <= now && (refusal.resetsAt !== undefined || !readSince(over))) {
|
|
246
284
|
return "answered";
|
|
247
285
|
}
|
|
248
286
|
// Where it is known which account it refused (it named one, or there is only one), the provider's own instant
|
|
@@ -252,7 +290,7 @@ export const refusalVerdict = (
|
|
|
252
290
|
return "standing";
|
|
253
291
|
}
|
|
254
292
|
if (known) {
|
|
255
|
-
return
|
|
293
|
+
return whose.some((facts) => answers(refusal, facts)) ? "answered" : "standing";
|
|
256
294
|
}
|
|
257
295
|
/* A nameless refusal over several accounts is one of them saying no, and nothing says which: the translator benches
|
|
258
296
|
the one it refused and routes on, so any of them reading room since answers it, whatever kind of allowance it was
|
|
@@ -261,6 +299,22 @@ export const refusalVerdict = (
|
|
|
261
299
|
return accounts.some((facts) => answers(refusal, facts, true)) ? "answered" : "standing";
|
|
262
300
|
};
|
|
263
301
|
|
|
302
|
+
// A stamp a reading has outlived (refusalVerdict) says nothing of when the pool now holding the refusal reopens: that
|
|
303
|
+
// pool's own reset does.
|
|
304
|
+
const unstampedPast = (refusal: ProviderRefusal, now: number): ProviderRefusal => {
|
|
305
|
+
if (refusal.poolResetsAt === undefined || refusal.poolResetsAt * 1000 > now) {
|
|
306
|
+
return refusal;
|
|
307
|
+
}
|
|
308
|
+
const { poolResetsAt: _outlived, ...standing } = refusal;
|
|
309
|
+
return standing;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/** The refusals still standing over each of a provider's accounts: those naming it, and those naming none. */
|
|
313
|
+
export const standingOver = (accounts: readonly ServiceFacts[], refusals: RefusalsOnFile, now: number = Date.now()): ((account: string) => RefusalsOnFile) => {
|
|
314
|
+
const standing = refusals.filter((refusal) => refusalVerdict(refusal, accounts, now) === "standing").map((refusal) => unstampedPast(refusal, now));
|
|
315
|
+
return (account) => standing.filter((refusal) => (refusal.account ?? account) === account);
|
|
316
|
+
};
|
|
317
|
+
|
|
264
318
|
// The account reopens for this question when the pools holding it shut do: all of them for a model's own question (or
|
|
265
319
|
// the pools gating every model), the first to reopen when only per-model slices are full. Unknown if any is unpublished.
|
|
266
320
|
const reopening = (full: readonly UsageWindow[], first: boolean): number | undefined => {
|
|
@@ -268,7 +322,20 @@ const reopening = (full: readonly UsageWindow[], first: boolean): number | undef
|
|
|
268
322
|
return resets.length === full.length && resets.length > 0 ? (first ? Math.min(...resets) : Math.max(...resets)) : undefined;
|
|
269
323
|
};
|
|
270
324
|
|
|
271
|
-
|
|
325
|
+
// Spent until the pools holding it reopen, named by the one that decides when, where the plan meters it apart (an
|
|
326
|
+
// undivided allowance is just "the allowance").
|
|
327
|
+
const spentOn = (holding: readonly UsageWindow[], first: boolean): AccountState => {
|
|
328
|
+
const state: AccountState & { kind: "spent" } = { kind: "spent" };
|
|
329
|
+
const reopensAt = reopening(holding, first);
|
|
330
|
+
if (reopensAt !== undefined) {
|
|
331
|
+
state.reopensAt = reopensAt;
|
|
332
|
+
}
|
|
333
|
+
const deciding = holding.find((window) => window.resetsAt === reopensAt) ?? holding[0];
|
|
334
|
+
if (deciding !== undefined && deciding.gates !== "all" && deciding.label !== undefined) {
|
|
335
|
+
state.pool = deciding.label;
|
|
336
|
+
}
|
|
337
|
+
return state;
|
|
338
|
+
};
|
|
272
339
|
|
|
273
340
|
// A turn on the model this reading was contradicted for went through while the pool read full: the pool is at the line,
|
|
274
341
|
// not proven empty, exactly as a rounded one is (provenFull).
|
|
@@ -289,17 +356,15 @@ const holdingPools = (full: readonly UsageWindow[], open: readonly UsageWindow[]
|
|
|
289
356
|
};
|
|
290
357
|
|
|
291
358
|
/**
|
|
292
|
-
* What the
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* named `headroomState` until then.
|
|
359
|
+
* What a reading says, at the contract's one spent line, never whether the account can serve: that is `serviceStates`,
|
|
360
|
+
* which first lays every standing refusal on the reading (judgedReading). A provider that rounds its percentages (Claude
|
|
361
|
+
* does, every pool) is never proven spent here, so a caller reading a polled reading's verdict as the account's sees every
|
|
362
|
+
* spent account as open: a pick between two spent accounts re-booked a held turn for "now" that way (2026-10-19).
|
|
363
|
+
* Imported only by the few modules lint allows (.oxlintrc.json, no-restricted-imports).
|
|
298
364
|
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* models their room, which is then read off the fullest pool still open.
|
|
365
|
+
* With a model, the pools that model spends: spent once any is full, else the room the fullest leaves. With none, whether
|
|
366
|
+
* the account can serve SOME turn: spent once a pool gating every model is full, or every per-model slice is; a full
|
|
367
|
+
* slice alone (Opus at 100%) leaves the other models their room, which is then read off the fullest pool still open.
|
|
303
368
|
*/
|
|
304
369
|
export const readingHeadroom = (usage: AccountUsage | undefined, model?: ModelRef): AccountState => {
|
|
305
370
|
const windows = gatingWindows(usage, model).map((window) => (answeredDespite(usage, window) ? { ...window, rounded: true } : window));
|
|
@@ -319,101 +384,168 @@ export const readingHeadroom = (usage: AccountUsage | undefined, model?: ModelRe
|
|
|
319
384
|
if (sliced && (usage?.uncovered?.length ?? 0) > 0) {
|
|
320
385
|
return { kind: "unknown" };
|
|
321
386
|
}
|
|
322
|
-
return
|
|
387
|
+
return spentOn(holding, sliced);
|
|
323
388
|
};
|
|
324
389
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
390
|
+
// What a person calls a refused pool no reading names: the model it refused, or the allowance as a whole.
|
|
391
|
+
const REFUSED_ALLOWANCE = "Allowance";
|
|
392
|
+
|
|
393
|
+
// The pool a limit refusal proves full where no pool of the reading gates the model it refused: a plan that publishes
|
|
394
|
+
// nothing (Cursor's, Devin's) or names no pool for that model. Its own, gating only that model (every model, where it
|
|
395
|
+
// named none), reopening when the provider said.
|
|
396
|
+
const refusedPool = (refusal: ProviderRefusal): UsageWindow => {
|
|
397
|
+
const pool: UsageWindow = {
|
|
398
|
+
kind: `refused:${refusal.model ?? "all"}`,
|
|
399
|
+
label: refusal.model === undefined ? REFUSED_ALLOWANCE : humanizeModelId(refusal.model),
|
|
400
|
+
utilization: SPENT_UTILIZATION,
|
|
401
|
+
gates: refusal.model === undefined ? "all" : { models: [refusal.model] },
|
|
402
|
+
};
|
|
403
|
+
if (refusal.resetsAt !== undefined) {
|
|
404
|
+
pool.resetsAt = refusal.resetsAt;
|
|
405
|
+
}
|
|
406
|
+
return pool;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// One limit refusal laid on a reading: the pool the refused turn's model spends (else the one asked about, else the
|
|
410
|
+
// tightest) read full, since a polled reading freezes the moment a pool actually empties, and reopening when the provider
|
|
411
|
+
// said to try again, where it said, over whatever the polled pool last said. Never another model's allowance.
|
|
412
|
+
const pinRefusal = (usage: AccountUsage | undefined, refusal: ProviderRefusal, model: ModelRef | undefined): AccountUsage => {
|
|
332
413
|
const binding = bindingWindow(usage, refusal.model === undefined ? model : { id: refusal.model });
|
|
333
414
|
if (usage === undefined || binding === undefined) {
|
|
334
|
-
return usage;
|
|
415
|
+
return { ...(usage ?? { measuredAt: refusal.at }), windows: [...(usage?.windows ?? []), refusedPool(refusal)] };
|
|
416
|
+
}
|
|
417
|
+
const pinned = pinnedFull(binding);
|
|
418
|
+
// Reopening when the refusal ends (refusalVerdict), so the "back at" every surface draws is when it flips.
|
|
419
|
+
const over = refusal.resetsAt ?? refusal.poolResetsAt;
|
|
420
|
+
if (over !== undefined) {
|
|
421
|
+
pinned.resetsAt = over;
|
|
335
422
|
}
|
|
336
|
-
|
|
337
|
-
refusal.resetsAt === undefined ? pinnedFull(window) : { ...pinnedFull(window), resetsAt: refusal.resetsAt };
|
|
338
|
-
return { ...usage, windows: usage.windows.map((window) => (window === binding ? pinned(window) : window)) };
|
|
423
|
+
return { ...usage, windows: usage.windows.map((window) => (window === binding ? pinned : window)) };
|
|
339
424
|
};
|
|
340
425
|
|
|
341
426
|
/**
|
|
342
|
-
*
|
|
343
|
-
* the
|
|
344
|
-
*
|
|
345
|
-
* model picker, both saying 15:53 (2026-10-20).
|
|
427
|
+
* An account's reading as the refusals standing over it leave it: what every surface draws and every verdict judges,
|
|
428
|
+
* never the polled reading alone. Each standing limit refusal is laid on it, oldest first so the newest says when its
|
|
429
|
+
* pool reopens. `standing` is what standingOver answers for the account; a refusal of another kind pins nothing.
|
|
346
430
|
*/
|
|
347
|
-
export const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
};
|
|
431
|
+
export const judgedReading = (usage: AccountUsage | undefined, standing: RefusalsOnFile, model?: ModelRef): AccountUsage | undefined =>
|
|
432
|
+
standing
|
|
433
|
+
.filter((refusal) => refusal.kind === "limit")
|
|
434
|
+
.toSorted((left, right) => left.at - right.at)
|
|
435
|
+
.reduce<AccountUsage | undefined>((reading, refusal) => pinRefusal(reading, refusal, model), usage);
|
|
436
|
+
|
|
437
|
+
/** The newest of some refusals: the latest word, where a surface says one line about a provider or an account. */
|
|
438
|
+
export const newestRefusal = (refusals: RefusalsOnFile): ProviderRefusal | undefined =>
|
|
439
|
+
refusals.reduce<ProviderRefusal | undefined>((newest, refusal) => (newest === undefined || refusal.at > newest.at ? refusal : newest), undefined);
|
|
357
440
|
|
|
358
441
|
/**
|
|
359
|
-
* One account's verdict. `
|
|
360
|
-
*
|
|
361
|
-
*
|
|
442
|
+
* One account's verdict on everything on file over it. `standing` is the refusals still standing over it (standingOver).
|
|
443
|
+
* Precedence: a revoked sign-in outranks a lost seat (reconnecting fixes one, nothing here fixes the other), both outrank
|
|
444
|
+
* the newest standing refusal where it refused the account itself, then a translator bench, then the plan limits on the
|
|
445
|
+
* reading the refusals leave (judgedReading).
|
|
362
446
|
*/
|
|
363
|
-
export const serviceState = (facts: ServiceFacts,
|
|
447
|
+
export const serviceState = (facts: ServiceFacts, standing: RefusalsOnFile = [], model?: ModelRef, now: number = Date.now()): AccountState => {
|
|
364
448
|
if (facts.needsReauth === true) {
|
|
365
449
|
return { kind: "blocked", fix: "reconnect", reason: facts.detail ?? "sign-in expired" };
|
|
366
450
|
}
|
|
367
451
|
if (facts.seatRefusal !== undefined) {
|
|
368
452
|
return { kind: "blocked", fix: "admin", reason: facts.seatRefusal };
|
|
369
453
|
}
|
|
370
|
-
|
|
371
|
-
|
|
454
|
+
// The newest word on the account: a sign-in or an entitlement refused after any spent allowance blocks it, and a
|
|
455
|
+
// spent allowance refused after either says the account authenticated and was served, so that one is over.
|
|
456
|
+
const newest = newestRefusal(standing);
|
|
457
|
+
if (newest?.kind === "auth") {
|
|
458
|
+
return { kind: "blocked", fix: "reconnect", reason: newest.message };
|
|
372
459
|
}
|
|
373
|
-
if (
|
|
374
|
-
return { kind: "blocked", fix: "admin", reason:
|
|
460
|
+
if (newest?.kind === "entitlement") {
|
|
461
|
+
return { kind: "blocked", fix: "admin", reason: newest.message };
|
|
375
462
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
463
|
+
const plan = readingHeadroom(judgedReading(facts.usage, standing, model), model);
|
|
464
|
+
return facts.cooling === undefined ? plan : benched(facts.cooling, plan, now);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// A translator bench over the plan verdict. One the provider wants a person for: the account's owner confirms it on the
|
|
468
|
+
// provider's page, and no wait or reconnect here lifts it, whatever the retry instant says. One with no instant is one no
|
|
469
|
+
// wait lifts either (a Google account with no project): somebody has to connect again. One with an instant is the
|
|
470
|
+
// translator's own retry, a floor rather than the reopening: a Kimi credential benched for thirty minutes with its
|
|
471
|
+
// five-hour window two hours from reopening was re-run at thirty minutes and refused again (2026-10-17), so it lifts once
|
|
472
|
+
// its pools have reopened too. Past its instant it is over, and the plan speaks.
|
|
473
|
+
const benched = (cooling: NonNullable<ServiceFacts["cooling"]>, plan: AccountState, now: number): AccountState => {
|
|
474
|
+
if (cooling.verify !== undefined) {
|
|
380
475
|
return { kind: "blocked", fix: "verify", reason: cooling.reason ?? "the provider wants this account verified", url: cooling.verify };
|
|
381
476
|
}
|
|
382
|
-
|
|
383
|
-
if (cooling !== undefined && cooling.until === undefined) {
|
|
477
|
+
if (cooling.until === undefined) {
|
|
384
478
|
return { kind: "blocked", fix: "reconnect", reason: cooling.reason ?? "benched by the translator" };
|
|
385
479
|
}
|
|
386
|
-
if (cooling
|
|
387
|
-
return
|
|
480
|
+
if (cooling.until * 1000 <= now) {
|
|
481
|
+
return plan;
|
|
388
482
|
}
|
|
389
|
-
|
|
483
|
+
const until = plan.kind === "spent" && plan.reopensAt !== undefined ? Math.max(cooling.until, plan.reopensAt) : cooling.until;
|
|
484
|
+
return { kind: "blocked", fix: "wait", reason: cooling.reason ?? "cooling down", until };
|
|
390
485
|
};
|
|
391
486
|
|
|
392
|
-
/**
|
|
393
|
-
export type RefusalsOnFile = ProviderRefusal | readonly ProviderRefusal[] | undefined;
|
|
394
|
-
|
|
395
|
-
const refusalList = (refusals: RefusalsOnFile): readonly ProviderRefusal[] =>
|
|
396
|
-
refusals === undefined ? [] : "kind" in refusals ? [refusals] : refusals;
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Every account of one provider, judged together, since a refusal naming no account is answered by any of them. Each
|
|
400
|
-
* account answers to the newest refusal that still stands over it: the provider's last one moves on to whichever account
|
|
401
|
-
* said no next, and an account's own refusal on file stays true of it until something answers it.
|
|
402
|
-
*/
|
|
487
|
+
/** Every account of one provider, judged together, since a refusal naming no account is answered by any of them. */
|
|
403
488
|
export const serviceStates = (
|
|
404
489
|
accounts: readonly ServiceFacts[],
|
|
405
|
-
refusals: RefusalsOnFile,
|
|
490
|
+
refusals: RefusalsOnFile = [],
|
|
406
491
|
model?: ModelRef,
|
|
407
492
|
now: number = Date.now(),
|
|
408
493
|
): ReadonlyMap<string, AccountState> => {
|
|
409
|
-
const standing =
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
494
|
+
const standing = standingOver(accounts, refusals, now);
|
|
495
|
+
return new Map(accounts.map((facts) => [facts.account, serviceState(facts, standing(facts.account), model, now)]));
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/** When an account that cannot serve says it can again, in epoch seconds; undefined where its verdict names no instant. */
|
|
499
|
+
export const reopensAtOf = (state: AccountState): number | undefined => {
|
|
500
|
+
if (state.kind === "spent") {
|
|
501
|
+
return state.reopensAt;
|
|
502
|
+
}
|
|
503
|
+
return state.kind === "blocked" && state.fix === "wait" ? state.until : undefined;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* What one provider's accounts can do for one model, together: the answer every gate, listing, picker and offer reads,
|
|
508
|
+
* so none can call a model spent that another lets through. An unmeasured account may still answer, so it keeps the
|
|
509
|
+
* model servable; only every account spent or blocked makes it spent.
|
|
510
|
+
*/
|
|
511
|
+
export type FleetCapacity =
|
|
512
|
+
// No account to judge: nothing on file says either way, so the model is asked.
|
|
513
|
+
| { readonly kind: "unknown" }
|
|
514
|
+
// `serving` of the `accounts` are ready or unmeasured; `roomMeasuredAt` is the newest reading with room among them.
|
|
515
|
+
| { readonly kind: "serving"; readonly serving: number; readonly accounts: number; readonly roomMeasuredAt?: number | undefined }
|
|
516
|
+
// Every account is spent or blocked: `reopensAt` is the soonest any is back (any one unblocks the turn), where one
|
|
517
|
+
// says when, and `pool` what holds that one.
|
|
518
|
+
| { readonly kind: "spent"; readonly accounts: number; readonly reopensAt?: number | undefined; readonly pool?: string | undefined };
|
|
519
|
+
|
|
520
|
+
/** A provider's capacity for one model from its accounts as already judged on that model (serviceStates). */
|
|
521
|
+
export const fleetOf = (accounts: readonly ServiceFacts[], states: ReadonlyMap<string, AccountState>): FleetCapacity => {
|
|
522
|
+
if (accounts.length === 0) {
|
|
523
|
+
return { kind: "unknown" };
|
|
524
|
+
}
|
|
525
|
+
const judged = accounts.map((facts) => ({ facts, state: states.get(facts.account) ?? ({ kind: "unknown" } as const) }));
|
|
526
|
+
const serving = judged.filter(({ state }) => state.kind === "ready" || state.kind === "unknown");
|
|
527
|
+
if (serving.length > 0) {
|
|
528
|
+
const room = serving.flatMap(({ facts, state }) => (state.kind === "ready" && facts.usage !== undefined ? [facts.usage.measuredAt] : []));
|
|
529
|
+
return { kind: "serving", serving: serving.length, accounts: accounts.length, roomMeasuredAt: room.length === 0 ? undefined : Math.max(...room) };
|
|
530
|
+
}
|
|
531
|
+
// The soonest instant any account names; one with none only decides the pool where nothing names an instant.
|
|
532
|
+
const soonest = judged.reduce<AccountState | undefined>((best, { state }) => {
|
|
533
|
+
const at = reopensAtOf(state);
|
|
534
|
+
const bestAt = best === undefined ? undefined : reopensAtOf(best);
|
|
535
|
+
return best === undefined || (at !== undefined && (bestAt === undefined || at < bestAt)) ? state : best;
|
|
536
|
+
}, undefined);
|
|
537
|
+
return {
|
|
538
|
+
kind: "spent",
|
|
539
|
+
accounts: accounts.length,
|
|
540
|
+
reopensAt: soonest === undefined ? undefined : reopensAtOf(soonest),
|
|
541
|
+
pool: soonest?.kind === "spent" ? soonest.pool : undefined,
|
|
542
|
+
};
|
|
415
543
|
};
|
|
416
544
|
|
|
545
|
+
/** A provider's capacity for one model, judged on its accounts and every refusal on file. */
|
|
546
|
+
export const fleetCapacity = (accounts: readonly ServiceFacts[], refusals: RefusalsOnFile = [], model?: ModelRef, now: number = Date.now()): FleetCapacity =>
|
|
547
|
+
fleetOf(accounts, serviceStates(accounts, refusals, model, now));
|
|
548
|
+
|
|
417
549
|
// Worst last: proven room, no reading either way, a pool at the line (it may hold a sliver, or nothing), known spent,
|
|
418
550
|
// and nothing a turn can run on.
|
|
419
551
|
const preference = (state: AccountState): number => {
|
|
@@ -441,8 +573,8 @@ export const preferredAccount = <T extends { readonly state: AccountState }>(ent
|
|
|
441
573
|
|
|
442
574
|
/**
|
|
443
575
|
* The roomiest account with proven room, or none: what a move off a refused account may land on. Ties keep the caller's
|
|
444
|
-
* order. An account at the line is left out: a move there could be refused in turn, and
|
|
445
|
-
*
|
|
576
|
+
* order. An account at the line is left out: a move there could be refused in turn, and two such accounts would hand the
|
|
577
|
+
* turn back and forth.
|
|
446
578
|
*/
|
|
447
579
|
export const roomiestAccount = <T extends { readonly state: AccountState }>(entries: readonly T[]): T | undefined =>
|
|
448
580
|
preferredAccount(entries.filter((entry) => entry.state.kind === "ready" && entry.state.room > 0));
|
|
@@ -24,6 +24,9 @@ export const RunnerHelloSchema = z.object({
|
|
|
24
24
|
// tag names its own container (`intentic-sandbox-env-<slug>:<hash>`), so two sides on one base and one overlay
|
|
25
25
|
// never share it. Absent on an old image, which is then compared by `image` alone.
|
|
26
26
|
baseImage: z.string().optional(),
|
|
27
|
+
// That base's registry digest (SANDBOX_BASE_DIGEST), when the runner's machine could read one; parity then compares
|
|
28
|
+
// the exact image rather than a tag two machines may have pulled at different moments (state/sandbox-build.ts).
|
|
29
|
+
baseDigest: z.string().optional(),
|
|
27
30
|
channel: z.string().optional(),
|
|
28
31
|
overlayHash: z.string().optional(),
|
|
29
32
|
// Runner's declared settings-only sandbox.toml (definition.ts), diffed against the parent's to itemize drift;
|
|
@@ -147,7 +150,7 @@ export const RunnerSummarySchema = z.object({
|
|
|
147
150
|
overlayHash: z.string().optional(),
|
|
148
151
|
facts: RunnerFactsSchema.optional(),
|
|
149
152
|
lastSeen: z.number().optional(),
|
|
150
|
-
// Computed once by the daemon (
|
|
153
|
+
// Computed once by the daemon (buildParity, state/sandbox-build.ts), so every surface shows the same badge.
|
|
151
154
|
parity: RunnerParitySchema,
|
|
152
155
|
// Where the runner's environment differs from this sandbox's, one line per difference; absent if never reported,
|
|
153
156
|
// empty if they agree. A Setting line is fixable live; an overlay line needs a rebuild.
|
|
@@ -146,6 +146,7 @@ export const RAW_ROUTES = {
|
|
|
146
146
|
"POST /children/answer": { agent: true, control: "never" },
|
|
147
147
|
"POST /children/cancel": { agent: true, control: "never" },
|
|
148
148
|
"POST /children/merge": { agent: true, control: "never" },
|
|
149
|
+
"POST /children/settle": { agent: true, control: "never" },
|
|
149
150
|
// Handing a task to another of the owner's sandboxes over a link card (`agents delegate`), and which links this
|
|
150
151
|
// conversation's turn may hand work to (`agents links`); held to the cards the turn was granted, like the tool.
|
|
151
152
|
"POST /children/delegate": { agent: true, control: "never" },
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// WHAT MOVES A CONVERSATION NEXT WITH NOBODY PRESSING ANYTHING: the one answer every surface reads (AgentSummary.next).
|
|
4
|
+
// Five mechanisms can carry a conversation on by themselves: a spent allowance's held turn re-run at its reopen or moved
|
|
5
|
+
// to another account, a message held for memory sent once memory frees, and messages a person booked for a time or for
|
|
6
|
+
// after another conversation lands. Each used to publish its own raw state on the card (a booking flag copied onto the
|
|
7
|
+
// stored ending and re-synced every beat, the queue's own shadow, a failure code plus an answer the editor looked up in
|
|
8
|
+
// Settings), and each surface rebuilt "does it go by itself" from its own subset: the lane from two of them, the card's
|
|
9
|
+
// corner from three, the chat's bar from one more. They disagreed (a memory hold "sends when room" in the corner and
|
|
10
|
+
// waited in Attention; a booked resend read as Attention after a restart; a booked message cancelled with an archive
|
|
11
|
+
// that never counted it). The daemon now derives this one value from what the conversation holds, every time the card
|
|
12
|
+
// is read (conversation-next.ts), and nothing else on the card says whether or when it goes by itself.
|
|
13
|
+
export const AgentNextSchema = z.discriminatedUnion("kind", [
|
|
14
|
+
z.object({
|
|
15
|
+
kind: z.literal("rerun"),
|
|
16
|
+
at: z.number().describe("When the held turn goes again, in milliseconds: its allowance's reopen, or the next pass for a move. Already past means any moment now."),
|
|
17
|
+
onto: z.string().optional().describe("The account the owner's answer to the limit is moving it to. Absent when it goes again where it was refused."),
|
|
18
|
+
}),
|
|
19
|
+
z.object({ kind: z.literal("room") }).describe("A message, or a turn the sandbox kept, held because memory ran short: it goes once memory has stayed free a while."),
|
|
20
|
+
z.object({
|
|
21
|
+
kind: z.literal("send"),
|
|
22
|
+
at: z.number().describe("When the soonest booked message goes, in milliseconds."),
|
|
23
|
+
count: z.number().int().min(1).describe("How many messages are booked to go by themselves."),
|
|
24
|
+
}),
|
|
25
|
+
z.object({
|
|
26
|
+
kind: z.literal("land"),
|
|
27
|
+
after: z.string().describe("The conversation whose landed work the first booked message waits for."),
|
|
28
|
+
count: z.number().int().min(1).describe("How many messages are booked to go by themselves."),
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
export type AgentNext = z.infer<typeof AgentNextSchema>;
|
|
32
|
+
|
|
33
|
+
/** Whether a conversation carries on by itself, with nobody pressing anything: the one reading of `next`. */
|
|
34
|
+
export const goesBySelf = (agent: { readonly next?: AgentNext | undefined }): boolean => agent.next !== undefined;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// The board's search over what conversations said: the query, and what came back with its evidence.
|
|
4
|
+
|
|
5
|
+
// Searches only speech, both sides of the conversation, never thinking, tool output or protocol text, which would
|
|
6
|
+
// otherwise match nearly the whole board. Two-char minimum; `caseSensitive` off means case-insensitive, matching
|
|
7
|
+
// workspace search's own switch.
|
|
8
|
+
export const AgentSearchQuerySchema = z.object({
|
|
9
|
+
query: z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.min(2)
|
|
13
|
+
.describe(
|
|
14
|
+
"What to look for. Searched against what was said, both sides of the conversation, and nothing else: not the thinking, not the tool output, which between them name nearly every identifier in the workspace and would return most of the board.",
|
|
15
|
+
),
|
|
16
|
+
caseSensitive: z.stringbool().optional().describe("Whether capitals matter."),
|
|
17
|
+
});
|
|
18
|
+
// Why a row survived the filter: the matched line, windowed, and who said it. `speaker` rides with the text since agent
|
|
19
|
+
// prose alone can read as something the user typed.
|
|
20
|
+
export const SpeakerSchema = z.enum(["user", "agent"]);
|
|
21
|
+
export type Speaker = z.infer<typeof SpeakerSchema>;
|
|
22
|
+
export const MatchSnippetSchema = z.object({
|
|
23
|
+
text: z.string().describe("The matching line, with a little either side of it."),
|
|
24
|
+
speaker: SpeakerSchema.describe(
|
|
25
|
+
"Who said it. Carried with the words rather than beside them, because a line of the agent's prose under a card reads as something you typed until the row says otherwise.",
|
|
26
|
+
),
|
|
27
|
+
});
|
|
28
|
+
export type MatchSnippet = z.infer<typeof MatchSnippetSchema>;
|
|
29
|
+
// One matching agent and its evidence; `snippet` is absent for a title match, which the card already shows.
|
|
30
|
+
export const AgentMatchSchema = z.object({
|
|
31
|
+
id: z.string().describe("Which conversation matched."),
|
|
32
|
+
snippet: MatchSnippetSchema.optional().describe(
|
|
33
|
+
"Why, in its own words. Absent when the title was the match, which the card already shows: repeating it underneath is noise where evidence was wanted.",
|
|
34
|
+
),
|
|
35
|
+
});
|
|
36
|
+
export type AgentMatch = z.infer<typeof AgentMatchSchema>;
|
|
37
|
+
// How many agents were actually read, so the board can say a search saw less than the whole fleet.
|
|
38
|
+
export const AgentSearchResultSchema = z.object({
|
|
39
|
+
matches: z.array(AgentMatchSchema).describe("What matched, from the live fleet and the archive together."),
|
|
40
|
+
scanned: z
|
|
41
|
+
.number()
|
|
42
|
+
.describe(
|
|
43
|
+
"How many conversations were actually read, so a screen can say when a search saw less than everything rather than implying it saw all of it.",
|
|
44
|
+
),
|
|
45
|
+
indexing: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.describe(
|
|
48
|
+
"Whether what was said is still being read in the background. True means this answer can still grow, so a screen must say it is incomplete rather than presenting it as the whole list.",
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
export type AgentSearchResult = z.infer<typeof AgentSearchResultSchema>;
|
package/src/schemas/agent.ts
CHANGED
|
@@ -80,6 +80,16 @@ export * from "./conversation-links.js";
|
|
|
80
80
|
// batch rather than build a turn past this (turn-admission.ts, together): a turn the daemon makes must parse back.
|
|
81
81
|
export const ATTACHMENT_LIMIT = 20;
|
|
82
82
|
|
|
83
|
+
// Where a fork is cut from, as the client that cut it names it (its first turn, or a draft no turn has started yet).
|
|
84
|
+
export const ForkOfSchema = z.object({
|
|
85
|
+
conversationId: ConversationIdSchema.describe("The conversation this one was cut from."),
|
|
86
|
+
keep: z.number().int().nonnegative().describe("How many of that conversation's messages to copy in before this turn runs."),
|
|
87
|
+
files: z
|
|
88
|
+
.enum(["then", "now"])
|
|
89
|
+
.describe(
|
|
90
|
+
'Which files the fork opens on: "now" is the workspace as it stands, "then" is the files as they were at the cut, which needs a private copy.',
|
|
91
|
+
),
|
|
92
|
+
});
|
|
83
93
|
const AgentTurnFieldsSchema = z.object({
|
|
84
94
|
prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
|
|
85
95
|
errand: TurnErrandSchema.optional().describe(
|
|
@@ -189,18 +199,7 @@ const AgentTurnFieldsSchema = z.object({
|
|
|
189
199
|
// the new session from its own conversationId-keyed record.
|
|
190
200
|
// Where a fork was cut from, on its first turn only; requires conversationId, and files:"then" implies
|
|
191
201
|
// isolated.
|
|
192
|
-
forkOf:
|
|
193
|
-
.object({
|
|
194
|
-
conversationId: ConversationIdSchema.describe("The conversation this one was cut from."),
|
|
195
|
-
keep: z.number().int().nonnegative().describe("How many of that conversation's messages to copy in before this turn runs."),
|
|
196
|
-
files: z
|
|
197
|
-
.enum(["then", "now"])
|
|
198
|
-
.describe(
|
|
199
|
-
'Which files the fork opens on: "now" is the workspace as it stands, "then" is the files as they were at the cut, which needs a private copy.',
|
|
200
|
-
),
|
|
201
|
-
})
|
|
202
|
-
.optional()
|
|
203
|
-
.describe("Where this conversation was cut from, on its first turn only. Only the client knows this, so only the client can say it."),
|
|
202
|
+
forkOf: ForkOfSchema.optional().describe("Where this conversation was cut from, on its first turn only. Only the client knows this, so only the client can say it."),
|
|
204
203
|
// A fresh session's origin, on its first turn only; requires an isolated conversationId the registry has never seen.
|
|
205
204
|
freshOf: FreshOfSchema.optional().describe(FRESH_OF_DESCRIPTION),
|
|
206
205
|
// The browser sends the chosen model per turn; the provider token is the sandbox's own stored credential.
|
|
@@ -323,6 +322,16 @@ export type AgentTurn = z.infer<typeof AgentTurnSchema>;
|
|
|
323
322
|
// What `agent.run` takes: a turn, plus the one field only a request carries and no stored turn keeps. A daemon from before
|
|
324
323
|
// `continues` reads past it and takes `prompt` as the words, which is why a client still sends some.
|
|
325
324
|
export const AgentRunSchema = AgentTurnSchema.safeExtend({
|
|
325
|
+
// Request-only like `continues`: the draft goes in the same step that takes the message, so it is cleared exactly
|
|
326
|
+
// once, by the send itself, and a window that closes before its own clear arrives leaves no ghost of sent words.
|
|
327
|
+
draft: z
|
|
328
|
+
.number()
|
|
329
|
+
.int()
|
|
330
|
+
.positive()
|
|
331
|
+
.optional()
|
|
332
|
+
.describe(
|
|
333
|
+
"The revision of your composer draft for this conversation these words were sent from (`agents.drafts`). The sandbox clears that draft as it takes the message, unless it has been written again since. Leave it out to leave your draft as it is.",
|
|
334
|
+
),
|
|
326
335
|
continues: z
|
|
327
336
|
.literal(true)
|
|
328
337
|
.optional()
|