@intentic/sandbox-contract 1.246.1 → 1.248.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 (106) hide show
  1. package/dist/batch-runs.d.ts +2 -0
  2. package/dist/batch-runs.d.ts.map +1 -1
  3. package/dist/batch-runs.js +1 -0
  4. package/dist/batch-runs.js.map +1 -1
  5. package/dist/contracts/agent.contract.d.ts +20 -0
  6. package/dist/contracts/agent.contract.d.ts.map +1 -1
  7. package/dist/contracts/personas.contract.d.ts +38 -4
  8. package/dist/contracts/personas.contract.d.ts.map +1 -1
  9. package/dist/contracts/personas.contract.js +10 -1
  10. package/dist/contracts/personas.contract.js.map +1 -1
  11. package/dist/contracts/runner.contract.d.ts +93 -93
  12. package/dist/contracts/settings.contract.d.ts +109 -12
  13. package/dist/contracts/settings.contract.d.ts.map +1 -1
  14. package/dist/contracts/usage.contract.d.ts +22 -0
  15. package/dist/contracts/usage.contract.d.ts.map +1 -1
  16. package/dist/contracts/usage.contract.js +19 -0
  17. package/dist/contracts/usage.contract.js.map +1 -1
  18. package/dist/definition.d.ts +20 -24
  19. package/dist/definition.d.ts.map +1 -1
  20. package/dist/fast-tier.js +1 -1
  21. package/dist/fast-tier.js.map +1 -1
  22. package/dist/index.d.ts +191 -19
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/model-pins.d.ts +15 -0
  27. package/dist/model-pins.d.ts.map +1 -0
  28. package/dist/model-pins.js +22 -0
  29. package/dist/model-pins.js.map +1 -0
  30. package/dist/model-roles.d.ts +172 -0
  31. package/dist/model-roles.d.ts.map +1 -0
  32. package/dist/model-roles.js +167 -0
  33. package/dist/model-roles.js.map +1 -0
  34. package/dist/schemas/agent.d.ts +22 -2
  35. package/dist/schemas/agent.d.ts.map +1 -1
  36. package/dist/schemas/agent.js +4 -2
  37. package/dist/schemas/agent.js.map +1 -1
  38. package/dist/schemas/automations.d.ts.map +1 -1
  39. package/dist/schemas/automations.js.map +1 -1
  40. package/dist/schemas/personas.d.ts +48 -4
  41. package/dist/schemas/personas.d.ts.map +1 -1
  42. package/dist/schemas/personas.js +28 -5
  43. package/dist/schemas/personas.js.map +1 -1
  44. package/dist/schemas/plan-limits.d.ts +20 -0
  45. package/dist/schemas/plan-limits.d.ts.map +1 -1
  46. package/dist/schemas/plan-limits.js +21 -0
  47. package/dist/schemas/plan-limits.js.map +1 -1
  48. package/dist/schemas/settings.d.ts +88 -6
  49. package/dist/schemas/settings.d.ts.map +1 -1
  50. package/dist/schemas/settings.js +19 -23
  51. package/dist/schemas/settings.js.map +1 -1
  52. package/dist/schemas/usage.d.ts +5 -0
  53. package/dist/schemas/usage.d.ts.map +1 -1
  54. package/dist/schemas/usage.js +5 -0
  55. package/dist/schemas/usage.js.map +1 -1
  56. package/dist/workspace-state.d.ts +0 -5
  57. package/dist/workspace-state.d.ts.map +1 -1
  58. package/dist/workspace-state.js +0 -1
  59. package/dist/workspace-state.js.map +1 -1
  60. package/package.json +4 -4
  61. package/src/agent-catalog.ts +1 -1
  62. package/src/batch-runs.test.ts +10 -5
  63. package/src/batch-runs.ts +10 -3
  64. package/src/chores/chores.ts +1 -1
  65. package/src/chores/verdict.test.ts +2 -2
  66. package/src/chores/verdict.ts +3 -3
  67. package/src/contracts/personas.contract.ts +17 -0
  68. package/src/contracts/usage.contract.ts +31 -0
  69. package/src/events.ts +1 -1
  70. package/src/fast-tier.test.ts +1 -1
  71. package/src/fast-tier.ts +5 -5
  72. package/src/index.ts +2 -3
  73. package/src/model-pins.test.ts +121 -0
  74. package/src/model-pins.ts +132 -0
  75. package/src/model-roles.test.ts +52 -0
  76. package/src/model-roles.ts +320 -0
  77. package/src/plan-pools.ts +1 -1
  78. package/src/prompt-complexity.test.ts +1 -1
  79. package/src/prompt-complexity.ts +2 -2
  80. package/src/provider-specs.test.ts +1 -1
  81. package/src/schemas/agent.ts +42 -17
  82. package/src/schemas/agents.ts +2 -2
  83. package/src/schemas/automations.ts +6 -2
  84. package/src/schemas/personas.ts +85 -18
  85. package/src/schemas/plan-limits.ts +50 -0
  86. package/src/schemas/settings.ts +92 -98
  87. package/src/schemas/usage.ts +59 -0
  88. package/src/workspace-state.test.ts +0 -1
  89. package/src/workspace-state.ts +0 -6
  90. package/dist/agent-run-model.d.ts +0 -4
  91. package/dist/agent-run-model.d.ts.map +0 -1
  92. package/dist/agent-run-model.js +0 -13
  93. package/dist/agent-run-model.js.map +0 -1
  94. package/dist/quick-model.d.ts +0 -15
  95. package/dist/quick-model.d.ts.map +0 -1
  96. package/dist/quick-model.js +0 -39
  97. package/dist/quick-model.js.map +0 -1
  98. package/dist/schemas/context.d.ts +0 -30
  99. package/dist/schemas/context.d.ts.map +0 -1
  100. package/dist/schemas/context.js +0 -34
  101. package/dist/schemas/context.js.map +0 -1
  102. package/src/agent-run-model.test.ts +0 -76
  103. package/src/agent-run-model.ts +0 -65
  104. package/src/quick-model.test.ts +0 -158
  105. package/src/quick-model.ts +0 -162
  106. package/src/schemas/context.ts +0 -87
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ModelRoleSchema } from "../model-roles.js";
2
3
  import { NATIVE_PROVIDERS } from "../provider-specs.js";
