@intentic/sandbox-contract 1.166.0 → 1.168.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.
Files changed (67) hide show
  1. package/dist/agent-catalog.d.ts +20 -3
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/agent-catalog.js +81 -6
  4. package/dist/agent-catalog.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +36 -8
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/agent.contract.js +1 -2
  8. package/dist/contracts/agent.contract.js.map +1 -1
  9. package/dist/contracts/agents.contract.d.ts +55 -39
  10. package/dist/contracts/agents.contract.d.ts.map +1 -1
  11. package/dist/contracts/agents.contract.js.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts +1 -1
  13. package/dist/contracts/extensions.contract.d.ts +4 -0
  14. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  15. package/dist/contracts/gate.contract.d.ts +1 -1
  16. package/dist/contracts/kimi.contract.d.ts +0 -70
  17. package/dist/contracts/kimi.contract.d.ts.map +1 -1
  18. package/dist/contracts/kimi.contract.js +1 -5
  19. package/dist/contracts/kimi.contract.js.map +1 -1
  20. package/dist/contracts/sessions.contract.d.ts +1 -39
  21. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  22. package/dist/contracts/settings.contract.d.ts +29 -5
  23. package/dist/contracts/settings.contract.d.ts.map +1 -1
  24. package/dist/contracts/system.contract.d.ts +78 -4
  25. package/dist/contracts/system.contract.d.ts.map +1 -1
  26. package/dist/contracts/system.contract.js +9 -2
  27. package/dist/contracts/system.contract.js.map +1 -1
  28. package/dist/contracts/translator.contract.d.ts +47 -0
  29. package/dist/contracts/translator.contract.d.ts.map +1 -1
  30. package/dist/contracts/translator.contract.js.map +1 -1
  31. package/dist/events.d.ts +92 -159
  32. package/dist/events.d.ts.map +1 -1
  33. package/dist/events.js +37 -4
  34. package/dist/events.js.map +1 -1
  35. package/dist/index.d.ts +267 -181
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/schemas.d.ts +269 -44
  40. package/dist/schemas.d.ts.map +1 -1
  41. package/dist/schemas.js +92 -25
  42. package/dist/schemas.js.map +1 -1
  43. package/dist/title.d.ts.map +1 -1
  44. package/dist/title.js.map +1 -1
  45. package/dist/workspace-state.d.ts +9 -0
  46. package/dist/workspace-state.d.ts.map +1 -0
  47. package/dist/workspace-state.js +71 -0
  48. package/dist/workspace-state.js.map +1 -0
  49. package/package.json +2 -2
  50. package/src/agent-catalog.test.ts +118 -0
  51. package/src/agent-catalog.ts +183 -24
  52. package/src/contracts/agent.contract.ts +1 -14
  53. package/src/contracts/agents.contract.ts +8 -6
  54. package/src/contracts/gate.contract.ts +2 -2
  55. package/src/contracts/kimi.contract.ts +4 -21
  56. package/src/contracts/system.contract.ts +24 -1
  57. package/src/contracts/translator.contract.ts +4 -6
  58. package/src/events.test.ts +32 -0
  59. package/src/events.ts +105 -26
  60. package/src/index.ts +1 -0
  61. package/src/model-order.ts +1 -1
  62. package/src/quick-model.test.ts +1 -1
  63. package/src/schemas.test.ts +8 -4
  64. package/src/schemas.ts +296 -111
  65. package/src/title.ts +6 -2
  66. package/src/workspace-state.test.ts +129 -0
  67. package/src/workspace-state.ts +160 -0
package/src/events.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  LandConflictSchema,
7
7
  PermissionModeSchema,
8
8
  RateLimitInfoSchema,
9
+ SubagentKindSchema,
10
+ SubagentStatusSchema,
9
11
  UsageWindowSchema,
10
12
  } from "./schemas.js";
11
13
 
@@ -125,19 +127,36 @@ export type ToolCallContent = z.infer<typeof ToolCallContentSchema>;
125
127
  // tool_use/tool_result blocks, so a restored card carries everything the live `tool_call` frame did except
