@intentic/sandbox-contract 1.226.1 → 1.227.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 (54) hide show
  1. package/dist/contracts/agent.contract.d.ts +19 -2
  2. package/dist/contracts/agent.contract.d.ts.map +1 -1
  3. package/dist/contracts/agents.contract.d.ts +58 -0
  4. package/dist/contracts/agents.contract.d.ts.map +1 -1
  5. package/dist/contracts/endpoints.contract.d.ts +1 -0
  6. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  7. package/dist/contracts/extensions.contract.d.ts +1 -0
  8. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  9. package/dist/contracts/logs.contract.d.ts +17 -0
  10. package/dist/contracts/logs.contract.d.ts.map +1 -1
  11. package/dist/contracts/logs.contract.js +10 -1
  12. package/dist/contracts/logs.contract.js.map +1 -1
  13. package/dist/contracts/providers.contract.d.ts +1 -0
  14. package/dist/contracts/providers.contract.d.ts.map +1 -1
  15. package/dist/contracts/sessions.contract.d.ts +3 -0
  16. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  17. package/dist/contracts/settings.contract.d.ts +19 -0
  18. package/dist/contracts/settings.contract.d.ts.map +1 -1
  19. package/dist/contracts/system.contract.d.ts +7 -0
  20. package/dist/contracts/system.contract.d.ts.map +1 -1
  21. package/dist/events.d.ts +46 -0
  22. package/dist/events.d.ts.map +1 -1
  23. package/dist/events.js +14 -0
  24. package/dist/events.js.map +1 -1
  25. package/dist/index.d.ts +196 -70
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/prompt-complexity.d.ts +9 -1
  30. package/dist/prompt-complexity.d.ts.map +1 -1
  31. package/dist/prompt-complexity.js +13 -5
  32. package/dist/prompt-complexity.js.map +1 -1
  33. package/dist/request-id.d.ts +3 -0
  34. package/dist/request-id.d.ts.map +1 -0
  35. package/dist/request-id.js +3 -0
  36. package/dist/request-id.js.map +1 -0
  37. package/dist/schemas.d.ts +88 -0
  38. package/dist/schemas.d.ts.map +1 -1
  39. package/dist/schemas.js +56 -0
  40. package/dist/schemas.js.map +1 -1
  41. package/dist/starter.d.ts +3 -0
  42. package/dist/starter.d.ts.map +1 -0
  43. package/dist/starter.js +3 -0
  44. package/dist/starter.js.map +1 -0
  45. package/package.json +5 -4
  46. package/src/contract-lock.test.ts +8 -2
  47. package/src/contracts/logs.contract.ts +23 -2
  48. package/src/events.ts +52 -1
  49. package/src/index.ts +2 -0
  50. package/src/prompt-complexity.test.ts +69 -0
  51. package/src/prompt-complexity.ts +102 -39
  52. package/src/request-id.ts +41 -0
  53. package/src/schemas.ts +194 -0
  54. package/src/starter.ts +13 -0
package/src/schemas.ts CHANGED
@@ -378,6 +378,18 @@ export const AgentTurnSchema = z
378
378
  .describe(
379
379
  "Ask for the same work at a higher rate for a higher price. A request rather than a promise: the answer says what actually happened.",
380
380
  ),
381
+ /* KEEP THIS TURN ON THE MODEL I PICKED: the user's veto over automatic tier selection, riding the turn
382
+ * like `fast` does because it changes what the turn costs and so belongs to the turn rather than to the
383
+ * workspace. The judge still runs and the verdict is still recorded — a deny is the strongest label the
384
+ * calibration ledger ever gets (UsageTurn.tierDenied) — but nothing is substituted. The composer sends
385
+ * its conversation-level toggle here every turn, and the registry persists it beside `fast`, so the
386
+ * choice survives reopening the tab. Absent ⇒ no opinion, routing follows settings.autoTier. */
387
+ tierHold: z
388
+ .boolean()
389
+ .optional()
390
+ .describe(
391
+ "Run exactly the model that was picked, even when the turn looks simple enough for a cheaper one. The judgement is still recorded; nothing is substituted.",
392
+ ),
381
393
  // The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