3
4
  import { AgentPlacementSchema } from "../runner-protocol.js";
4
5
  import { entryId } from "./internal.js";
@@ -267,6 +268,23 @@ export const AgentTurnSchema = z
267
268
  .describe(
268
269
  "Whether this turn's work merges into the workspace when it finishes. Overrides the conversation's own setting for this turn only.",
269
270
  ),
271
+ /* WHAT STARTED THIS TURN, when it was not a person at a composer, and therefore which of the owner's
272
+ * model lists answers for it (settings.modelRoles, model-roles.ts declares the roles).
273
+ *
274
+ * IT NAMES THE JOB, NOT THE TIER. Before this field there was one list for every unattended turn, so a
275
+ * production incident and a documentation sweep were configured together, and a surface added tomorrow
276
+ * inherited that tier by saying nothing. Now a starter says what it IS, and the owner gets to answer
277
+ * per job: cheap for the sweep, frontier for the incident.
278
+ *
279
+ * ONLY FILLS A SILENCE. The daemon applies the role's list to a turn that named no model AND no
280
+ * provider (turn-resume.ts): a caret pick, an acceptance run's own choice, a workflow step pinned to a
281
+ * provider — all of those already answered the question, and this must not overrule them.
282
+ *
283
+ * A `run` role, always. The `helper` roles never reach here: a one-shot is not a turn, it has no
284
+ * conversation and no worktree, and it asks its own role directly at the seam that spends it. */
285
+ runRole: ModelRoleSchema.optional().describe(
286
+ "What started this turn, when it was not a person typing: which of the sandbox's per-job model lists answers for it. Only used when the turn names no model of its own.",
287
+ ),
270
288
  // Set ONLY by the daemon's own automation dispatchers: this turn opens a conversation on behalf of an
271
289
  // outside message rather than a user. Recorded on the registry entry so the fleet can say where the
272
290
  // agent came from. Requires conversationId, there is nothing to record it on otherwise.
@@ -311,7 +329,7 @@ export const AgentTurnSchema = z
311
329
  * opposite defaults, the chat wants the provider's own catalog default, an unattended run wants the