126
128
  // the streaming-only correlation fields.
127
129
  //
128
- // One restored tool card. Subagent (Task) calls do NOT nest here: the SDK stores a delegation's own calls in a
129
- // separate per-subagent file, so a Task card restores as a leaf and its children stay collapsed the live
130
- // stream still nests them (see ChatTool.children).
131
- export const RestoredToolCallSchema = z.object({
132
- id: z.string(),
133
- name: z.string(),
134
- category: ToolKindSchema,
135
- status: ToolCallStatusSchema,
136
- target: z.string().optional(),
137
- locations: z.array(ToolCallLocationSchema).optional(),
138
- content: z.array(ToolCallContentSchema).optional(),
139
- });
140
- export type RestoredToolCall = z.infer<typeof RestoredToolCallSchema>;
130
+ // One restored tool card. A subagent's own calls and its thinking nest under the Agent card that spawned them,
131
+ // the same two fields (and the same recursion) the live ChatTool carries so a reopened chat redraws the
132
+ // delegation it was showing instead of a leaf card with the whole child collapsed into its result text.
133
+ // z.lazy because the shape refers to itself: a subagent that delegates nests one level deeper.
134
+ export const RestoredToolCallSchema: z.ZodType<RestoredToolCall> = z.lazy(() =>
135
+ z.object({
136
+ id: z.string(),
137
+ name: z.string(),
138
+ category: ToolKindSchema,
139
+ status: ToolCallStatusSchema,
140
+ target: z.string().optional(),
141
+ locations: z.array(ToolCallLocationSchema).optional(),
142
+ content: z.array(ToolCallContentSchema).optional(),
143
+ children: z.array(RestoredToolCallSchema).optional(),
144
+ thinking: z.string().optional(),
145
+ }),
146
+ );
147
+ // Mutable, unlike most of this file: both builders settle a card IN PLACE when its result arrives turns later
148
+ // (restoredTurn's `cards` map, readWorkspaceSession's `awaiting`), which is what saves them a second pass.
149
+ export interface RestoredToolCall {
150
+ id: string;
151
+ name: string;
152
+ category: ToolKind;
153
+ status: ToolCallStatus;
154
+ target?: string | undefined;
155
+ locations?: ToolCallLocation[] | undefined;
156
+ content?: ToolCallContent[] | undefined;
157
+ children?: RestoredToolCall[] | undefined;
158
+ thinking?: string | undefined;
159
+ }
141
160
 
142
161
  // One restored bubble. Each stored assistant message becomes its own, which is what reproduces the live
143
162
  // interleaving — prose, the tool cards that prose introduced, then the next block of prose — rather than
