@intentic/sandbox-contract 1.160.0 → 1.161.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/dist/contracts/agent.contract.d.ts +24 -2
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +45 -20
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +2 -2
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/usage.contract.js +2 -2
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/events.d.ts +24 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +9 -0
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +69 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +76 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +18 -3
- package/dist/schemas.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/settings.contract.ts +12 -4
- package/src/contracts/usage.contract.ts +2 -2
- package/src/events.ts +43 -5
- package/src/schemas.test.ts +7 -0
- package/src/schemas.ts +95 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/sandbox-contract",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.161.0",
|
|
4
4
|
"description": "oRPC wire contract for the intentic sandbox daemon — shared by the daemon and its browser client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@orpc/contract": "1.14.3",
|
|
46
46
|
"tslib": "2.8.1",
|
|
47
47
|
"zod": "4.4.3",
|
|
48
|
-
"@intentic/extension-api": "1.
|
|
48
|
+
"@intentic/extension-api": "1.161.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "24.13.2",
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BuiltinPromptSchema,
|
|
4
|
+
BuiltinPromptTextSchema,
|
|
5
|
+
DayWindowQuerySchema,
|
|
6
|
+
OkSchema,
|
|
7
|
+
SandboxSettingsSchema,
|
|
8
|
+
SavingsReportSchema,
|
|
9
|
+
} from "../schemas.js";
|
|
3
10
|
|
|
4
11
|
// Per-sandbox agent settings (.intentic/settings.json). `get` returns the current flags with defaults applied
|
|
5
|
-
// when the file is absent; `set` overwrites them. `savings` reports
|
|
6
|
-
//
|
|
12
|
+
// when the file is absent; `set` overwrites them. `savings` reports what each token-reduction mechanism was
|
|
13
|
+
// worth — the cleaners' realized per-command savings and the terse steer's measured A/B — over an inclusive
|
|
14
|
+
// UTC day window, the same one the spend ledger takes, so a screen can filter both with one calendar.
|
|
7
15
|
// `builtinPrompt` returns one of the two built-in system prompts as text — Intentic's own, or Claude Code's
|
|
8
16
|
// read out of the installed CLI — so the settings page can SHOW the prompt behind a mode instead of asking the
|
|
9
17
|
// user to trust a description of it, and can fork either into a custom one.
|
|
10
18
|
export const settingsContract = {
|
|
11
19
|
get: oc.route({ method: "GET", path: "/settings" }).output(SandboxSettingsSchema),
|
|
12
20
|
set: oc.route({ method: "POST", path: "/settings" }).input(SandboxSettingsSchema).output(OkSchema),
|
|
13
|
-
savings: oc.route({ method: "GET", path: "/settings/savings" }).output(
|
|
21
|
+
savings: oc.route({ method: "GET", path: "/settings/savings" }).input(DayWindowQuerySchema).output(SavingsReportSchema),
|
|
14
22
|
builtinPrompt: oc.route({ method: "GET", path: "/settings/system-prompt/{base}" }).input(BuiltinPromptSchema).output(BuiltinPromptTextSchema),
|
|
15
23
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
-
import {
|
|
2
|
+
import { DayWindowQuerySchema, UsageRollupSchema } from "../schemas.js";
|
|
3
3
|
|
|
4
4
|
// The durable spend ledger (see UsageTurnSchema). Read-only over the wire — rows are appended daemon-side at
|
|
5
5
|
// turn end, so the ledger stays a trustworthy record of what was actually spent, the same principle as the
|
|
6
6
|
// activity log. `rollup` serves every cost/usage panel: it groups by day × provider × account × model, and the
|
|
7
7
|
// browser re-projects from there (spend per day, cost by model, cache hit rate) without another round trip.
|
|
8
8
|
export const usageContract = {
|
|
9
|
-
rollup: oc.route({ method: "GET", path: "/usage/rollup" }).input(
|
|
9
|
+
rollup: oc.route({ method: "GET", path: "/usage/rollup" }).input(DayWindowQuerySchema).output(UsageRollupSchema),
|
|
10
10
|
};
|
package/src/events.ts
CHANGED
|
@@ -239,6 +239,27 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
239
239
|
// The live gate: the provider's answer to "may this turn run", pushed mid-turn. Drives the rate-limited
|
|
240
240
|
// notice, not the headroom readouts — see RateLimitInfoSchema.
|
|
241
241
|
RateLimitInfoSchema.extend({ kind: z.literal("rate_limit_info"), account: z.string().optional() }),
|
|
242
|
+
/* The turn is alive but WAITING on the provider: a request failed transiently (5xx, 529, a dropped socket)
|
|
243
|
+
* and the harness is retrying it inside this same turn. A status, not a failure — nothing has been lost and
|
|
244
|
+
* the turn may still finish normally, so the client renders it where "thinking" goes rather than in the
|
|
245
|
+
* transcript.
|
|
246
|
+
*
|
|
247
|
+
* It exists because the retry budget is deliberately long (see CLAUDE_CODE_RETRY_WATCHDOG in
|
|
248
|
+
* harness-credentials.ts): a turn can now sit silent for minutes riding out an outage, and silence reads as
|
|
249
|
+
* a hang. The one action a user takes against an apparent hang is Stop, which is the only action that
|
|
250
|
+
* actually loses the work — so the wait has to be visible, with its own next-attempt clock.
|
|
251
|
+
*
|
|
252
|
+
* `attempt`/`maxAttempts` are the harness's own counters; `nextAttemptAt` (epoch ms) is when it will try
|
|
253
|
+
* again, so the readout counts down instead of freezing on a number nobody can interpret. */
|
|
254
|
+
z.object({
|
|
255
|
+
kind: z.literal("provider_retry"),
|
|
256
|
+
attempt: z.number(),
|
|
257
|
+
maxAttempts: z.number(),
|
|
258
|
+
nextAttemptAt: z.number(),
|
|
259
|
+
// The HTTP status behind it when there was one (529 reads as capacity, 500 as a fault — the client says
|
|
260
|
+
// which). Absent for a transport failure that never got a response.
|
|
261
|
+
status: z.number().optional(),
|
|
262
|
+
}),
|
|
242
263
|
// Every plan-limit pool for the account that served the turn, read from the CLI's usage endpoint once the
|
|
243
264
|
// turn settles. `account` tags which Claude account it belongs to, so the client keys headroom by account;
|
|
244
265
|
// absent on an env-token turn, which has no account to attribute it to. No `measuredAt` on the wire: both
|
|
@@ -291,6 +312,12 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
291
312
|
// that resumed itself, not a request for the user to do anything. It only reaches the client
|
|
292
313
|
// when the resume could NOT start, which is when reconnecting really is the fix.
|
|
293
314
|
"claude-token-refused",
|
|
315
|
+
// The model provider itself failed transiently — 500/502/503, a 529 at capacity, a dropped
|
|
316
|
+
// socket — and the harness's own in-turn retries did not outlast it. Nothing about the workspace
|
|
317
|
+
// or the request is wrong, so the daemon remembers the turn and re-runs it on an escalating
|
|
318
|
+
// backoff (provider-health.ts): the frame is a notice about a turn that is coming back, and
|
|
319
|
+
// reaches the client as a plain failure only once the attempts are spent.
|
|
320
|
+
"provider-outage",
|
|
294
321
|
// The harness read the message as a slash command it doesn't have, and discarded everything
|
|
295
322
|
// after the name — the model never saw the message. Nothing was processed, so the client holds
|
|
296
323
|
// the text back instead of leaving the user to retype it (same treatment as claude-reauth).
|
|
@@ -302,13 +329,24 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
302
329
|
])
|
|
303
330
|
.optional(),
|
|
304
331
|
// rate_limit only: when the exhausted window reopens (epoch seconds, from the stream's own
|
|
305
|
-
// rate_limit_event or the account's persisted usage windows)
|
|
306
|
-
//
|
|
307
|
-
// "available" = the daemon remembered the failed turn and enabling the autoResumeOnLimit setting
|
|
308
|
-
// arms that same resume, which is what the chat's offer banner hangs off. Absent together when the
|
|
309
|
-
// reset instant is unknown (nothing to schedule against).
|
|
332
|
+
// rate_limit_event or the account's persisted usage windows). Absent when the reset instant is unknown
|
|
333
|
+
// (nothing to schedule against).
|
|
310
334
|
resetsAt: z.number().optional(),
|
|
335
|
+
// Where the daemon's resume of THIS turn stands — the same two states for a spent allowance and for a
|
|
336
|
+
// provider outage, because the client's reading of them is the same: "scheduled" = the resume is armed
|
|
337
|
+
// and this turn comes back by itself; "available" = the daemon remembered the failed turn and turning
|
|
338
|
+
// the setting on (autoResumeOnLimit / resumeAfterOutage) arms that same resume, which is what the
|
|
339
|
+
// chat's offer banner hangs off. Absent ⇒ there is nothing to resume.
|
|
311
340
|
autoResume: z.enum(["scheduled", "available"]).optional(),
|
|
341
|
+
/* provider-outage only: the shape of the wait. `retryAt` (epoch seconds) is when the next attempt is
|
|
342
|
+
* due — not a fixed cadence, because an outage has no reset instant to aim at and hammering a provider
|
|
343
|
+
* that is down only spends tokens on refusals, so each attempt waits longer than the last
|
|
344
|
+
* (provider-health.ts owns the schedule).
|
|
345
|
+
*
|
|
346
|
+
* `attempt`/`maxAttempts` are on the wire so the notice can say the automation is BOUNDED. An
|
|
347
|
+
* on-by-default retry that gives no account of how long it will keep going is the kind users switch off
|
|
348
|
+
* defensively; one that says "attempt 2 of 6" is one they leave on. */
|
|
349
|
+
outage: z.object({ retryAt: z.number(), attempt: z.number(), maxAttempts: z.number() }).optional(),
|
|
312
350
|
// rate_limit only: the account whose allowance is spent, as the DAEMON resolved it (the client's own
|
|
313
351
|
// selection can be empty, which means "the provider's first"). It is what lets the chat offer the
|
|
314
352
|
// provider's OTHER accounts as a resume-now instead of a wait — see /agent/resume-limit.
|
package/src/schemas.test.ts
CHANGED
|
@@ -20,6 +20,7 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
20
20
|
};
|
|
21
21
|
expect(SandboxSettingsSchema.parse(older)).toEqual({
|
|
22
22
|
...older,
|
|
23
|
+
terseHoldout: 0,
|
|
23
24
|
filterBackend: "native",
|
|
24
25
|
systemPromptMode: "intentic",
|
|
25
26
|
systemPrompt: "",
|
|
@@ -27,6 +28,7 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
27
28
|
agentRetentionDays: 3,
|
|
28
29
|
autoLand: true,
|
|
29
30
|
autoResumeOnLimit: false,
|
|
31
|
+
resumeAfterOutage: true,
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
|
|
@@ -36,6 +38,8 @@ test("an empty object is the full default settings object", () => {
|
|
|
36
38
|
skills: [],
|
|
37
39
|
hashlineEdits: false,
|
|
38
40
|
terseOutput: false,
|
|
41
|
+
// Off: the steer's turn-level control spends the tokens it measures, so measuring is opt-in.
|
|
42
|
+
terseHoldout: 0,
|
|
39
43
|
iqSearch: false,
|
|
40
44
|
outputCleaners: "off",
|
|
41
45
|
outputHoldout: 0,
|
|
@@ -54,6 +58,9 @@ test("an empty object is the full default settings object", () => {
|
|
|
54
58
|
// sandbox's finished work on branches nobody is watching.
|
|
55
59
|
autoLand: true,
|
|
56
60
|
autoResumeOnLimit: false,
|
|
61
|
+
// On, unlike the limit resume beside it: an outage resume spends nothing the dead turn hadn't already
|
|
62
|
+
// committed, and the turns it saves are the unattended ones nobody is watching to restart by hand.
|
|
63
|
+
resumeAfterOutage: true,
|
|
57
64
|
});
|
|
58
65
|
});
|
|
59
66
|
|
package/src/schemas.ts
CHANGED
|
@@ -648,6 +648,14 @@ export const SandboxSettingsSchema = z.object({
|
|
|
648
648
|
skills: z.array(z.string()).default([]),
|
|
649
649
|
hashlineEdits: z.boolean().default(false),
|
|
650
650
|
terseOutput: z.boolean().default(false),
|
|
651
|
+
/* Measurement control for the terse steer, at TURN level — the same trick `outputHoldout` plays over
|
|
652
|
+
* commands, one layer up. A fraction [0,1] of otherwise-eligible turns run WITHOUT the steer and record
|
|
653
|
+
* which arm they ran on (UsageTurn.terse), so the savings report can compare two real populations.
|
|
654
|
+
*
|
|
655
|
+
* It has to be an experiment: unlike a cleaned command, which yields its own raw baseline in the same
|
|
656
|
+
* event, a turn cannot be re-run to see what it would have said unsteered. 0 ⇒ no measurement (every
|
|
657
|
+
* eligible turn is steered), which is the default because the control costs the very tokens it measures. */
|
|
658
|
+
terseHoldout: z.number().min(0).max(1).default(0),
|
|
651
659
|
/* WHICH SYSTEM PROMPT THE AGENT RUNS ON — the base, before anything this turn composes.
|
|
652
660
|
*
|
|
653
661
|
* intentic — Intentic's own prompt, tuned for this harness (intentic-prompt.ts). The default.
|
|
@@ -697,6 +705,16 @@ export const SandboxSettingsSchema = z.object({
|
|
|
697
705
|
// records every limit-hit either way and the chat OFFERS the toggle at the moment it would have helped —
|
|
698
706
|
// enabling it then still resumes the turn that just bounced.
|
|
699
707
|
autoResumeOnLimit: z.boolean().default(false),
|
|
708
|
+
/* When a turn dies because the MODEL PROVIDER was failing (500/502/503, a 529 at capacity, a dropped
|
|
709
|
+
* socket), re-run it on an escalating backoff until it goes through or the attempts are spent.
|
|
710
|
+
*
|
|
711
|
+
* Defaults ON, unlike autoResumeOnLimit, and the difference is not an inconsistency: a spent allowance is
|
|
712
|
+
* the user's own budget, and resuming into a fresh window spends something they may have been saving. An
|
|
713
|
+
* outage resume spends nothing the dead turn had not already committed, resolves in minutes rather than
|
|
714
|
+
* hours, and — the deciding argument — the turns hurt worst by it are the ones with nobody in the room
|
|
715
|
+
* (automation wakes, Discord, webhooks), which no browser-held preference could ever rescue. It is the same
|
|
716
|
+
* reasoning that leaves the auth resume ungated: this is the provider's failure, not the user's decision. */
|
|
717
|
+
resumeAfterOutage: z.boolean().default(true),
|
|
700
718
|
});
|
|
701
719
|
export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
702
720
|
|
|
@@ -710,22 +728,43 @@ export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
|
710
728
|
export const BuiltinPromptTextSchema = z.object({ text: z.string(), version: z.string() });
|
|
711
729
|
export type BuiltinPromptText = z.infer<typeof BuiltinPromptTextSchema>;
|
|
712
730
|
|
|
713
|
-
|
|
714
|
-
|
|
731
|
+
/* ---- savings report: what each token-reduction mechanism actually saved ----
|
|
732
|
+
*
|
|
733
|
+
* TWO FAMILIES, deliberately never one list of bars. They are measured differently, and a chart that ranks
|
|
734
|
+
* them side by side claims a confidence and a denominator that only one of them has:
|
|
735
|
+
*
|
|
736
|
+
* input — shell output the cleaners trimmed before the model ever saw it. Both sides of the comparison come
|
|
737
|
+
* off the SAME command (raw in, emitted out), so the counterfactual is observed rather than
|
|
738
|
+
* estimated: exact, per command, no sample size to argue about.
|
|
739
|
+
* output — the model's own tokens under the terse steer. There is no second run of the same turn to compare
|
|
740
|
+
* against, so the only honest number is an experiment: a turn-level holdout, an n per arm, and a
|
|
741
|
+
* margin. It is absent entirely until both arms are large enough for the delta to mean anything.
|
|
742
|
+
*
|
|
743
|
+
* The two are also in different units of value — a saved tool-output token is saved again on every later
|
|
744
|
+
* request of that conversation, an output token is saved once but costs several times as much — which is the
|
|
745
|
+
* other reason they are separate sections with separate totals rather than one number.
|
|
746
|
+
*/
|
|
747
|
+
|
|
748
|
+
// One mechanism's realized saving, biggest first. `savedTokens` is what THIS stage removed from what reached
|
|
749
|
+
// it in pipeline order — sequential attribution, which is why the stages sum exactly to raw − emitted and can
|
|
750
|
+
// be drawn as one stacked bar. It is NOT "what turning this cleaner off would cost you": the cap downstream
|
|
751
|
+
// would have eaten some of the same lines. `commands` is how many commands the stage ran on. Negative for the
|
|
752
|
+
// `footer` stage, which adds the retrieval pointer back — a cost on the same ledger as what it bought.
|
|
753
|
+
export const SavingsStageSchema = z.object({ id: z.string(), commands: z.number(), savedTokens: z.number() });
|
|
754
|
+
|
|
755
|
+
// Whichever cleaner is ACTUALLY compressing output owns these numbers, so they are read from that backend's
|
|
715
756
|
// own ledger: "native" aggregates historyRoot/logs/filter-stats.jsonl (one row per agent Bash command, written
|
|
716
757
|
// by agent-output-filter), "rtk" reads rtk's own gain ledger. Reading one ledger regardless of backend is how
|
|
717
758
|
// this card went stale: under rtk the native filter is switched off, nothing appends, and the last numbers
|
|
718
759
|
// written — a test run's, as it happened — sat on the card looking live.
|
|
719
|
-
|
|
720
|
-
// `perCleaner` attributes which cleaner ids fired across commands; `holdout` is the measured control (commands
|
|
721
|
-
// the holdout bypassed) vs the cleaned population — a real saved-% rather than an estimate; `gaps` are
|
|
722
|
-
// high-volume commands that matched no cleaner (the next handler to write). All three are native-only: rtk
|
|
723
|
-
// reports totals, not per-command attribution, so they arrive empty under that backend. Empty/zeroed when no
|
|
724
|
-
// commands have run yet.
|
|
725
|
-
export const CleanerSavingsSchema = z.object({
|
|
760
|
+
export const InputSavingsSchema = z.object({
|
|
726
761
|
// Which backend's ledger these numbers came from — shown on the card, because a number without its source
|
|
727
762
|
// cannot be told apart from a stale one. Defaulted for the same daemon-older-than-browser seam as settings.
|
|
728
763
|
source: z.enum(["native", "rtk"]).default("native"),
|
|
764
|
+
// False ⇒ these totals cover the ledger's whole life, not the range the reader selected: `rtk gain` reports
|
|
765
|
+
// no timestamps, so its numbers cannot be windowed and the screen has to say so rather than let a 7-day
|
|
766
|
+
// filter sit above an all-time figure.
|
|
767
|
+
windowed: z.boolean(),
|
|
729
768
|
// When that ledger last recorded a command (epoch ms), so the card can show its age instead of implying
|
|
730
769
|
// freshness it doesn't have. Absent when the ledger has never been written or its age can't be read.
|
|
731
770
|
updatedAt: z.number().optional(),
|
|
@@ -733,11 +772,43 @@ export const CleanerSavingsSchema = z.object({
|
|
|
733
772
|
rawTokens: z.number(),
|
|
734
773
|
emittedTokens: z.number(),
|
|
735
774
|
savedPct: z.number(),
|
|
736
|
-
|
|
775
|
+
// Per-stage attribution, biggest first. Native-only: rtk reports totals, not which of its handlers fired.
|
|
776
|
+
perCleaner: z.array(SavingsStageSchema),
|
|
777
|
+
// The measured control — commands the holdout left raw — against the cleaned population. A real saved-%
|
|
778
|
+
// for the pipeline as a whole rather than an estimate, and the only whole-pipeline counterfactual there is.
|
|
737
779
|
holdout: z.object({ cleaned: z.number(), heldOut: z.number(), measuredSavedPct: z.number().optional() }),
|
|
780
|
+
// High-volume commands that matched no cleaner: where the next handler is worth writing. Native-only.
|
|
738
781
|
gaps: z.array(z.object({ command: z.string(), tokens: z.number() })),
|
|
739
782
|
});
|
|
740
|
-
export type
|
|
783
|
+
export type InputSavings = z.infer<typeof InputSavingsSchema>;
|
|
784
|
+
|
|
785
|
+
// One arm of the turn-level experiment: the turns that ran with the steer, and the turns the holdout ran
|
|
786
|
+
// without it. Mean output tokens PER TURN, because the arms never hold the same number of turns.
|
|
787
|
+
export const SavingsArmSchema = z.object({ turns: z.number(), meanOutputTokens: z.number() });
|
|
788
|
+
|
|
789
|
+
// The terse steer, as measured. Only turns where the steer was ELIGIBLE are counted — a turn under a custom
|
|
790
|
+
// system prompt drops the steer along with everything else the daemon appends, so it belongs to neither arm.
|
|
791
|
+
export const OutputSavingsSchema = z.object({
|
|
792
|
+
on: SavingsArmSchema,
|
|
793
|
+
off: SavingsArmSchema,
|
|
794
|
+
// Turns per arm before a delta is reported at all. Carried on the wire so the screen's "measuring…" state
|
|
795
|
+
// counts toward the daemon's real threshold instead of a number the browser guessed.
|
|
796
|
+
minTurns: z.number(),
|
|
797
|
+
/* The three below are present TOGETHER, and only once both arms clear `minTurns` — a schema that can't
|
|
798
|
+
* express a half-measured experiment is how a 34%-that-becomes-8%-tomorrow never reaches the screen.
|
|
799
|
+
* deltaPct — change in mean output tokens per turn under the steer; negative is a saving.
|
|
800
|
+
* marginPct — ± percentage points, 95% (Welch, unequal variances and unequal arms).
|
|
801
|
+
* savedTokens — what the delta is worth over the turns that actually ran with the steer, in this window. */
|
|
802
|
+
deltaPct: z.number().optional(),
|
|
803
|
+
marginPct: z.number().optional(),
|
|
804
|
+
savedTokens: z.number().optional(),
|
|
805
|
+
});
|
|
806
|
+
export type OutputSavings = z.infer<typeof OutputSavingsSchema>;
|
|
807
|
+
|
|
808
|
+
// `output` is absent when the experiment isn't running at all (terse off, or no holdout set) — a section that
|
|
809
|
+
// isn't there reads as "not measured", which is the truth, while zeros would read as "measured, worth nothing".
|
|
810
|
+
export const SavingsReportSchema = z.object({ input: InputSavingsSchema, output: OutputSavingsSchema.optional() });
|
|
811
|
+
export type SavingsReport = z.infer<typeof SavingsReportSchema>;
|
|
741
812
|
|
|
742
813
|
// ---- intentic CLI ----
|
|
743
814
|
|
|
@@ -2260,6 +2331,14 @@ export const UsageTurnSchema = z.object({
|
|
|
2260
2331
|
cacheCreationTokens: z.number(),
|
|
2261
2332
|
costUsd: z.number(),
|
|
2262
2333
|
durationMs: z.number(),
|
|
2334
|
+
/* Which arm of the terse experiment this turn ran on (settings.terseHoldout) — the only record of it, and
|
|
2335
|
+
* the reason the savings report can say what the steer is worth instead of guessing.
|
|
2336
|
+
*
|
|
2337
|
+
* ABSENT means "not part of the experiment", not "off": a turn under a custom system prompt drops the
|
|
2338
|
+
* steer along with everything else the daemon appends, and a turn run with the experiment switched off has
|
|
2339
|
+
* no control to be compared against. Pooling those into the off-arm would compare steered turns against a
|
|
2340
|
+
* population selected by something other than the coin flip, which is not a control at all. */
|
|
2341
|
+
terse: z.boolean().optional(),
|
|
2263
2342
|
});
|
|
2264
2343
|
export type UsageTurn = z.infer<typeof UsageTurnSchema>;
|
|
2265
2344
|
|
|
@@ -2287,12 +2366,14 @@ export const UsageRollupRowSchema = z.object({
|
|
|
2287
2366
|
durationMs: z.number(),
|
|
2288
2367
|
});
|
|
2289
2368
|
export type UsageRollupRow = z.infer<typeof UsageRollupRowSchema>;
|
|
2290
|
-
// Inclusive UTC day bounds (YYYY-MM-DD). Both absent ⇒ the whole ledger.
|
|
2291
|
-
|
|
2369
|
+
// Inclusive UTC day bounds (YYYY-MM-DD). Both absent ⇒ the whole ledger. Shared by every windowed read of a
|
|
2370
|
+
// daemon ledger (spend, savings): one window shape, so a screen that filters two ledgers at once filters them
|
|
2371
|
+
// with the same calendar.
|
|
2372
|
+
export const DayWindowQuerySchema = z.object({
|
|
2292
2373
|
from: z.string().optional(),
|
|
2293
2374
|
to: z.string().optional(),
|
|
2294
2375
|
});
|
|
2295
|
-
export type
|
|
2376
|
+
export type DayWindowQuery = z.infer<typeof DayWindowQuerySchema>;
|
|
2296
2377
|
export const UsageRollupSchema = z.object({ rows: z.array(UsageRollupRowSchema) });
|
|
2297
2378
|
|
|
2298
2379
|
// ---- usage: per-account token/cost totals ----
|