312
330
  * tier its owner chose for work that spends money while they are not watching.
313
331
  *
314
- * The daemon fills `agent`/`model` and the pinned entry's own knobs from agentRunModels for any turn that says
332
+ * The daemon fills `agent`/`model` and the pinned entry's own knobs from the turn's own role list for any turn that says
315
333
  * this and names none of them (startConversationTurn), walking that list until one can actually be
316
334
  * started. Naming one still wins: every surface-started run now carries a caret that overrides the list
317
335
  * for that run alone, and Acceptance picks per run because it fans a session out per story. Either way
@@ -416,14 +434,14 @@ export type AgentTurn = z.infer<typeof AgentTurnSchema>;
416
434
  * Shared rather than re-declared per route because every surface that starts an agent for the user now carries
417
435
  * that caret, and they must all mean the same thing by it: the pair rides onto the turn as `agent`/`model`, and
418
436
  * the daemon's own fill step then leaves it alone (turn-resume.ts fills only what is absent). ABSENT is the
419
- * ordinary case and the one to keep cheap, nobody touched the caret, so `agentRunModels` answers.
437
+ * ordinary case and the one to keep cheap, nobody touched the caret, so the turn's `runRole` list answers.
420
438
  *
421
439
  * Both halves or neither, because a model id is only meaningful to the provider that vends it: half a pick
422
440
  * would send a Codex model id to Claude. Routes that accept this pass it through verbatim; a model this build
423
441
  * has never heard of is a supported pick, since the picker offers a custom-id escape hatch.
424
442
  *
425
443
  * AND THE TIER IT RUNS AT, because naming a model is only half of what the standing setting says. A pinned
426
- * entry carries its own effort (AgentRunPinSchema), and the daemon applies the pin's knobs ONLY to a turn that
444
+ * entry carries its own effort (ModelPinSchema), and the daemon applies the pin's knobs ONLY to a turn that
427
445
  * named no model (turn-resume.ts): so a caret that could re-point the model but not the tier moved every
428
446
  * override onto the provider's own default effort, and the one moment somebody reaches for the caret is the
429
447
  * failure that just beat the standing order. Optional, and absent means absent, the turn goes out without an
@@ -442,29 +460,36 @@ export const AgentRunPickSchema = z
442
460
  })
443
461
  .optional();
444
462
  export type AgentRunPick = z.infer<typeof AgentRunPickSchema>;
445
- /* A MODEL PINNED FOR EVERY SURFACE-STARTED RUN, one entry of settings.agentRunModels: the standing version of
446
- * the pick above, and not merely which model but HOW it is to be run.
463
+ /* ONE ENTRY OF ONE ROLE'S MODEL LIST (settings.modelRoles): the standing version of the pick above, and not
464
+ * merely which model but HOW it is to be run.
447
465
  *
448
- * THE KNOBS RIDE THE ENTRY RATHER THAN THE LIST, which is the whole reason this is an object where the setting
449
- * used to hold a `${provider}:${model}` string. The reasoning effort was a single field beside the list, so one
450
- * tier answered for every model in it — and the entries of that list are deliberately NOT interchangeable: it
451
- * is a frontier pin with the cheap account underneath that catches it when the first is spent. A tier scale is
452
- * a property of the MODEL as well ('max' is off Kimi's scale entirely, and off Claude's own the moment thinking
466
+ * THE KNOBS RIDE THE ENTRY RATHER THAN THE LIST, which is the whole reason this is an object rather than a
467
+ * `${provider}:${model}` string. The reasoning effort was once a single field beside a list, so one tier
468
+ * answered for every model in it — and the entries of such a list are deliberately NOT interchangeable: it is a
469
+ * frontier pin with the cheap account underneath that catches it when the first is spent. A tier scale is a
470
+ * property of the MODEL as well ('max' is off Kimi's scale entirely, and off Claude's own the moment thinking
453
471
  * is switched off), so a shared effort was either off-scale for half the list or the lowest common rung for all
454
- * of it. Each entry now carries what the composer's picker configures for the turn in front of you.
472
+ * of it. Each entry carries what the composer's picker configures for the turn in front of you.
473
+ *
474
+ * THE SAME SHAPE FOR EVERY ROLE, one-shot helpers included, and that is a deliberate widening. A commit
475
+ * message or a session title used to be pinnable by model alone, on the argument that the daemon runs those
476
+ * with reasoning off and no effort, so a control for either would be a switch with nothing behind it. True of
477
+ * the machinery, and it made the machinery the argument: an owner who pins a reasoning model to their commit
478
+ * subjects was paying that model's price to have its distinguishing feature suppressed. The knobs now travel
479
+ * through the one-shot path too, so an entry means the same thing wherever it is written.
455
480
  *
456
- * EVERY FIELD BUT THE PAIR IS OPTIONAL, AND ABSENT MEANS ABSENT: the turn goes out without the field and the
481
+ * EVERY FIELD BUT THE PAIR IS OPTIONAL, AND ABSENT MEANS ABSENT: the work goes out without the field and the
457
482
  * provider's own default answers, exactly as an unconfigured pin always did. Nothing here invents a "low".
458
483
  *
459
484
  * NO TIER HOLD, and its absence is the rule rather than an omission: automatic tier selection gates on
460
- * `unattended` (prompt-complexity.ts), so a surface-started run is never downgraded in the first place and a
461
- * veto over it would be a control whose state can make no difference to anything.
485
+ * `unattended` (prompt-complexity.ts), so a role-started run is never downgraded in the first place and a veto
486
+ * over it would be a control whose state can make no difference to anything.
462
487
  *
463
488
  * The pair is BOTH HALVES for the reason the pick above is: a model id is only meaningful to the provider that
464
489
  * vends it, so half a pin would send a Codex id to Claude. Taken verbatim, never validated against a catalog:
465
490
  * the picker offers a custom-id escape hatch, so a model this build has never heard of is a supported pin. */