@@ -162,7 +181,8 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({ sessionId:
162
181
  // without a UI mapping is dropped. `plan`/`question`/`permission` pause the turn until the user answers on the
163
182
  // `POST /agent/reply` side channel, and `resolved` releases the one it names; `mode` reports the live
164
183
  // permission posture as the agent changes it.
165
- // `parentToolUseId` tags frames produced inside a subagent (Task tool).
184
+ // `parentToolUseId` tags frames produced inside a subagent (Task tool); `subagent`/`subagent_update` report the
185
+ // subagent itself, keyed by the same tool_use id those tagged frames carry.
166
186
  export const AgentEventSchema = z.discriminatedUnion("kind", [
167
187
  z.object({ kind: z.literal("session"), sessionId: z.string() }),
168
188
  /* First frame of an isolated turn: the conversation's worktree identity — its branch (agent/<id>) and the
@@ -229,6 +249,40 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
229
249
  // `browser-<id>` session, and the client surfaces it in the same panel as the terminals. One per turn, for
230
250
  // the same reason: one browser serves every browser call the turn makes.
231
251
  z.object({ kind: z.literal("browser"), session: z.string() }),
252
+ /* THE AGENT STARTED ANOTHER AGENT — an Agent/Task subagent, or a Codex/Grok CLI it drove from its own Bash
253
+ * (see SubagentSessionSchema). One `subagent` frame per child, then `subagent_update` as it works: the same
254
+ * call/update pair `tool_call`/`tool_call_update` uses, and for the same reason — the fields that move
255
+ * (status, spend, what it is doing) arrive many times and must REPLACE, while the fields that identify it are
256
+ * said once.
257
+ *
258
+ * `id` is the SPAWNING TOOL CALL's id — the same id the client already nests the child's inner frames under
259
+ * (`parentToolUseId`), so both frames land on the card that spawned the child by the lookup that is already
260
+ * there (mapToolAnywhere). No second correlation, and nothing to get wrong.
261
+ *
262
+ * These exist because the SDK's task messages were dropped. A BACKGROUNDED child (the Agent tool's default)
263
+ * emits its tool_use and then nothing until its result lands, which for a long child is minutes of a spinner
264
+ * that cannot say whether anything is happening. */
265
+ z.object({
266
+ kind: z.literal("subagent"),
267
+ id: z.string(),
268
+ subagentKind: SubagentKindSchema,
269
+ agentType: z.string().optional(),
270
+ description: z.string().optional(),
271
+ model: z.string().optional(),
272
+ background: z.boolean().optional(),
273
+ // A delegation's tmux session — the one live view a subagent doesn't have (SubagentSessionSchema).
274
+ terminal: z.string().optional(),
275
+ }),
276
+ z.object({
277
+ kind: z.literal("subagent_update"),
278
+ id: z.string(),
279
+ status: SubagentStatusSchema.optional(),
280
+ tokens: z.number().optional(),
281
+ toolUses: z.number().optional(),
282
+ lastTool: z.string().optional(),
283
+ summary: z.string().optional(),
284
+ error: z.string().optional(),
285
+ }),
232
286
  z.object({ kind: z.literal("todos"), items: z.array(TodoItemSchema) }),
233
287
  // The provider's own slash commands (ACP available_commands_update), replaced whole each time — the
234
288
  // composer's `/` popover lists them; invoking one is plain `/name …` prompt text (the ACP convention).
@@ -322,9 +376,11 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
322
376
  "claude-reauth",
323
377
  // The API refused this turn's token MID-FLIGHT — nearly always one superseded by a rotation,
324
378
  // which Anthropic retires the moment its successor is minted. Distinct from claude-reauth: the
325
- // account is fine and the daemon re-mints on the spot, so this frame is a notice about a turn
326
- // that resumed itself, not a request for the user to do anything. It only reaches the client
327
- // when the resume could NOT start, which is when reconnecting really is the fix.
379
+ // account is fine and the daemon re-mints on the spot, so this is usually a notice about a turn
380
+ // that is coming back rather than a request for the user to do anything. `autoResume` says
381
+ // which of the two: "scheduled" means the re-mint-and-re-run is armed, and its absence means
382
+ // nothing is coming (the turn was already a resume, or it ran on a credential with nothing to
383
+ // re-mint from) — that is the case where reconnecting really is the fix.
328
384
  "claude-token-refused",
329
385
  // The model provider itself failed transiently — 500/502/503, a 529 at capacity, a dropped
330
386
  // socket — and the harness's own in-turn retries did not outlast it. Nothing about the workspace
@@ -346,11 +402,12 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
346
402
  // rate_limit_event or the account's persisted usage windows). Absent when the reset instant is unknown
347
403
  // (nothing to schedule against).
348
404
  resetsAt: z.number().optional(),
349
- // Where the daemon's resume of THIS turn stands the same two states for a spent allowance and for a
350
- // provider outage, because the client's reading of them is the same: "scheduled" = the resume is armed
351
- // and this turn comes back by itself; "available" = the daemon remembered the failed turn and turning
352
- // the setting on (autoResumeOnLimit / resumeAfterOutage) arms that same resume, which is what the
353
- // chat's offer banner hangs off. Absent there is nothing to resume.
405
+ // Where the daemon's resume of THIS turn stands, for the two codes that have one (provider-outage,
406
+ // claude-token-refused). "scheduled" = the resume is armed and this turn comes back by itself;
407
+ // "available" = the daemon remembered the failed turn and turning resumeAfterOutage on arms that same
408
+ // resume, which is what the chat's offer banner hangs off outage only, since a renewal is never gated
409
+ // on a setting. Absent means there is nothing automatic to resume: a spent usage limit never has one,
410
+ // and a refused credential has none once re-minting it has already been tried and failed.
354
411
  autoResume: z.enum(["scheduled", "available"]).optional(),
355
412
  /* provider-outage only: the shape of the wait. `retryAt` (epoch seconds) is when the next attempt is
356
413
  * due — not a fixed cadence, because an outage has no reset instant to aim at and hammering a provider
@@ -361,10 +418,6 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
361
418
  * on-by-default retry that gives no account of how long it will keep going is the kind users switch off
362
419
  * defensively; one that says "attempt 2 of 6" is one they leave on. */
363
420
  outage: z.object({ retryAt: z.number(), attempt: z.number(), maxAttempts: z.number() }).optional(),
364
- // rate_limit only: the account whose allowance is spent, as the DAEMON resolved it (the client's own
365
- // selection can be empty, which means "the provider's first"). It is what lets the chat offer the
366
- // provider's OTHER accounts as a resume-now instead of a wait — see /agent/resume-limit.
367
- account: z.string().optional(),
368
421
  }),
369
422
  z.object({ kind: z.literal("done") }),
370
423
  ]);