382
394
  editorContext: EditorContextSchema.optional().describe(
383
395
  'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
@@ -1080,6 +1092,21 @@ export const AgentSummarySchema = z.object({
1080
1092
  effort: z.string().optional().describe("How hard that turn was told to think."),
1081
1093
  thinking: z.boolean().optional().describe("Whether that turn showed its reasoning."),
1082
1094
  fast: z.boolean().optional().describe("Whether that turn asked for higher speed. What was asked for, not what was served."),
1095
+ /* WHAT THE COMPLEXITY JUDGE MADE OF THE LAST TURN HERE, mirrored from the persisted entry (agents-store.ts
1096
+ * `tier`) so a client opening the conversation tomorrow can seed its composer preview with the one judge
1097
+ * input a draft cannot contain (prompt-complexity.ts `afterHardTurn`). The JUDGEMENT, never what ran, for
1098
+ * the reason the store states: what ran is a fact about configuration, the next turn is asking about the
1099
+ * difficulty of the work. Absent ⇒ nothing judged yet. */
1100
+ tier: z
1101
+ .enum(["fast", "standard"])
1102
+ .optional()
1103
+ .describe("How hard its last turn looked to the complexity judge. What the next turn's preview needs, not what actually ran."),
1104
+ // The conversation's standing "keep every turn on my pick" choice, the composer's memory of it, the same
1105
+ // shape as `fast` above: what was asked for, restored into the composer on open, sent back on every turn.
1106
+ tierHold: z
1107
+ .boolean()
1108
+ .optional()
1109
+ .describe("Whether this conversation is pinned to the picked model, so a turn that looks simple is never moved to a cheaper one."),
1083
1110
  account: z.string().optional().describe("Which connected account paid for it."),
1084
1111
  // The worktree branch (agent/<id>); absent for a non-isolated (main-tree) conversation.
1085
1112
  branch: z.string().optional().describe("The branch its private copy works on. Absent for a conversation that works directly in the shared tree."),
@@ -2018,6 +2045,15 @@ export const ModelSchema = z.object({
2018
2045
  "What it is good for, in the provider's own words. Absent where the provider publishes only ids, which is the honest answer rather than something to paper over with a hand-written table.",
2019
2046
  ),
2020
2047
  badges: z.array(ModelBadgeSchema).optional().describe("What it is known for, where the provider says so."),
2048
+ /* HOW MUCH THE SERVER WILL ACCEPT IN ONE REQUEST, where the server says so, and the one field here that a
2049
+ * turn is refused against rather than merely rendered (agent/context-budget.ts).
2050
+ *
2051
+ * The SERVED window, never the weights' training length. An inference server takes its context size from a
2052
+ * flag and then clamps it to the memory it actually has, so a 3B model whose GGUF advertises 131k can be
2053
+ * serving 16k, and it is the 16k that refuses the request. Read from llama.cpp's /props and vLLM's
2054
+ * `max_model_len`; absent for every provider that publishes no such number, which is most of them, and
2055
+ * absent means unknown rather than unlimited: nothing gates on a window it was never told. */
2056
+ contextWindow: z.number().optional().describe("How many tokens this model will accept in one request, where the server publishes it."),
2021
2057
  });
2022
2058
  export type Model = z.infer<typeof ModelSchema>;
2023
2059
  export const ModelsSchema = z.object({
@@ -2521,6 +2557,23 @@ export const SandboxSettingsSchema = z.object({
2521
2557
  .describe(
2522
2558
  "Whether an easy-looking turn may run on a cheaper model from the same provider. Three states rather than a switch, because the middle one is the only honest road to the third: it scores every turn and routes nothing, so the guess can become a measurement before it changes anything. It can only ever route down, so the worst case is one turn's quality rather than a bill nobody asked for.",
2523
2559
  ),
2560
+ /* HOW EAGER THE JUDGE IS, the one dial this feature exposes and the answer to what the Measure mode is for:
2561
+ * the numbers say how many turns were called simple, and this is the control that acts on them.
2562
+ *
2563
+ * Three named stops rather than a number, because the number means nothing to anyone who has not read the
2564
+ * weights, while "only the unmistakable" / "the default" / "an easy question about real code too" are three
2565
+ * sentences an owner can actually hold an opinion about (FAST_CEILINGS spells out each). It moves the
2566
+ * cutoff and nothing else: the rule that a downgrade needs something POSITIVE to have been said holds at
2567
+ * every stop, so no setting of this can start downgrading short vague requests.
2568
+ *
2569
+ * `balanced` is the default and is what every verdict recorded before this existed was judged against, so
2570
+ * the shadow history stays comparable across the change rather than silently becoming two populations. */
2571
+ autoTierEagerness: z
2572
+ .enum(["cautious", "balanced", "eager"])
2573
+ .default("balanced")
2574
+ .describe(
2575
+ "How readily a turn counts as simple enough for the cheaper model. It moves only the cutoff: at every setting a turn still has to say something positively easy, so nothing here can downgrade a short vague request.",
2576
+ ),
2524
2577
  /* WHICH CHEAP MODEL A DOWNGRADED TURN LANDS ON, an ordered list of `${provider}:${model}` keys
2525
2578
  * (quickModelKey), or EMPTY for Auto.
2526
2579
  *
@@ -2843,10 +2896,45 @@ export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
2843
2896
  // `output`/`search` are absent when that experiment isn't running at all (its flag off, or no holdout set), a
2844
2897
  // section that isn't there reads as "not measured", which is the truth, while zeros would read as "measured,
2845
2898
  // worth nothing".
2899
+ /* WHAT THE COMPLEXITY JUDGE HAS BEEN SAYING, read back off the spend ledger's tier fields (UsageTurn.tierScore
2900
+ * and friends) over the requested window. The three numbers docs/model-routing-design.md §4 says the feature
2901
+ * cannot be defended without, plus the veto count, and nothing else: no counterfactual "you would have saved
2902
+ * $X", because the ledger holds what turns COST, not what they would have cost on a model they never ran.
2903
+ *
2904
+ * NOT a TurnExperiment, deliberately. The experiments compare two randomized arms of one population; this is a
2905
+ * tally of what one mechanism observed and did. Dressing it in arms and margins would claim a control group that
2906
+ * does not exist (routing follows the settings mode, which follows time, not a coin flip).
2907
+ *
2908
+ * The whole section is absent when no turn in the window was judged at all (autoTier "off" throughout), which a
2909
+ * screen renders as absence: "not measured" is the truth, zeros would read as "measured, found nothing". */
2910
+ export const TierReportSchema = z.object({
2911
+ // Turns the judge ran on in the window, the denominator under everything below.
2912
+ judged: z.number(),
2913
+ // …of which landed at or below FAST_CEILING: the turns that looked simple. fast ÷ judged is the fast share.
2914
+ fast: z.number(),
2915
+ /* What the fast-judged turns that STAYED on the user's pick actually cost, the money measure mode is
2916
+ * pointing at. An upper bound on any saving, never an estimate of one: moving those turns to the cheap rung
2917
+ * would have cost something too, and this schema refuses to guess how much. */
2918
+ atStakeUsd: z.number(),
2919
+ // Turns that actually ran the cheap rung, and what they cost there. Realized, not projected.
2920
+ routed: z.number(),
2921
+ routedUsd: z.number(),
2922
+ /* THE GUARDRAIL: fast-judged turns whose conversation's very next ledger row asked for a dearer model, the
2923
+ * user reaching for the model picker right after a turn the judge called simple. The strongest negative
2924
+ * signal the ledger can carry (§4's first calibration row). Past a few percent of `fast`, the judge is
2925
+ * costing more in retries and trust than it saves in tokens. */
2926
+ escalated: z.number(),
2927
+ // Fast-judged turns the user vetoed outright (UsageTurn.tierDenied): the same signal, said even louder.
2928
+ denied: z.number(),
2929
+ });
2930
+ export type TierReport = z.infer<typeof TierReportSchema>;
2931
+
2846
2932
  export const SavingsReportSchema = z.object({
2847
2933
  input: InputSavingsSchema,
2848
2934
  output: TurnExperimentSchema.optional(),
2849
2935
  search: TurnExperimentSchema.optional(),
2936
+ // Automatic tier selection's readout, see TierReportSchema. Absent ⇒ nothing was judged in the window.
2937
+ tier: TierReportSchema.optional(),
2850
2938
  });
2851
2939
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
2852
2940
 
@@ -8839,7 +8927,47 @@ export const UsageTurnSchema = z.object({
8839
8927
  .describe(
8840
8928
  "The model that actually ran, past whatever was asked for and every default. Absent only when the provider's own default served it without being named.",
8841
8929
  ),
8930
+ /* WHAT THE CLIENT ASKED FOR, beside `model` above, which is what ran. The pair is the point: a routing
8931
+ * surprise is then a diff on one row rather than an investigation through the routing code.
8932
+ *
8933
+ * The gap between the two is real and was unreadable. A pick is resolved past the tier judge's downgrade,
8934
+ * a provider's own subscription default, a catalog validity check that silently substitutes (Grok rejects
8935
+ * a retired models.dev id, so an invalid pin becomes the catalog default), and CLIProxyAPI's own choice on
8936
+ * a routed turn. Every one of those is a legitimate substitution and none of them was recorded, so "I
8937
+ * chose one model and got another's error" could only be answered by reading four resolution paths and
8938
+ * guessing which had fired.
8939
+ *
8940
+ * Absent ⇒ the client named nothing and asked for the default, which is not the same as asking for what it
8941
+ * got. Equal to `model` on the overwhelming majority of turns; the rows where they differ are the whole
8942
+ * reason this is here. */
8943
+ modelRequested: z
8944
+ .string()
8945
+ .optional()
8946
+ .describe("The model that was asked for, when one was named. Differs from `model` when something resolved it."),
8842
8947
  harness: z.string().describe("Which agentic loop it ran on."),
8948
+ /* HOW THE TURN ENDED. The field that turns the ledger from an accounting record into a diagnostic one.
8949
+ *
8950
+ * Without it a turn that died is byte-for-byte indistinguishable from one that succeeded, except that it
8951
+ * cost less, so "four sessions all broke a minute ago" had no record to read and had to be answered by
8952
+ * re-running the destructive act in a live sandbox. The failure was never nowhere: it was in the activity
8953
+ * log, which prunes to its most recent entries, so an incident survives only until the feed rolls past it.
8954
+ * This log is never pruned, which is the entire difference.
8955
+ *
8956
+ * "cancelled" is a user pressing Stop, which is not a failure and must never be read as one, the registry
8957
+ * learned that lesson already (see the abort branch in streamAgent). "error" is a turn the provider or the
8958
+ * request killed. Both still carry whatever they spent before they ended.
8959
+ *
8960
+ * Absent ⇒ the row predates this being recorded, NOT a turn that succeeded. Readers that count failures
8961
+ * must treat absent as unknown, and the experiment readers do exactly that. */
8962
+ outcome: z.enum(["ok", "error", "cancelled"]).optional().describe("How it ended: finished, failed, or was stopped by the user."),
8963
+ // The failing frame's code, when it carried one, e.g. `rate_limit`, `provider-outage`, `claude-not-entitled`.
8964
+ // Present only alongside outcome "error", and absent even then for a failure that named no code, which is
8965
+ // itself the interesting case: an unclassified failure is one nothing downstream knows how to handle.
8966
+ errorCode: z.string().optional().describe("The failure's code, when it had one."),
8967
+ // The failing frame's own sentence, capped at ERROR_MESSAGE_CHARS. Capped rather than omitted because the
8968
+ // provider's wording is routinely the only thing that distinguishes two failures sharing one code, and
8969
+ // uncapped it would let one bad provider message dominate a file that must stay cheap to read whole.
8970
+ errorMessage: z.string().optional().describe("What the failure said, trimmed."),
8843
8971
  // The conversation this turn belonged to, so spend can join to a fleet agent. Absent only for an internal
8844
8972
  // one-shot turn that has no conversation identity.
8845
8973
  conversationId: z
@@ -8942,6 +9070,24 @@ export const UsageTurnSchema = z.object({
8942
9070
  tierScore: z.number().optional(),
8943
9071
  tierRules: z.array(z.string()).optional(),
8944
9072
  tierRouted: z.boolean().optional(),
9073
+ /* THE VERDICT ITSELF, and the cutoff it was reached against.
9074
+ *
9075
+ * `tierScore` stopped being able to answer "was this called simple" the moment the cutoff became the owner's
9076
+ * to choose (settings.autoTierEagerness): 0.35 is standard on the middle stop and fast on the eager one, and
9077
+ * a fast verdict also requires a positively-easy signal that no score can express. So the answer is written
9078
+ * down rather than re-derived, and the ceiling goes with it because a refit reading a column of bare scores
9079
+ * could not otherwise tell two rows apart.
9080
+ *
9081
+ * Absent on rows written before the knob existed, which were all judged at the `balanced` cutoff — which is
9082
+ * exactly what a reader falls back to (FAST_CEILING), so old and new rows stay one population. */
9083
+ tierFast: z.boolean().optional(),
9084
+ tierCeiling: z.number().optional(),
9085
+ /* THE USER SAID NO: the turn carried AgentTurn.tierHold, so a fast verdict moved nothing. Recorded rather
9086
+ * than folded into `tierRouted: false` because it is the strongest calibration label this ledger ever gets,
9087
+ * a person looking at this very conversation deciding the cheap rung was not to be trusted with it, and the
9088
+ * refit (docs/model-routing-design.md §4) needs it kept apart from "nothing cheaper was published". Absent ⇒
9089
+ * no veto, which is every row written before the control existed and most rows after. */
9090
+ tierDenied: z.boolean().optional(),
8945
9091
  });
8946
9092
  export type UsageTurn = z.infer<typeof UsageTurnSchema>;
8947
9093
 
@@ -9043,6 +9189,54 @@ export const LogReadSchema = z.object({
9043
9189
  });
9044
9190
  export type LogRead = z.infer<typeof LogReadSchema>;
9045
9191
 
9192
+ /* ---- client diagnostics: what the BROWSER saw, landed where the daemon's own records live ----
9193
+ *
9194
+ * The one gap no daemon-side record could ever close. Everything the editor measured or caught ended at
9195
+ * `console.warn`: the perf ring buffer dies on reload, a render error reaches Vue's handler and stops there, and
9196
+ * the startup self-heal CLEARS this origin's storage and reloads, destroying the evidence for the one class of
9197
+ * bug that reproduces least often. So when a user hit a bug in their own browser the durable record was
9198
+ * nothing, and the only way to investigate was to re-drive a browser and hope. Measured over 728 sessions that
9199
+ * is 1,545 screenshots against 65 console reads, and a quarter of all prompts arriving with a picture attached
9200
+ * because there was no other channel.
9201
+ *
9202
+ * Deliberately NARROW. This is not analytics (PostHog owns product events) and not a log pipe: it carries the
9203
+ * things a person cannot describe and a screenshot cannot show, which are caught errors, self-heal wipes, and
9204
+ * spans that blew their frame budget. Everything is capped at the schema so one looping component cannot fill
9205
+ * the /history volume, and the daemon appends to logs/client.jsonl under the same prune sweep as the rest. */
9206
+ export const ClientDiagnosticSchema = z.object({
9207
+ // When the BROWSER saw it. Distinct from the `time` the daemon stamps on the record, because a batch can
9208
+ // arrive seconds late and a reader correlating a stutter to a daemon span needs the moment it happened.
9209
+ seenAt: z.number().describe("When the browser saw it, in milliseconds."),
9210
+ level: z.enum(["warn", "error"]).describe("How bad it was."),
9211
+ // A stable name, not free text: this is what a reader filters on. e.g. `vue.render`, `window.error`,
9212
+ // `unhandled.rejection`, `self-heal.wipe`, `perf.slow`.
9213
+ event: z.string().min(1).max(100).describe("What kind of thing it was, as a stable name."),
9214
+ message: z.string().max(2_000).describe("What it said."),
9215
+ // The app route the user was on, which is the single most useful field for reproducing anything.
9216
+ route: z.string().max(300).optional().describe("Which page they were on."),
9217
+ /* The id this browser put on the daemon call it was making, when there was one. The join key: without it
9218
+ * "the UI stuttered at 15:22" and "slow http.request at 15:22" can only be matched by eye. */
9219
+ requestId: z.string().max(100).optional().describe("Which daemon call it belonged to, when it belonged to one."),
9220
+ // Which build this browser was running, so a report from a tab nobody has reloaded in a week says so.
9221
+ build: z.string().max(100).optional().describe("Which build of the app was running."),
9222
+ // Bounded and primitive: a stack, an op name, a duration. Kept flat so a line stays greppable.
9223
+ fields: z
9224
+ .record(z.string().max(60), z.union([z.string().max(4_000), z.number(), z.boolean()]))
9225
+ .optional()
9226
+ .describe("Whatever else was worth keeping."),
9227
+ });
9228
+ export type ClientDiagnostic = z.infer<typeof ClientDiagnosticSchema>;
9229
+
9230
+ // A batch. Capped: the client coalesces and drops on its own side too, and a route that accepts an unbounded
9231
+ // array is a way to fill a disk with one request.
9232
+ export const ClientDiagnosticsReportSchema = z.object({
9233
+ events: z.array(ClientDiagnosticSchema).min(1).max(50).describe("What the browser has to report, oldest first."),
9234
+ });
9235
+ export type ClientDiagnosticsReport = z.infer<typeof ClientDiagnosticsReportSchema>;
9236
+ export const ClientDiagnosticsAcceptedSchema = z.object({
9237
+ recorded: z.number().describe("How many were written down."),
9238
+ });
9239
+
9046
9240
  // A tab's self-report of what it is looking at, keyed by its /events connection's clientId. Full replace,
9047
9241
  // not a merge, an absent field means "cleared", so a tab leaving a file drops the path with the same report.
9048
9242
  export const PresenceReportSchema = z.object({
package/src/starter.ts ADDED
@@ -0,0 +1,13 @@
1
+ /* THE STARTER SITE'S NAMES, declared where both sides of the wire read them.
2
+ *
3
+ * A fresh sandbox opens with one real, running thing in it: a one-page site, copied out of the image and
4
+ * started by the daemon's first boot (sandbox src/scaffold/starter-site.ts). The browser's job is the other
5
+ * half of that promise, to have it on screen when the user arrives (web shell/ShellDesktop.vue), and to do
6
+ * that it has to name the same repo and the same app the daemon seeded.
7
+ *
8
+ * Two constants rather than one because they are two different kinds of name: `site` is the repo directory a
9
+ * person would have picked, and `landing` is the template's own app name, which the apps extension, the
10
+ * preview hostname (preview-site--landing-<id>.<zone>) and the folder under `_apps/` all inherit. Renaming
11
+ * either is a product decision, not a rename: the seeded workspaces already out there keep the old names. */
12
+ export const STARTER_REPO = "site";
13
+ export const STARTER_APP = "landing";