466
- export const AgentRunPinSchema = z.object({
467
- provider: AgentProviderSchema.describe("Which provider serves the run."),
491
+ export const ModelPinSchema = z.object({
492
+ provider: AgentProviderSchema.describe("Which provider serves this work."),
468
493
  model: z.string().min(1).describe("Which of its models. Both halves, because a model name only means anything to the provider that serves it."),
469
494
  effort: z
470
495
  .string()
@@ -474,7 +499,7 @@ export const AgentRunPinSchema = z.object({
474
499
  fast: z.boolean().optional().describe("Ask for this model's work at a higher rate for a higher price. A request rather than a promise."),
475
500
  harness: AgentHarnessSchema.optional().describe("Which agentic loop runs it. Leave it out to use the provider's own."),
476
501
  });
477
- export type AgentRunPin = z.infer<typeof AgentRunPinSchema>;
502
+ export type ModelPin = z.infer<typeof ModelPinSchema>;
478
503
  // POST /agent's ack: the daemon-minted id of the detached turn run it started. The turn executes daemon-side
479
504
  // regardless of any client connection; every window, the initiator included, renders it via /agent/attach.
480
505
  export const StartedTurnSchema = z.object({
@@ -182,8 +182,8 @@ export const LandedMessageSchema = z.object({
182
182
  .describe("What this change takes away, for anything already relying on it. Nearly always absent: it is for removals, not for additions."),
183
183
  });
184
184
  export type LandedMessage = z.infer<typeof LandedMessageSchema>;
185
- /* ONE MODEL'S TURN IN THE DRAFTING WALK, asked, and what became of the ask. The quick-model chain tries the
186
- * connected models in order (agent/quick-model.ts), and each rung ends one of four ways:
185
+ /* ONE MODEL'S TURN IN THE DRAFTING WALK, asked, and what became of the ask. The one-shot helper chain tries the
186
+ * connected models in order (agent/role-model.ts), and each rung ends one of four ways:
187
187
  * asking , in flight right now; `ms` absent because it is still being spent.
188
188
  * answered, it wrote the sentence, in `ms`.
189
189
  * refused , it failed or declined, in `ms`, with its own words in `reason`.
@@ -264,7 +264,9 @@ export type WebchatMessage = z.infer<typeof WebchatMessageSchema>;
264
264
  export const AutomationSchema = z.object({
265
265
  id: entryId.describe("The automation's id."),
266
266
  trigger: TriggerSchema.describe("What sets it off: a schedule, an event in the workspace, a message arriving from outside, or a webhook."),
267
- // Shell command run in the workspace root before waking; exit 0 ⇒ wake, non-zero ⇒ the run is "skipped".
267
+ /* Shell command run in the workspace root before waking; exit 0 ⇒ wake, non-zero ⇒ the run is "skipped".
268
+ * Its environment carries `AUTOMATION_ID` (this automation's own id, so a guard can look up what its past
269
+ * fires did) and, for a trigger that arrived with one, `AUTOMATION_PAYLOAD`. */
268
270
  guard: z
269
271
  .string()
270
272
  .min(1)
@@ -279,7 +281,9 @@ export const AutomationSchema = z.object({
279
281
  // field rather than a shared "public endpoint" bag: the two sources answer different questions (a chat's
280
282
  // greeting and access model, an intake's dedup ceiling and ingest key) and a union of both would be a
281
283
  // schema where most fields are wrong for whichever source is reading it.
282
- issues: IssuesConfigSchema.optional().describe("Settings for the bug reporter, for an automation that takes crash reports from your own sites and apps."),
284
+ issues: IssuesConfigSchema.optional().describe(
285
+ "Settings for the bug reporter, for an automation that takes crash reports from your own sites and apps.",
286
+ ),
283
287
  /* NARROW THIS ONE JOB FURTHER than the persona it runs as, raw tool names, and the escape hatch under the
284
288
  * shelves rather than the way anyone is expected to answer this question.
285
289
  *
@@ -1,14 +1,25 @@
1
1
  // Personas: the named faces a sandbox shows the outside world — which accounts each speaks for, what a
2
2
  // session wearing one may do, and where it works.
3
3
  import { z } from "zod";
4
+ import { type ModelSource, readyChain } from "../model-pins.js";
5
+ import { type ModelPin, ModelPinSchema } from "./agent.js";
4
6
  import { entryId } from "./internal.js";
5
7
  import { SkillDraftSchema, SkillNameSchema, SystemPromptModeSchema } from "./settings.js";
6
8
  /* A NAMED PERSONA THE SANDBOX SHOWS THE OUTSIDE WORLD, "work-reddit", "the studio account", and the layer
7
9
  * that decides which connected accounts a given turn may act through.
8
10
  *
9
- * IT ANSWERS FOUR QUESTIONS AND NO MORE: who it speaks as, what it may do, where it works, and what it is told.
10
- * Making one is then a name, a few accounts, some switches and, only if you want one, a prompt. That is the
11
- * whole of what an owner is deciding, and short enough that they finish.
11
+ * IT ANSWERS FIVE QUESTIONS AND NO MORE: who it speaks as, what it may do, where it works, what it is told, and
12
+ * what it runs on (the repositories its tree holds, the models it runs on). Making one is then a name, a few
13
+ * accounts, some switches and, only if you want one, a prompt. That is the whole of what an owner is deciding,
14
+ * and short enough that they finish.
15
+ *
16
+ * A CARD IS THE SANDBOX'S ONE STATIC DESCRIPTION OF A WORKING POSTURE, and static is the design. Everything a
17
+ * session sees before the user's first word is a function of the card it wears: the accounts, the tool set the
18
+ * powers leave, the kit's prompt and skills, and now the tree it opens on. Two sessions on one card open on
19
+ * the same prefix, which is what lets a provider's prompt cache serve the second. The only per-task decision
20
+ * left is WHICH card, and that is a classification over the owner's own short list (`brief`), once per chat,
21
+ * which a cheap model does reliably; asking a model to compose a context per session instead either picks
22
+ * badly or costs a second strong run and a hand-off of everything it decided.
12
23
  *
13
24
  * NO PUBLISH-OR-DRAFT SWITCH. It read as a lock and was a sentence: it asked the turn to route outward things
14
25
  * through the approvals queue and could not stop it posting. The queue is the mechanism, and a control whose
@@ -122,6 +133,28 @@ export const PersonaWorkspaceSchema = z.object({
122
133
  folders: z.array(z.string().min(1)).max(50).optional().describe("Which folders it may touch at all. Absent means the whole workspace."),
123
134
  });
124
135
  export type PersonaWorkspace = z.infer<typeof PersonaWorkspaceSchema>;
136
+ /* WHAT A CONVERSATION WEARING THIS CARD CARRIES: which repositories its checkout HOLDS, as opposed to
137
+ * `workspace.folders`, which fences what the file tools may TOUCH inside a tree that holds everything.
138
+ *
139
+ * `repos` names NESTED repositories by their workspace-relative dir (the ids repo-discovery reports). The root
140
+ * repository is the workspace itself and is always carried, never listed, so an empty list is the workspace
141
+ * alone. ABSENT `context` is every repository the workspace has, which is what every card meant before this
142
+ * existed and what a card that has never thought about it should get. The sandbox brings a conversation's
143
+ * checkout to this list on every turn (agents/worktrees.ts `selection`): a repository the list names joins,
144
+ * one it stops naming leaves with its work committed to the branch.
145
+ *
146
+ * An OBJECT rather than a bare list, so the next kinds of thing a session can carry (a directory inside a
147
+ * repository through a sparse cone, a documents folder) arrive as fields beside this one rather than as a
148
+ * grammar inside it. */
149
+ export const PersonaContextSchema = z.object({
150
+ repos: z
151
+ .array(z.string().min(1).max(200))
152
+ .max(50)
153
+ .describe(
154
+ "Which nested repositories a conversation wearing this card carries, by workspace-relative path. The workspace itself is always carried; empty means the workspace alone.",
155
+ ),
156
+ });
157
+ export type PersonaContext = z.infer<typeof PersonaContextSchema>;
125
158
  export const PersonaSchema = z.object({
126
159
  id: entryId.describe("The persona's id."),
127
160
  // What the owner calls it in the composer chip. Absent ⇒ surfaces read the id, which is already human-chosen.
@@ -138,28 +171,40 @@ export const PersonaSchema = z.object({
138
171
  .describe(
139
172
  "Which connected accounts are its hands. Named individually rather than by site, because two accounts on one site is the whole problem this solves. Naming one that is not connected yet is not an error: it is a card describing an account this sandbox has still to sign into.",
140
173
  ),
141
- /* Which workspace repos prefer this persona, so a chat opened on a project starts with the right chip already
142
- * selected. A PREFERENCE, not a fence, the owner's chosen chat default is still "every account", and it
143
- * lives on the card rather than in each project's own config so that one account named by three repos stays
144
- * one definition instead of three that drift. */
145
- repos: z
146
- .array(z.string().min(1))
147
- .max(50)
174
+ /* WHAT THIS PERSONA IS FOR, in one line: the sentence a new chat is routed on (the sandbox's
175
+ * agent/persona-router.ts reads one line per card and names one) and the row's subtitle on the Personas
176
+ * page. Written by the owner rather than derived from the card, because a classifier reading "backend work
177
+ * on the api and billing services, never the marketing site" routes better than one reading a list of
178
+ * account ids, and because it is the one sentence a reader scanning the list wants under each name. Absent
179
+ * is a card the router can still name from what else the card says, with less to go on. */
180
+ brief: z
181
+ .string()
182
+ .max(200)
148
183
  .optional()
149
- .describe(
150
- "Which repositories prefer this persona, so a conversation opened on one starts with the right choice already made. A preference rather than a fence.",
151
- ),
184
+ .describe("What this persona is for, in one line. A new chat is routed onto a persona by this sentence, and the Personas page shows it under the name."),
152
185
  // What a session wearing this card may do, and where it works. Both absent ⇒ the full toolbox and the whole
153
186
  // workspace, so a card written before these existed keeps behaving exactly as it did.
154
187
  powers: PersonaPowersSchema.optional().describe(
155
188
  "What a conversation wearing it may do. Absent means the full toolbox, so a card written before this existed behaves exactly as it did.",
156
189
  ),
157
190
  workspace: PersonaWorkspaceSchema.optional().describe("Where it works. Absent means the whole workspace."),
158
- /* WHICH PART OF THE WORKSPACE A SESSION WEARING THIS CARD CARRIES, the id of a context shelf
159
- * (schemas/context.ts). A different question from `workspace.folders`: that one fences what the file tools
160
- * may TOUCH inside a tree that holds everything, this one decides what the tree HOLDS. Absent falls through to
161
- * the sandbox's `contextShelf` setting, and from there to everything. */
162
- context: entryId.optional().describe("Which context shelf a conversation wearing it opens on: the part of the workspace it carries. Absent follows the sandbox setting."),
191
+ context: PersonaContextSchema.optional().describe(
192
+ "Which part of the workspace a conversation wearing it carries: the repositories its checkout holds. Absent means every repository.",
193
+ ),
194
+ /* WHICH MODELS A CONVERSATION WEARING THIS CARD RUNS ON, in order: the ladder shape every role list has
195
+ * (settings.modelRoles, model-pins.ts), for the same reason, a single pin is a single point of failure and
196
+ * the next entry catches an account that is spent today. ABSENT is the caller's own answer: the composer's
197
+ * pick for a chat, the run role's list for a turn a surface started. Present, the composer moves its model
198
+ * pill to the ladder's head when the card is picked (so a chat routed onto a card runs on that card's
199
+ * model as well as in its context), and the sandbox fills an unattended turn's silence from it before the
200
+ * run role's list (agent/turn-resume.ts). A model named on the turn itself always wins. */
201
+ models: z
202
+ .array(ModelPinSchema)
203
+ .max(10)
204
+ .optional()
205
+ .describe(
206
+ "Which models a conversation wearing it runs on, tried in order. Absent means whatever the chat or the job would have run on anyway; a model chosen for the turn itself always wins.",
207
+ ),
163
208
  /* WHICH SYSTEM PROMPT A SESSION WEARING THIS CARD RUNS ON, the same three bases the sandbox chooses
164
209
  * between, asked per card. ABSENT is the fourth answer and the default: follow the sandbox, which is what
165
210
  * every card meant before this field existed and what almost every card will go on meaning.
@@ -179,6 +224,28 @@ export const PersonaSchema = z.object({
179
224
  systemPromptMode: SystemPromptModeSchema.optional(),
180
225
  });
181
226
  export type Persona = z.infer<typeof PersonaSchema>;
227
+ /* WHICH OF A CARD'S MODELS THIS SANDBOX CAN RUN, IN ORDER: the ladder filtered to connected providers and
228
+ * deduplicated — exactly the walk a role list gets, and now the SAME CALL rather than a sibling of one
229
+ * (model-pins.ts readyChain). It used to be "that walk minus the role's floor"; there is no floor any more, so
230
+ * the two lists ask one question. An absent ladder, or one whose every provider is disconnected, is empty
231
+ * here, and empty means the caller's own answer: a card that says nothing about models has not asked for a
232
+ * cheap one, it has left the question to whoever opened the chat. */
233
+ export const personaModels = (card: Pick<Persona, "models">, sources: readonly ModelSource[]): readonly ModelPin[] => readyChain(sources, card.models ?? []);
234
+ /* WHAT A NEW CHAT IS ROUTED ON, and what comes back. The composer asks once per settled draft on a chat that
235
+ * has no turns and no persona pinned by hand (its personaRoute composable), and the sandbox answers with the
236
+ * one card the message belongs to, or none. `folder` and `paths` are the two facts a card's `context` and
237
+ * `workspace.startIn` can be matched against and the words alone cannot supply. */
238
+ export const PersonaRouteAskSchema = z.object({
239
+ prompt: z.string().min(1).max(20000).describe("The message a new chat is about to open with."),
240
+ folder: z.string().max(200).optional().describe("The workspace folder the chat was opened in, when it was opened in one."),
241
+ paths: z.array(z.string().min(1).max(500)).max(50).default([]).describe("Workspace paths the message names: uploads, @-mentions, the editor's own file."),
242
+ });
243
+ export type PersonaRouteAsk = z.infer<typeof PersonaRouteAskSchema>;
244
+ export const PersonaRouteSchema = z.object({
245
+ persona: entryId.optional().describe("The card this message belongs to, or absent when none does and the chat should stay open to everything."),
246
+ reason: z.string().describe("Why, in the one line a chip can show. Present whether or not a card was named."),
247
+ });
248
+ export type PersonaRoute = z.infer<typeof PersonaRouteSchema>;
182
249
  /* THE ONE CARD ID THE PRODUCT NAMES ITSELF, the read-only persona a public web chat answers through.
183
250
  *
184
251
  * Nothing else is stock: a fresh workspace has no personas at all, and every card on the Personas page is one
@@ -58,6 +58,56 @@ export const AccountUsageSchema = z.object({
58
58
  measuredAt: z.number(),
59
59
  });
60
60
  export type AccountUsage = z.infer<typeof AccountUsageSchema>;
61
+ /* THE ONE WAY PAST A SPENT SESSION WINDOW THAT IS NOT WAITING, which Anthropic grants once a week per account.
62
+ *
63
+ * Every other affordance around a refused turn is about WHEN: arm the appointment, count down to the reset,
64
+ * press when it opens. This one moves the clock. The provider reopens the five-hour window immediately and
65
+ * charges the account one of its weekly resets; the WEEKLY allowance is untouched and still binds, so this
66
+ * buys back the session pool and nothing else. Upstream's own CLI spells it `/limit-reset`.
67
+ *
68
+ * THE ANSWER IS THE PROVIDER'S, NEVER OURS. There is no rule here to re-derive: eligibility turns on the plan
69
+ * tier, how long the account has existed, whether it is actually at the wall, whether another experiment holds
70
+ * it, and whether the week's reset is already spent — all of it decided server-side and none of it visible from
71
+ * a usage reading. So this shape is a transcription of what the endpoint said, and the button exists only while
72
+ * it says `available`. A client must never infer availability from a 100% window.
73
+ *
74
+ * `reason` is the provider's own word for the refusal ("tier", "tenure", "not_at_wall", "weekly_limit",
75
+ * "already_used", …) and is carried rather than translated, because the set is the provider's to extend and a
76
+ * word we don't recognise is still worth showing to somebody asking why the button is not there. */
77
+ export const LimitResetStatusSchema = z.object({
78
+ available: z
79
+ .boolean()
80
+ .describe("Whether the provider will reopen this account's session window right now. The only thing a button may be drawn from."),
81
+ reason: z
82
+ .string()
83
+ .optional()
84
+ .describe("Why not, in the provider's own word, when it gave one. Absent when it is available, or when the provider said nothing."),
85
+ // Both epoch SECONDS, matching every other reset instant on the wire (UsageWindow.resetsAt, limitResetsAt).
86
+ nextAvailableAt: z
87
+ .number()
88
+ .optional()
89
+ .describe("When the next reset may be claimed, in epoch seconds, where the provider publishes it. Absent means unknown, never 'now'."),
90
+ weeklyResetsAt: z.number().optional().describe("When the weekly allowance itself reopens, in epoch seconds, where the provider publishes it."),
91
+ });
92
+ export type LimitResetStatus = z.infer<typeof LimitResetStatusSchema>;
93
+ /* WHAT CLAIMING IT DID, in the provider's own vocabulary plus the two failures that are ours.
94
+ *
95
+ * `reset` is the only outcome that changed anything, and the caller's cue to send the held turn again. The rest
96
+ * are all "nothing happened", and they are kept APART rather than folded into one failure because they are read
97
+ * by somebody who just pressed a button and is owed the difference: `already_used` means come back next week,
98
+ * `not_limited` means the window reopened while they were reading, `ineligible` means this account never had
99
+ * it, and `unavailable`/`error` mean try again. Collapsing them would make every one of those read as a fault.
100
+ *
101
+ * Never throws over the wire: a claim that fails leaves the account exactly as it was, and the honest answer to
102
+ * a press is a word, not a stack trace. */
103
+ export const LimitResetClaimSchema = z.object({
104
+ result: z
105
+ .enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "error"])
106
+ .describe("What the provider did. Only `reset` reopened the window; every other value means nothing changed."),
107
+ nextAvailableAt: z.number().optional().describe("When another reset may be claimed, in epoch seconds, where the provider published it."),
108
+ detail: z.string().optional().describe("What went wrong, in words, for the two outcomes that are this sandbox's fault rather than the plan's."),
109
+ });
110
+ export type LimitResetClaim = z.infer<typeof LimitResetClaimSchema>;
61
111
  /* THE LAST TIME A PROVIDER ACTUALLY REFUSED A TURN, the other half of "can I run on this", and the half no
62
112
  * meter can supply.
63
113
  *