@@ -383,6 +436,32 @@ export const AttachFrameSchema = z.discriminatedUnion("kind", [
383
436
  ]);
384
437
  export type AttachFrame = z.infer<typeof AttachFrameSchema>;
385
438
 
439
+ /* WHAT A RESUMED TURN'S PROMPT SAYS IT IS. The daemon re-runs a turn something underneath it killed (turn-resume.ts)
440
+ * by sending the original prompt again behind one of these sentences, so the model knows what interrupted it.
441
+ *
442
+ * They live on the wire rather than in the daemon because the CLIENT has to recognise them too: an attach head
443
+ * carries the run's prompt verbatim, and a window joining a resumed run would otherwise render the note as a
444
+ * message the USER wrote — the same words the user already said one run up, with a machine's preamble on them.
445
+ * Recognising the prefix is what lets that window reuse the bubble that is already there instead. */
446
+ export const RESUME_NOTES = {
447
+ auth: "The Claude credential that interrupted this conversation has been renewed, and this turn resumed automatically.",
448
+ outage: "The model provider was briefly unavailable and interrupted this conversation; this turn resumed automatically.",
449
+ restart: "The sandbox restarted while this turn was running, which stopped it, and this turn resumed automatically once it came back.",
450
+ } as const;
451
+
452
+ // The prompt a resume actually sends: the note, then why the words below are being repeated, then them.
453
+ export const withResumeNote = (prompt: string, note: string): string =>
454
+ Object.values(RESUME_NOTES).some((known) => prompt.startsWith(known))
455
+ ? prompt
456
+ : `${note} The interrupted request is repeated below — where part of it was already completed in this session, continue from that point instead of starting over.\n\n${prompt}`;
457
+
458
+ // The user's own words inside a resumed prompt — the note and its explanation stripped back off. Returns the
459
+ // prompt unchanged when it is not a resume, so a caller can hand every attach head through it.
460
+ export const withoutResumeNote = (prompt: string): string => {
461
+ const note = Object.values(RESUME_NOTES).find((known) => prompt.startsWith(known));
462
+ return note === undefined ? prompt : prompt.slice(prompt.indexOf("\n\n") + 2);
463
+ };
464
+
386
465
  // One parsed line from `intentic … --output ndjson` (engine events, provider `log`, the terminal `result`).
387
466
  // Open-ended by design — the sandbox consumes the wire shape, not @intentic/engine's types — so a string
