@intentic/sandbox-contract 1.309.0 → 1.311.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -159
- package/dist/contracts/accounts.contract.d.ts +3 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +15 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +596 -1
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +13 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +2 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +1 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +45 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +11 -15
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +59 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +20 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +6 -1
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/system-events.d.ts +48 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +5 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +9 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/front/generated/wire.d.ts +74 -0
- package/dist/front/generated/wire.d.ts.map +1 -0
- package/dist/front/generated/wire.js +2 -0
- package/dist/front/generated/wire.js.map +1 -0
- package/dist/index.d.ts +717 -74
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/policy/command-run.d.ts +1 -1
- package/dist/policy/command-run.d.ts.map +1 -1
- package/dist/policy/command-run.js +7 -4
- package/dist/policy/command-run.js.map +1 -1
- package/dist/protocol/ingress-protocol.d.ts.map +1 -1
- package/dist/protocol/ingress-protocol.js +15 -0
- package/dist/protocol/ingress-protocol.js.map +1 -1
- package/dist/protocol/upgrade-relay.d.ts +4 -0
- package/dist/protocol/upgrade-relay.d.ts.map +1 -0
- package/dist/protocol/upgrade-relay.js +25 -0
- package/dist/protocol/upgrade-relay.js.map +1 -0
- package/dist/schemas/agents.d.ts +72 -3
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +10 -7
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +24 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +3 -0
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +4 -0
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/devices.d.ts +32 -6
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +8 -6
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +68 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -0
- package/dist/schemas/keep-warm.js +74 -0
- package/dist/schemas/keep-warm.js.map +1 -0
- package/dist/schemas/metrics.d.ts +24 -25
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +6 -31
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +3 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +4 -0
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/usage.d.ts +6 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +4 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/settings.d.ts +13 -29
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +48 -11
- package/dist/schemas/settings.js.map +1 -1
- package/dist/state/definition.d.ts +24 -20
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/fix-stance.d.ts.map +1 -1
- package/dist/state/fix-stance.js +1 -0
- package/dist/state/fix-stance.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +0 -7
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +0 -8
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +2 -0
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +20 -7
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/time/zone.d.ts +1 -0
- package/dist/time/zone.d.ts.map +1 -1
- package/dist/time/zone.js +5 -0
- package/dist/time/zone.js.map +1 -1
- package/package.json +27 -5
- package/src/chores/chores.test.ts +0 -1
- package/src/chores/digest.test.ts +0 -1
- package/src/chores/probes.test.ts +0 -1
- package/src/chores/stack.test.ts +0 -1
- package/src/chores/verdict.test.ts +0 -1
- package/src/contracts/agents.contract.ts +14 -2
- package/src/events/agent-events.ts +9 -1
- package/src/events/agent-words.test.ts +0 -1
- package/src/events/resume.test.ts +0 -1
- package/src/events/transcript.ts +14 -3
- package/src/events/watch-wake.test.ts +0 -1
- package/src/front/generated/wire.ts +73 -0
- package/src/ids/conversation-ids.test.ts +1 -2
- package/src/ids/hostnames.fixture.json +20 -0
- package/src/ids/hostnames.test.ts +20 -1
- package/src/ids/share-paths.test.ts +0 -1
- package/src/ids/tunnel-ids.test.ts +0 -1
- package/src/index.ts +1 -3
- package/src/models/agent-catalog.test.ts +0 -1
- package/src/models/capability-ledger.test.ts +0 -1
- package/src/models/model-label.test.ts +0 -1
- package/src/models/model-offer.test.ts +0 -1
- package/src/models/model-order.test.ts +0 -1
- package/src/models/model-pins.test.ts +0 -1
- package/src/models/model-roles.test.ts +0 -1
- package/src/models/plan-pools.test.ts +0 -1
- package/src/models/provider-specs.test.ts +0 -1
- package/src/policy/batch-runs.test.ts +0 -1
- package/src/policy/command-classes.test.ts +0 -1
- package/src/policy/command-run.test.ts +35 -0
- package/src/policy/command-run.ts +10 -5
- package/src/policy/credential-material.test.ts +0 -1
- package/src/policy/fence-paths.test.ts +0 -1
- package/src/policy/overlay-lint.test.ts +0 -1
- package/src/policy/owner-ticket.test.ts +0 -1
- package/src/policy/persona-home.test.ts +0 -1
- package/src/policy/reserved-servers.test.ts +0 -1
- package/src/policy/safety-policy.test.ts +0 -1
- package/src/policy/search-globs.test.ts +0 -1
- package/src/protocol/container-requirements.test.ts +0 -1
- package/src/protocol/ingress-contract.test.ts +0 -1
- package/src/protocol/ingress-protocol.test.ts +45 -2
- package/src/protocol/ingress-protocol.ts +25 -4
- package/src/protocol/peer-dial.test.ts +1 -2
- package/src/protocol/peer-mcp-server.test.ts +0 -1
- package/src/protocol/routes.test.ts +6 -3
- package/src/protocol/tunnel-heartbeat.test.ts +2 -3
- package/src/protocol/upgrade-relay.ts +28 -0
- package/src/schemas/agent.test.ts +0 -1
- package/src/schemas/agents.ts +14 -7
- package/src/schemas/areas.test.ts +1 -2
- package/src/schemas/capabilities.ts +6 -0
- package/src/schemas/ci.ts +3 -9
- package/src/schemas/context-trim.test.ts +0 -1
- package/src/schemas/dev-rebuild-log.test.ts +0 -1
- package/src/schemas/devices.ts +16 -9
- package/src/schemas/git/git.test.ts +0 -1
- package/src/schemas/keep-warm.test.ts +68 -0
- package/src/schemas/keep-warm.ts +124 -0
- package/src/schemas/metrics.ts +33 -64
- package/src/schemas/providers/provider-oauth.ts +6 -0
- package/src/schemas/providers/usage.ts +7 -0
- package/src/schemas/settings.test.ts +41 -7
- package/src/schemas/settings.ts +78 -44
- package/src/schemas/version-seam.test.ts +0 -1
- package/src/state/contract-lock.test.ts +0 -1
- package/src/state/fix-attempt-plan.test.ts +0 -1
- package/src/state/fix-stance.test.ts +14 -1
- package/src/state/fix-stance.ts +2 -0
- package/src/state/landed-commit.test.ts +0 -1
- package/src/state/runtime-state.test.ts +0 -1
- package/src/state/runtime-state.ts +4 -0
- package/src/state/versions.test.ts +0 -1
- package/src/state/workspace-state.test.ts +0 -3
- package/src/state/workspace-state.ts +0 -14
- package/src/text/archives.test.ts +0 -1
- package/src/text/documents.test.ts +0 -1
- package/src/text/embed.test.ts +2 -3
- package/src/text/emoji.test.ts +0 -1
- package/src/text/mentions.test.ts +0 -1
- package/src/text/path-refs.test.ts +0 -1
- package/src/text/title.test.ts +0 -1
- package/src/text/transcript-fold.test.ts +32 -4
- package/src/text/transcript-fold.ts +39 -9
- package/src/text/whisper.test.ts +0 -1
- package/src/text/workflow-faults.test.ts +0 -1
- package/src/time/zone.test.ts +0 -1
- package/src/time/zone.ts +7 -0
- package/src/webext/index.ts +1 -1
- package/dist/contracts/prepush.contract.d.ts +0 -32
- package/dist/contracts/prepush.contract.d.ts.map +0 -1
- package/dist/contracts/prepush.contract.js +0 -41
- package/dist/contracts/prepush.contract.js.map +0 -1
- package/src/contracts/prepush.contract.ts +0 -58
package/src/schemas/devices.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { DEV_VERSION } from "../state/versions.js";
|
|
|
9
9
|
// One sandbox's resource share as docker currently enforces it, read off the container by the machine agent.
|
|
10
10
|
// `overlayRuntime` is the environment's locked demand; `hostRuntime` is the owner's addition; `privileged`/`gpu` are
|
|
11
11
|
// docker's enforced truth.
|
|
12
|
+
// Reshape request, turned into `ic sandbox reshape` flags: absent means leave it, `null` on the two caps means back to
|
|
13
|
+
// the default; at least one key must be set.
|
|
14
|
+
export const SandboxResourcesAskFieldsSchema = z.object({
|
|
15
|
+
memoryGib: z.int().positive().nullable().optional(),
|
|
16
|
+
cpus: z.int().positive().nullable().optional(),
|
|
17
|
+
privileged: z.boolean().optional(),
|
|
18
|
+
gpu: z.boolean().optional(),
|
|
19
|
+
});
|
|
12
20
|
export const SandboxResourcesSchema = z.object({
|
|
13
21
|
// The cgroup memory ceiling in bytes; absent when docker imposes none (the hosted shape).
|
|
14
22
|
memoryBytes: z.number().optional(),
|
|
@@ -18,17 +26,12 @@ export const SandboxResourcesSchema = z.object({
|
|
|
18
26
|
gpu: z.boolean(),
|
|
19
27
|
hostRuntime: z.array(z.string()),
|
|
20
28
|
overlayRuntime: z.array(z.string()),
|
|
29
|
+
// A reshape saved for the sandbox's next restart (`ic sandbox reshape --later`) and not yet in force: the next
|
|
30
|
+
// update, rollback, rebuild, reshape or Restart applies it. Absent when nothing is saved.
|
|
31
|
+
saved: SandboxResourcesAskFieldsSchema.optional(),
|
|
21
32
|
});
|
|
22
33
|
export type SandboxResources = z.infer<typeof SandboxResourcesSchema>;
|
|
23
34
|
|
|
24
|
-
// Reshape request, turned into `ic sandbox reshape` flags: absent means leave it, `null` on the two caps means back to
|
|
25
|
-
// the default; at least one key must be set.
|
|
26
|
-
export const SandboxResourcesAskFieldsSchema = z.object({
|
|
27
|
-
memoryGib: z.int().positive().nullable().optional(),
|
|
28
|
-
cpus: z.int().positive().nullable().optional(),
|
|
29
|
-
privileged: z.boolean().optional(),
|
|
30
|
-
gpu: z.boolean().optional(),
|
|
31
|
-
});
|
|
32
35
|
// Fields also exported separately for callers that compose them and enforce the at-least-one rule themselves.
|
|
33
36
|
export const SandboxResourcesAskSchema = SandboxResourcesAskFieldsSchema.refine((ask) => Object.values(ask).some((value) => value !== undefined), {
|
|
34
37
|
message: "a reshape must change at least one thing",
|
|
@@ -81,8 +84,12 @@ export const DeviceSandboxFlowSchema = z.object({
|
|
|
81
84
|
slug: z.string().min(1),
|
|
82
85
|
// Approved overlay's sha256, required only by `rebuild`; only content matching it is ever built.
|
|
83
86
|
hash: z.string().optional(),
|
|
84
|
-
// What `reshape` should change,
|
|
87
|
+
// What `reshape` should change, meaningless to the rest. Absent on a reshape applies what is saved for the next
|
|
88
|
+
// restart now; absent with `later` forgets what is saved.
|
|
85
89
|
resources: SandboxResourcesAskSchema.optional(),
|
|
90
|
+
// `reshape` only: save `resources` for the sandbox's next restart instead of restarting it now, replacing
|
|
91
|
+
// whatever was saved before (`ic sandbox reshape --later`).
|
|
92
|
+
later: z.boolean().optional(),
|
|
86
93
|
// `runner-up` only, daemon-filled, never by the caller: the browser never holds the pairing credential.
|
|
87
94
|
parentUrl: z.string().optional(),
|
|
88
95
|
pair: z.string().optional().meta({ secret: true }),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TranscriptFold } from "../text/transcript-fold.js";
|
|
2
|
+
import {
|
|
3
|
+
changedParts,
|
|
4
|
+
keepWarmCap,
|
|
5
|
+
keepWarmDueAt,
|
|
6
|
+
keepWarmHorizon,
|
|
7
|
+
keepWarmLeadMs,
|
|
8
|
+
keepWarmMaxRefreshes,
|
|
9
|
+
keepWarmRefreshes,
|
|
10
|
+
keptWarmLine,
|
|
11
|
+
} from "./keep-warm.js";
|
|
12
|
+
|
|
13
|
+
const MINUTE = 60_000;
|
|
14
|
+
const HOUR = 60 * MINUTE;
|
|
15
|
+
|
|
16
|
+
// The arithmetic both the daemon and every press read, so the price shown before a press is the price the daemon keeps.
|
|
17
|
+
describe("keep-warm economics", () => {
|
|
18
|
+
test("refreshes a fifth of an entry's life early, never under a minute or over ten", () => {
|
|
19
|
+
expect(keepWarmLeadMs(HOUR)).toBe(10 * MINUTE);
|
|
20
|
+
expect(keepWarmLeadMs(5 * MINUTE)).toBe(MINUTE);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("spends at most half of what a cold resume would rewrite", () => {
|
|
24
|
+
// A 1h rewrite costs 2x base input and a read 0.1x: nineteen reads break even, so nine.
|
|
25
|
+
expect(keepWarmMaxRefreshes(HOUR)).toBe(9);
|
|
26
|
+
// A 5m rewrite costs 1.25x: eleven reads break even, so five.
|
|
27
|
+
expect(keepWarmMaxRefreshes(5 * MINUTE)).toBe(5);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("counts the refreshes that keep an entry alive through the deadline", () => {
|
|
31
|
+
const cache = { at: 0, ttlMs: HOUR };
|
|
32
|
+
expect(keepWarmRefreshes(cache, HOUR)).toBe(0);
|
|
33
|
+
expect(keepWarmRefreshes(cache, 4 * HOUR)).toBe(4);
|
|
34
|
+
expect(keepWarmDueAt(cache)).toBe(50 * MINUTE);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("reaches no further than the horizon, or the date change when that comes first", () => {
|
|
38
|
+
const cache = { at: 0, ttlMs: HOUR };
|
|
39
|
+
expect(keepWarmHorizon(cache)).toBe(HOUR + 9 * 50 * MINUTE);
|
|
40
|
+
expect(keepWarmCap(cache, 3 * HOUR)).toBe(3 * HOUR);
|
|
41
|
+
expect(keepWarmCap({ at: 0, ttlMs: 5 * MINUTE }, undefined)).toBe(5 * MINUTE + 5 * 4 * MINUTE);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("names the parts two fingerprints disagree on", () => {
|
|
45
|
+
const before = { hash: "a", parts: { version: "1", model: "m", day: "d1" } };
|
|
46
|
+
expect(changedParts(before, { hash: "b", parts: { version: "2", model: "m", day: "d2" } })).toEqual(["day", "version"]);
|
|
47
|
+
expect(changedParts(before, before)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("the receipt a kept conversation's turn leaves", () => {
|
|
52
|
+
test("says it picked up warm, and for how long it was kept", () => {
|
|
53
|
+
expect(keptWarmLine({ readTokens: 251_000, writtenTokens: 3_000, kept: { forMs: 3 * HOUR + 12 * MINUTE, refreshes: 4 } })).toBe(
|
|
54
|
+
"Picked up warm: 251k tokens read from the cache, kept warm for 3h 12m with 4 refreshes.",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("says so plainly when the turn went cold anyway", () => {
|
|
59
|
+
expect(keptWarmLine({ readTokens: 0, writtenTokens: 250_000, kept: { forMs: 40 * MINUTE, refreshes: 1 } })).toContain("Picked up cold");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("is a notice row in the transcript, and nothing for a turn nobody kept", () => {
|
|
63
|
+
const fold = new TranscriptFold([]);
|
|
64
|
+
expect(fold.apply({ kind: "prompt_cache", readTokens: 10, writtenTokens: 200_000 })).toEqual([]);
|
|
65
|
+
fold.apply({ kind: "prompt_cache", readTokens: 251_000, writtenTokens: 3_000, kept: { forMs: HOUR, refreshes: 1 } });
|
|
66
|
+
expect(fold.rows.at(-1)).toMatchObject({ role: "notice", text: expect.stringContaining("Picked up warm") });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// keep-warm: holding an idle conversation's prompt cache open by re-reading it shortly before it expires.
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const HOUR_MS = 3_600_000;
|
|
6
|
+
|
|
7
|
+
// Anthropic's price multipliers on base input: a read costs 0.1x, a write 2x under the 1h TTL and 1.25x under 5m.
|
|
8
|
+
const READ_COST = 0.1;
|
|
9
|
+
const writeCost = (ttlMs: number): number => (ttlMs >= HOUR_MS ? 2 : 1.25);
|
|
10
|
+
|
|
11
|
+
// A cache clock as the daemon publishes it: the last request that touched the entry, and the entry's lifetime.
|
|
12
|
+
export interface CacheClock {
|
|
13
|
+
readonly at: number;
|
|
14
|
+
readonly ttlMs: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** How long before expiry a refresh starts: a fifth of the entry's life, never under a minute or over ten. */
|
|
18
|
+
export const keepWarmLeadMs = (ttlMs: number): number => Math.min(10 * 60_000, Math.max(60_000, Math.round(ttlMs / 5)));
|
|
19
|
+
|
|
20
|
+
/** Refreshes one hold may spend: half of what a cold resume rewrites, the other half left for each refresh's own tail. */
|
|
21
|
+
export const keepWarmMaxRefreshes = (ttlMs: number): number => Math.floor((writeCost(ttlMs) - READ_COST) / READ_COST / 2);
|
|
22
|
+
|
|
23
|
+
/** How many refreshes keep an entry alive through `until`. */
|
|
24
|
+
export const keepWarmRefreshes = (cache: CacheClock, until: number): number => {
|
|
25
|
+
const beyond = until - (cache.at + cache.ttlMs);
|
|
26
|
+
return beyond <= 0 ? 0 : Math.ceil(beyond / (cache.ttlMs - keepWarmLeadMs(cache.ttlMs)));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The latest instant a hold may reach, given the refreshes it already spent: past it, refreshing costs more than the cold resume it saves. */
|
|
30
|
+
export const keepWarmHorizon = (cache: CacheClock, spent = 0): number =>
|
|
31
|
+
cache.at + cache.ttlMs + Math.max(0, keepWarmMaxRefreshes(cache.ttlMs) - spent) * (cache.ttlMs - keepWarmLeadMs(cache.ttlMs));
|
|
32
|
+
|
|
33
|
+
/** When the next refresh is due for an entry last touched at `cache.at`. */
|
|
34
|
+
export const keepWarmDueAt = (cache: CacheClock): number => cache.at + cache.ttlMs - keepWarmLeadMs(cache.ttlMs);
|
|
35
|
+
|
|
36
|
+
/** The furthest `until` a hold can honestly promise: the economic horizon, or the date change, whichever comes first. */
|
|
37
|
+
export const keepWarmCap = (cache: CacheClock, rollsAt: number | undefined, spent = 0): number =>
|
|
38
|
+
rollsAt === undefined ? keepWarmHorizon(cache, spent) : Math.min(keepWarmHorizon(cache, spent), rollsAt);
|
|
39
|
+
|
|
40
|
+
// Why a hold ended before anyone picked the conversation up. `elapsed` is the one ending that is not a problem.
|
|
41
|
+
export const KeepWarmEndSchema = z.enum(["elapsed", "allowance", "limited", "changed", "rewrote", "cold", "failed", "midnight", "moved"]);
|
|
42
|
+
export type KeepWarmEnd = z.infer<typeof KeepWarmEndSchema>;
|
|
43
|
+
|
|
44
|
+
export const KeepWarmSchema = z.object({
|
|
45
|
+
since: z.number().describe("When keeping it warm started, in milliseconds."),
|
|
46
|
+
until: z
|
|
47
|
+
.number()
|
|
48
|
+
.describe(
|
|
49
|
+
"When it stops by itself, in milliseconds: the time asked for, shortened to what the sandbox can honestly keep, which is never past the point where refreshing costs more than re-reading, nor past the date change that rewrites the prompt.",
|
|
50
|
+
),
|
|
51
|
+
auto: z.boolean().optional().describe("Started by the sandbox-wide setting after a turn, rather than by a press on this conversation."),
|
|
52
|
+
refreshes: z.number().int().min(0).describe("Refreshes sent so far."),
|
|
53
|
+
readTokens: z
|
|
54
|
+
.number()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("How much the last refresh read back from the provider's cache, in tokens: the proof the cache was still there."),
|
|
57
|
+
ended: z
|
|
58
|
+
.object({
|
|
59
|
+
at: z.number().describe("When it stopped, in milliseconds."),
|
|
60
|
+
reason: KeepWarmEndSchema.describe(
|
|
61
|
+
"Why: `elapsed` the time asked for ran out; `allowance` the account reached the reserve kept for real work; `limited` the provider refused a refresh; `changed` what the next turn would send no longer matches the cache; `rewrote` a refresh found the cache already gone; `cold` it expired before a refresh could run; `failed` a refresh failed; `midnight` the date in the prompt changed; `moved` the conversation's session or account changed.",
|
|
62
|
+
),
|
|
63
|
+
detail: z.string().optional().describe("The specifics, when there are any: which parts of the prompt changed, or the failure's own words."),
|
|
64
|
+
})
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Why keeping it warm stopped before anyone picked the conversation up. Absent while it is still being kept."),
|
|
67
|
+
});
|
|
68
|
+
export type KeepWarm = z.infer<typeof KeepWarmSchema>;
|
|
69
|
+
|
|
70
|
+
export const AgentKeepWarmSchema = z.object({
|
|
71
|
+
id: z.string().min(1).describe("Which conversation."),
|
|
72
|
+
until: z
|
|
73
|
+
.number()
|
|
74
|
+
.nullable()
|
|
75
|
+
.describe(
|
|
76
|
+
"Keep its prompt cache warm until this instant, in milliseconds; shortened to what the sandbox can honestly keep. Null stops keeping it warm.",
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
export type AgentKeepWarm = z.infer<typeof AgentKeepWarmSchema>;
|
|
80
|
+
|
|
81
|
+
// What a turn's first request found in the cache, and how long it had been kept warm for it when it was.
|
|
82
|
+
export const PromptCacheOpeningSchema = z.object({
|
|
83
|
+
readTokens: z.number().describe("Tokens the turn's first request read from the provider's cache."),
|
|
84
|
+
writtenTokens: z.number().describe("Tokens it wrote to the cache, which is what it paid full price for."),
|
|
85
|
+
kept: z
|
|
86
|
+
.object({
|
|
87
|
+
forMs: z.number().describe("How long the cache had been kept warm for this turn, in milliseconds."),
|
|
88
|
+
refreshes: z.number().int().min(0).describe("How many refreshes that took."),
|
|
89
|
+
})
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Present when this turn picked up a conversation the sandbox had been keeping warm."),
|
|
92
|
+
});
|
|
93
|
+
export type PromptCacheOpening = z.infer<typeof PromptCacheOpeningSchema>;
|
|
94
|
+
|
|
95
|
+
const tokensLabel = (tokens: number): string => (tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : String(tokens));
|
|
96
|
+
|
|
97
|
+
const spanLabel = (ms: number): string => {
|
|
98
|
+
const minutes = Math.max(1, Math.round(ms / 60_000));
|
|
99
|
+
const hours = Math.floor(minutes / 60);
|
|
100
|
+
return hours === 0 ? `${minutes}m` : `${hours}h ${minutes % 60}m`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** The transcript's line for a turn that picked up a conversation kept warm for it; undefined for any other turn. */
|
|
104
|
+
export const keptWarmLine = (opening: PromptCacheOpening): string | undefined => {
|
|
105
|
+
const { kept } = opening;
|
|
106
|
+
if (kept === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const held = `kept warm for ${spanLabel(kept.forMs)} with ${kept.refreshes} ${kept.refreshes === 1 ? "refresh" : "refreshes"}`;
|
|
110
|
+
return opening.readTokens >= opening.writtenTokens
|
|
111
|
+
? `Picked up warm: ${tokensLabel(opening.readTokens)} tokens read from the cache, ${held}.`
|
|
112
|
+
: `Picked up cold despite being ${held}: ${tokensLabel(opening.writtenTokens)} tokens sent again, because this turn's prompt no longer matched the cache.`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// A turn's prefix, part by part, so two turns that should share a cache can say which part stopped matching.
|
|
116
|
+
export const PromptFingerprintSchema = z.object({
|
|
117
|
+
hash: z.string().describe("One short hash over every part."),
|
|
118
|
+
parts: z.record(z.string(), z.string()).describe("Each part's own short hash or value, by name."),
|
|
119
|
+
});
|
|
120
|
+
export type PromptFingerprint = z.infer<typeof PromptFingerprintSchema>;
|
|
121
|
+
|
|
122
|
+
/** Which parts differ between two fingerprints, by name; empty when they match. */
|
|
123
|
+
export const changedParts = (before: PromptFingerprint, after: PromptFingerprint): string[] =>
|
|
124
|
+
[...new Set([...Object.keys(before.parts), ...Object.keys(after.parts)])].filter((name) => before.parts[name] !== after.parts[name]).toSorted();
|
package/src/schemas/metrics.ts
CHANGED
|
@@ -59,9 +59,7 @@ export const SandboxUsageSchema = z.object({
|
|
|
59
59
|
.optional()
|
|
60
60
|
.describe("CPU the whole sandbox used over the window, as a percentage of all it may use (`cores`). Absent on a first reading."),
|
|
61
61
|
cores: z.number().describe("How many cores the sandbox may use: its CPU quota, or every core it is allowed to run on when it has none."),
|
|
62
|
-
memoryBytes: z
|
|
63
|
-
.number()
|
|
64
|
-
.describe("Memory in use, less the file cache the kernel takes back on demand: the figure that runs into the limit."),
|
|
62
|
+
memoryBytes: z.number().describe("Memory in use, less the file cache the kernel takes back on demand: the figure that runs into the limit."),
|
|
65
63
|
memoryLimitBytes: z.number().describe("The memory limit: the container's own, or the machine's memory when the container has none."),
|
|
66
64
|
swapBytes: z.number().optional().describe("Memory pushed out to swap. Absent where the sandbox cannot see its own swap."),
|
|
67
65
|
diskBytes: z.number().optional().describe("Space used on the volume the workspace lives on. Absent when the volume would not say."),
|
|
@@ -70,7 +68,9 @@ export const SandboxUsageSchema = z.object({
|
|
|
70
68
|
.tuple([z.number(), z.number(), z.number()])
|
|
71
69
|
.describe("The load average over 1, 5 and 15 minutes. It is the machine's, so other sandboxes on it count too."),
|
|
72
70
|
processes: z.number().describe("How many processes are running in the sandbox."),
|
|
73
|
-
pressure: PressureMetricsSchema.optional().describe(
|
|
71
|
+
pressure: PressureMetricsSchema.optional().describe(
|
|
72
|
+
"How much work waited on CPU, memory or disk lately. Absent where the kernel does not report it.",
|
|
73
|
+
),
|
|
74
74
|
});
|
|
75
75
|
export type SandboxUsage = z.infer<typeof SandboxUsageSchema>;
|
|
76
76
|
|
|
@@ -109,89 +109,58 @@ export type SandboxMetrics = z.infer<typeof SandboxMetricsSchema>;
|
|
|
109
109
|
// What is filling the sandbox's disk, by what it is for (GET /system/storage). Sizes are apparent bytes, and a file
|
|
110
110
|
// with several hard links counts once, toward the first place the scan met it.
|
|
111
111
|
|
|
112
|
-
// Every category a byte on the sandbox's volumes can belong to; STORAGE_CLEANABILITY below says which may be cleaned.
|
|
113
|
-
export const STORAGE_CATEGORIES = [
|
|
114
|
-
// The workspace's own files: repositories, their dependencies, anything the owner or an agent put there.
|
|
115
|
-
"workspace",
|
|
116
|
-
// Transcripts, runtime session stores, attachments and loop memory of every conversation, archived ones included.
|
|
117
|
-
"conversations",
|
|
118
|
-
// Each open conversation's own checkout of the repositories, and the dependencies its turns installed over them.
|
|
119
|
-
"checkouts",
|
|
120
|
-
// The minute-by-minute and per-turn snapshots the History timeline restores from.
|
|
121
|
-
"restorePoints",
|
|
122
|
-
// The repositories' own git data: every commit and branch, pushed or not.
|
|
123
|
-
"repositories",
|
|
124
|
-
// Installed versions of the agent runtimes: the one turns use and the one kept for Revert.
|
|
125
|
-
"engines",
|
|
126
|
-
// Search indexes and caches the sandbox keeps up to date by itself while it runs.
|
|
127
|
-
"indexes",
|
|
128
|
-
// Installed extensions and their scratch.
|
|
129
|
-
"extensions",
|
|
130
|
-
// The sandbox's own Docker: images, containers, volumes.
|
|
131
|
-
"docker",
|
|
132
|
-
// Settings, credentials, sign-ins and the ledgers of what was spent and done.
|
|
133
|
-
"state",
|
|
134
|
-
// Anything no part of the sandbox claims.
|
|
135
|
-
"other",
|
|
136
|
-
// Git data and checkouts of repositories deleted from the workspace.
|
|
137
|
-
"trash",
|
|
138
|
-
// Copies of the sandbox packed for download.
|
|
139
|
-
"exports",
|
|
140
|
-
// Files agents produced: generated images, reports, harness output.
|
|
141
|
-
"artifacts",
|
|
142
|
-
// Screenshots and page snapshots the agent's browser took.
|
|
143
|
-
"browserCaptures",
|
|
144
|
-
// The agent browser's profiles, with the sites it is signed in to.
|
|
145
|
-
"browserProfiles",
|
|
146
|
-
// Weights downloaded for local models.
|
|
147
|
-
"modelWeights",
|
|
148
|
-
// The sandbox's own logs and terminal captures.
|
|
149
|
-
"logs",
|
|
150
|
-
// Scratch space agents and checks leave behind.
|
|
151
|
-
"scratch",
|
|
152
|
-
// Package managers' content stores, which the next install refills.
|
|
153
|
-
"packageStores",
|
|
154
|
-
// Build tools' output caches.
|
|
155
|
-
"buildCaches",
|
|
156
|
-
] as const;
|
|
157
|
-
export const StorageCategoryIdSchema = z.enum(STORAGE_CATEGORIES);
|
|
158
|
-
export type StorageCategoryId = z.infer<typeof StorageCategoryIdSchema>;
|
|
159
|
-
|
|
160
112
|
// none: never removed from here; safe: comes back by itself, removed without asking; confirm: removed only once the
|
|
161
113
|
// owner has read what it costs.
|
|
162
114
|
export const StorageCleanabilitySchema = z.enum(["none", "safe", "confirm"]);
|
|
163
115
|
export type StorageCleanability = z.infer<typeof StorageCleanabilitySchema>;
|
|
164
116
|
|
|
165
|
-
//
|
|
166
|
-
// confirm by it. State, credentials and live work are `none`; what regenerates is `safe`; the owner's own data asks.
|
|
117
|
+
// Every category a byte on the volumes can belong to, and its cleanability: the cleaner refuses by it, the editor words its confirm by it.
|
|
167
118
|
export const STORAGE_CLEANABILITY = {
|
|
119
|
+
// The workspace's own files: repositories, their dependencies, anything the owner or an agent put there.
|
|
168
120
|
workspace: "none",
|
|
121
|
+
// Transcripts, runtime session stores, attachments and loop memory of every conversation, archived ones included.
|
|
169
122
|
conversations: "none",
|
|
170
|
-
//
|
|
123
|
+
// Each open conversation's own checkout of the repositories and the dependencies its turns installed; archiving the conversation frees it.
|
|
171
124
|
checkouts: "none",
|
|
172
|
-
//
|
|
125
|
+
// Per-turn and minute-by-minute snapshots History restores from; no compaction to offer: `git gc --auto` runs after each, all is reachable.
|
|
173
126
|
restorePoints: "none",
|
|
127
|
+
// The repositories' own git data: every commit and branch, pushed or not.
|
|
174
128
|
repositories: "none",
|
|
175
|
-
//
|
|
129
|
+
// Installed agent runtimes, the one turns use and the one kept for Revert; the Engines card owns which is which, so removing one strands it.
|
|
176
130
|
engines: "none",
|
|
177
|
-
//
|
|
131
|
+
// Search indexes and caches the sandbox keeps up to date while it runs, held open by it: removing a file frees nothing until it closes.
|
|
178
132
|
indexes: "none",
|
|
133
|
+
// Installed extensions and their scratch.
|
|
179
134
|
extensions: "none",
|
|
135
|
+
// The sandbox's own Docker: images, containers, volumes.
|
|
180
136
|
docker: "none",
|
|
137
|
+
// Settings, credentials, sign-ins and the ledgers of what was spent and done.
|
|
181
138
|
state: "none",
|
|
139
|
+
// Anything no part of the sandbox claims.
|
|
182
140
|
other: "none",
|
|
183
|
-
//
|
|
141
|
+
// Git data and checkouts of repositories deleted from the workspace; nothing reads it again, but it may hold commits never pushed.
|
|
184
142
|
trash: "confirm",
|
|
143
|
+
// Copies of the sandbox packed for download.
|
|
185
144
|
exports: "confirm",
|
|
145
|
+
// Files agents produced: generated images, reports, harness output.
|
|
186
146
|
artifacts: "confirm",
|
|
147
|
+
// Screenshots and page snapshots the agent's browser took.
|
|
187
148
|
browserCaptures: "confirm",
|
|
149
|
+
// The agent browser's profiles, with the sites it is signed in to.
|
|
188
150
|
browserProfiles: "confirm",
|
|
151
|
+
// Weights downloaded for local models.
|
|
189
152
|
modelWeights: "confirm",
|
|
153
|
+
// The sandbox's own logs and terminal captures.
|
|
190
154
|
logs: "safe",
|
|
155
|
+
// Scratch space agents and checks leave behind.
|
|
191
156
|
scratch: "safe",
|
|
157
|
+
// Package managers' content stores, which the next install refills.
|
|
192
158
|
packageStores: "safe",
|
|
159
|
+
// Build tools' output caches.
|
|
193
160
|
buildCaches: "safe",
|
|
194
|
-
} as const satisfies Record<
|
|
161
|
+
} as const satisfies Record<string, StorageCleanability>;
|
|
162
|
+
export type StorageCategoryId = keyof typeof STORAGE_CLEANABILITY;
|
|
163
|
+
export const StorageCategoryIdSchema = z.enum(Object.keys(STORAGE_CLEANABILITY) as StorageCategoryId[]);
|
|
195
164
|
|
|
196
165
|
export const StorageItemSchema = z.object({
|
|
197
166
|
path: z.string().describe("Where it is, as an absolute path inside the sandbox."),
|
|
@@ -245,11 +214,11 @@ export type StorageCleanInput = z.infer<typeof StorageCleanInputSchema>;
|
|
|
245
214
|
|
|
246
215
|
export const StorageCleanResultSchema = z.object({
|
|
247
216
|
category: StorageCategoryIdSchema,
|
|
248
|
-
freedBytes: z
|
|
249
|
-
removed: z.number().describe("How many items were removed."),
|
|
250
|
-
kept: z
|
|
217
|
+
freedBytes: z
|
|
251
218
|
.number()
|
|
252
|
-
.describe("
|
|
219
|
+
.describe("Space the removals gave back, in bytes. A file that is still linked elsewhere frees nothing and is not counted."),
|
|
220
|
+
removed: z.number().describe("How many items were removed."),
|
|
221
|
+
kept: z.number().describe("How many were left in place: changed too recently, in use by a running program, or no longer this category's."),
|
|
253
222
|
failed: z.number().describe("How many removals the filesystem refused."),
|
|
254
223
|
});
|
|
255
224
|
export type StorageCleanResult = z.infer<typeof StorageCleanResultSchema>;
|
|
@@ -22,6 +22,12 @@ export const OauthAccountSchema = z.object({
|
|
|
22
22
|
.optional()
|
|
23
23
|
.describe("Its stored credential can no longer be renewed and somebody has to sign in again. Absent means healthy, or not checked yet."),
|
|
24
24
|
detail: z.string().optional().describe("Why, in words a person can act on."),
|
|
25
|
+
seatRefusal: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"Its organisation has switched it off for this harness (no seat): it still signs in and its plan limits may still read, but no turn can run on it until an admin gives access back. The provider's own sentence; cleared by the next turn that runs on it. Absent means no refusal is on file.",
|
|
30
|
+
),
|
|
25
31
|
usage: AccountUsageSchema.optional().describe(
|
|
26
32
|
"How full its plan limits were when last measured, so a picker can show what is left before committing work to it. Absent until a reading exists, which reads as unknown rather than as nothing left.",
|
|
27
33
|
),
|
|
@@ -103,6 +103,13 @@ export const UsageTurnSchema = z.object({
|
|
|
103
103
|
compactions: z.number().optional(),
|
|
104
104
|
contextTokens: z.number().optional(),
|
|
105
105
|
contextWindow: z.number().optional(),
|
|
106
|
+
// Absent is a turn; `keep-warm` is a cache refresh sent while the conversation sat idle, billed but not a turn.
|
|
107
|
+
purpose: z.enum(["keep-warm"]).optional().describe("What the row is when it is not a turn: `keep-warm` is a cache refresh sent while the conversation sat idle."),
|
|
108
|
+
// The first request alone: a warm resume reads nearly the whole context here, a cold one writes it.
|
|
109
|
+
openingCacheReadTokens: z.number().optional().describe("Tokens the turn's first request read from the provider's cache."),
|
|
110
|
+
openingCacheCreationTokens: z.number().optional().describe("Tokens the turn's first request wrote to the provider's cache."),
|
|
111
|
+
// Equal on two turns means the prefix both sent was assembled from the same parts (PromptFingerprintSchema).
|
|
112
|
+
promptFingerprint: z.string().optional().describe("A short hash over the parts of the prompt a cache is keyed on; a change between two turns names why the second could not reuse the first's cache."),
|
|
106
113
|
// This turn's model was chosen by the Auto judge reading the conversation's opening message, not picked by hand.
|
|
107
114
|
// Marked on that one turn only; a later row of the same conversation naming a different model is the user
|
|
108
115
|
// overruling it, which is the escalation rate this feature has to be able to answer for.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RuleSchema } from "./settings.js";
|
|
1
|
+
import { RepoChecksFileSchema, RuleSchema, SandboxSettingsSchema } from "./settings.js";
|
|
3
2
|
|
|
4
3
|
// A rule that saves cleanly and silently does nothing is the failure this schema exists to refuse: an action, and a
|
|
5
4
|
// built-in's name, are checked against the moment they are written at.
|
|
@@ -12,14 +11,49 @@ describe(`which built-in stands at which moment`, () => {
|
|
|
12
11
|
expect(rule(`turn.ending`, { kind: `builtin`, name: `version-landed` }).success).toBe(false);
|
|
13
12
|
});
|
|
14
13
|
|
|
15
|
-
test(`the
|
|
16
|
-
expect(rule(`turn.ending`, { kind: `builtin`, name: `verify-edits` }).success).toBe(true);
|
|
17
|
-
expect(rule(`agent.landed`, { kind: `builtin`, name: `verify-edits` }).success).toBe(false);
|
|
14
|
+
test(`the look stands at the turn's end and not at the landing`, () => {
|
|
15
|
+
expect(rule(`turn.ending`, { kind: `builtin`, name: `verify-ui-edits` }).success).toBe(true);
|
|
16
|
+
expect(rule(`agent.landed`, { kind: `builtin`, name: `verify-ui-edits` }).success).toBe(false);
|
|
18
17
|
});
|
|
19
18
|
|
|
20
|
-
test(`the landed moment takes no verdict
|
|
19
|
+
test(`the landed moment takes no verdict and no command: the work is already in the tree`, () => {
|
|
21
20
|
expect(rule(`agent.landed`, { kind: `verdict`, verdict: `allow` }).success).toBe(false);
|
|
22
21
|
expect(rule(`agent.landed`, { kind: `command`, command: `pnpm test` }).success).toBe(false);
|
|
23
|
-
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test(`there is no push moment: the repository's own pre-push hook gates a push`, () => {
|
|
25
|
+
expect(rule(`push.starting`, { kind: `command`, command: `pnpm test` }).success).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe(`where a command lives`, () => {
|
|
30
|
+
test(`settings refuse a command rule, since it belongs to the repository it checks`, () => {
|
|
31
|
+
const command = { id: `lint`, label: `lint`, moment: `file.edited`, action: { kind: `command`, command: `oxlint {file}` } };
|
|
32
|
+
expect(SandboxSettingsSchema.safeParse({ rules: [command] }).success).toBe(false);
|
|
33
|
+
const land = { id: `auto-land`, label: `land`, moment: `agent.finished`, action: { kind: `verdict`, verdict: `allow` } };
|
|
34
|
+
expect(SandboxSettingsSchema.safeParse({ rules: [land] }).success).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test(`a repository declares edit, turn and land checks, and no push check`, () => {
|
|
38
|
+
const file = (checks: unknown[]) => RepoChecksFileSchema.safeParse({ checks }).success;
|
|
39
|
+
expect(
|
|
40
|
+
file([
|
|
41
|
+
{ when: `edit`, run: `lint {file}` },
|
|
42
|
+
{ when: `turn`, run: `pnpm verify:turn` },
|
|
43
|
+
{ when: `land`, run: `pnpm verify` },
|
|
44
|
+
]),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
expect(file([{ when: `push`, run: `pnpm verify:push` }])).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test(`a land check is one per repository and narrows by no path`, () => {
|
|
50
|
+
const file = (checks: unknown[]) => RepoChecksFileSchema.safeParse({ checks }).success;
|
|
51
|
+
expect(
|
|
52
|
+
file([
|
|
53
|
+
{ when: `land`, run: `pnpm verify` },
|
|
54
|
+
{ when: `land`, run: `pnpm test` },
|
|
55
|
+
]),
|
|
56
|
+
).toBe(false);
|
|
57
|
+
expect(file([{ when: `land`, run: `pnpm verify`, paths: [`src/**`] }])).toBe(false);
|
|
24
58
|
});
|
|
25
59
|
});
|