388
467
  // `kind` plus arbitrary extra fields pass through. The apply-events tail (intentic.contract `applyEvents`) rides
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ export * from "./effects.js";
67
67
  export * from "./events.js";
68
68
  export * from "./sse.js";
69
69
  export * from "./routes.js";
70
+ export * from "./workspace-state.js";
70
71
  export * from "./agent-catalog.js";
71
72
  export * from "./hostnames.js";
72
73
  export * from "./model-order.js";
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Anthropic's REST /v1/models answers newest-first: that IS a provider opinion, and Claude's catalog rides it
5
5
  * (claude-models.ts). Every other provider here is read through an OpenAI-compatible /v1/models — Codex and
6
- * Gemini via the bundled translator, Kimi via Moonshot — or out of xAI's "Did you mean" rejection, and those
6
+ * Gemini and Kimi via the bundled translator — or out of xAI's "Did you mean" rejection, and those
7
7
  * endpoints publish a SET, not a ranking: they hand the ids back in whatever order their registry iterates,
8
8
  * which in practice is alphabetical. Reading that as a preference is what put "GPT 5.4 Mini" at the head of the
9
9
  * Codex group with GPT 5.6 below it, and what made a fresh Codex conversation start on whichever id happened to
@@ -31,7 +31,7 @@ test("puts tier ahead of cost — a free frontier model is still the wrong tool
31
31
  expect(resolveQuickModel([CLAUDE, proOnly], ``)).toEqual({ provider: `claude`, model: `claude-haiku-4-5-20251001` });
32
32
  });
33
33
 
34
- test("prefers a subscription to a metered key at equal tier, since only one of them charges per click", () => {
34
+ test("uses stable provider order when two subscriptions offer the same tier", () => {
35
35
  const kimiCheap: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2-mini`] };
36
36
  const claudeCheap: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-haiku-4-5`] };
37
37
 
@@ -21,13 +21,14 @@ test("a payload from a build that predates a toggle parses, with the new toggle
21
21
  expect(SandboxSettingsSchema.parse(older)).toEqual({
22
22
  ...older,
23
23
  terseHoldout: 0,
24
+ iqContext: false,
25
+ iqContextHoldout: 0,
24
26
  filterBackend: "native",
25
27
  systemPromptMode: "intentic",
26
28
  systemPrompt: "",
27
29
  quickModel: "",
28
30
  agentRetentionDays: 3,
29
31
  autoLand: true,
30
- autoResumeOnLimit: false,
31
32
  resumeAfterOutage: true,
32
33
  autoResumeOnRestart: true,
33
34
  gateCommand: "",
@@ -46,6 +47,10 @@ test("an empty object is the full default settings object", () => {
46
47
  // Off: the steer's turn-level control spends the tokens it measures, so measuring is opt-in.
47
48
  terseHoldout: 0,
48
49
  iqSearch: false,
50
+ // Off, and its holdout with it: pre-injection spends input tokens on every eligible turn, and the
51
+ // control that would tell you whether they paid for themselves costs the turns it measures.
52
+ iqContext: false,
53
+ iqContextHoldout: 0,
49
54
  outputCleaners: "off",
50
55
  outputHoldout: 0,
51
56
  filterBackend: "native",
@@ -62,9 +67,8 @@ test("an empty object is the full default settings object", () => {
62
67
  // On because it is the historical behaviour — defaulting off would silently hold every existing
63
68
  // sandbox's finished work on branches nobody is watching.
64
69
  autoLand: true,
65
- autoResumeOnLimit: false,
66
- // On, unlike the limit resume beside it: an outage resume spends nothing the dead turn hadn't already
67
- // committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
70
+ // On, where a spent usage limit re-runs nothing: an outage resume spends nothing the dead turn hadn't
71
+ // already committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
68
72
  resumeAfterOutage: true,
69
73
  // On: a daemon restart is usually intentic's own doing (an image update, an approved environment
70
74
  // change), not the user's decision, so the turn it interrupted resumes rather than staying stuck.