@coreplane/switchboard 0.0.0 → 1.18.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/LICENSE +201 -0
- package/README.md +18 -1
- package/dist/assets/.dockerignore +27 -0
- package/dist/assets/.env.example +33 -0
- package/dist/assets/Dockerfile +111 -0
- package/dist/assets/config/config.example.yaml +359 -0
- package/dist/assets/deploy/bin/build-stamp.d.mts +15 -0
- package/dist/assets/deploy/bin/build-stamp.mjs +98 -0
- package/dist/assets/deploy/bin/cf-logs +32 -0
- package/dist/assets/deploy/cloudflare/package.json +29 -0
- package/dist/assets/deploy/cloudflare/preflight.mjs +243 -0
- package/dist/assets/deploy/cloudflare/tsconfig.json +18 -0
- package/dist/assets/deploy/cloudflare/worker.ts +382 -0
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +67 -0
- package/dist/assets/deploy/cloudflare/write-build.d.mts +7 -0
- package/dist/assets/deploy/cloudflare/write-build.mjs +53 -0
- package/dist/assets/deploy/cloudflare-docs/package.json +18 -0
- package/dist/assets/deploy/cloudflare-docs/wrangler.template.jsonc +30 -0
- package/dist/assets/deploy/cloudflare-memory/package.json +25 -0
- package/dist/assets/deploy/cloudflare-memory/tsconfig.json +17 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +2635 -0
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +50 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +91 -0
- package/dist/assets/deploy/cloudflare-resident/gc.ts +287 -0
- package/dist/assets/deploy/cloudflare-resident/node-async-hooks.d.ts +11 -0
- package/dist/assets/deploy/cloudflare-resident/package.json +29 -0
- package/dist/assets/deploy/cloudflare-resident/preflight.mjs +224 -0
- package/dist/assets/deploy/cloudflare-resident/tsconfig.json +19 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +6637 -0
- package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +120 -0
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +67 -0
- package/dist/assets/deploy/cloudflare-sandbox/docker-wrapper.sh +37 -0
- package/dist/assets/deploy/cloudflare-sandbox/package.json +26 -0
- package/dist/assets/deploy/cloudflare-sandbox/tsconfig.json +20 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +410 -0
- package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +67 -0
- package/dist/assets/deploy/profile.example.json +13 -0
- package/dist/assets/deploy/secrets.manifest.json +108 -0
- package/dist/assets/docker-entrypoint.sh +15 -0
- package/dist/assets/package-lock.json +18407 -0
- package/dist/assets/package.json +104 -0
- package/dist/assets/project.json +219 -0
- package/dist/assets/source.json +5 -0
- package/dist/assets/src/core/authz/actor.ts +100 -0
- package/dist/assets/src/core/authz/authorize.ts +169 -0
- package/dist/assets/src/core/authz/grants.ts +347 -0
- package/dist/assets/src/core/authz/policy.ts +281 -0
- package/dist/assets/src/core/authz/resource.ts +147 -0
- package/dist/assets/src/core/authz/types.ts +164 -0
- package/dist/assets/src/core/drain.ts +54 -0
- package/dist/assets/src/core/ingressTokens.ts +64 -0
- package/dist/assets/src/core/memory/engine.ts +115 -0
- package/dist/assets/src/core/memory/scorer.ts +147 -0
- package/dist/assets/src/core/memory/types.ts +120 -0
- package/dist/assets/src/core/normalizeSpans.ts +299 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +54 -0
- package/dist/assets/src/core/redact.ts +113 -0
- package/dist/assets/src/core/runEvents.ts +537 -0
- package/dist/assets/src/core/runFriction.ts +665 -0
- package/dist/assets/src/core/runLedger/decisions.ts +126 -0
- package/dist/assets/src/core/runLedger/types.ts +177 -0
- package/dist/assets/src/core/runRecord.ts +627 -0
- package/dist/assets/src/core/runShape.ts +61 -0
- package/dist/assets/src/core/schedules.ts +452 -0
- package/dist/assets/src/core/time/formatDuration.ts +61 -0
- package/dist/assets/src/core/trace/attrs.ts +203 -0
- package/dist/assets/src/core/trace/classify.ts +49 -0
- package/dist/assets/src/core/trace/clock.ts +6 -0
- package/dist/assets/src/core/trace/context.ts +9 -0
- package/dist/assets/src/core/trace/ids.ts +23 -0
- package/dist/assets/src/core/trace/partition.ts +235 -0
- package/dist/assets/src/core/trace/sinks.ts +68 -0
- package/dist/assets/src/core/trace/streamSpans.ts +163 -0
- package/dist/assets/src/core/trace/traceparent.ts +29 -0
- package/dist/assets/src/core/trace/tracer.ts +247 -0
- package/dist/assets/src/core/trace/types.ts +125 -0
- package/dist/assets/src/core/trace/workerTrace.ts +97 -0
- package/dist/assets/src/deploy/buildStamp.ts +93 -0
- package/dist/assets/src/deploy/liveGate.ts +203 -0
- package/dist/assets/src/deploy/profile.ts +162 -0
- package/dist/assets/src/deploy/restart.ts +393 -0
- package/dist/assets/src/effort.ts +17 -0
- package/dist/assets/src/execution/bashTimeout.ts +78 -0
- package/dist/assets/src/execution/bindingPurge.ts +43 -0
- package/dist/assets/src/execution/residentBackupTransfer.ts +50 -0
- package/dist/assets/src/execution/residentCleanliness.ts +95 -0
- package/dist/assets/src/execution/residentCredentials.ts +81 -0
- package/dist/assets/src/execution/residentDepCache.ts +321 -0
- package/dist/assets/src/execution/residentDepsStore.ts +326 -0
- package/dist/assets/src/execution/residentDetach.ts +48 -0
- package/dist/assets/src/execution/residentDisk.ts +107 -0
- package/dist/assets/src/execution/residentDiskBudget.ts +448 -0
- package/dist/assets/src/execution/residentExecWrap.ts +100 -0
- package/dist/assets/src/execution/residentHead.ts +85 -0
- package/dist/assets/src/execution/residentReadonly.ts +72 -0
- package/dist/assets/src/execution/residentRefresh.ts +429 -0
- package/dist/assets/src/execution/residentRestoreExtract.ts +130 -0
- package/dist/assets/src/execution/residentState.ts +47 -0
- package/dist/assets/src/execution/residentStepReport.ts +98 -0
- package/dist/assets/src/execution/residentStepTrace.ts +97 -0
- package/dist/assets/src/execution/residentSteps.ts +99 -0
- package/dist/assets/src/execution/residentText.ts +83 -0
- package/dist/assets/src/execution/residentTrace.ts +119 -0
- package/dist/assets/src/execution/sandboxEnv.ts +42 -0
- package/dist/assets/src/execution/sandboxErrors.ts +159 -0
- package/dist/assets/src/execution/sandboxKeepalive.ts +118 -0
- package/dist/assets/src/execution/shellQuote.ts +8 -0
- package/dist/assets/src/mcp/registry.ts +242 -0
- package/dist/assets/src/providers/types.ts +152 -0
- package/dist/assets/web/dist/.vite/manifest.json +176 -0
- package/dist/assets/web/dist/assets/AppShell-Bk2gbvet.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-CTZcMYYx.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-C-BuaSm8.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-D3shEnzl.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-DWIubQ05.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BMjuE-oX.js +126 -0
- package/dist/assets/web/dist/assets/RunRoutePage-XVFj0XDc.css +1 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-C3_jYIo0.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-C4krAL9o.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-g1W58mtN.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-DcPRw3zu.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-DJUkMYjo.js +1 -0
- package/dist/assets/web/dist/assets/favicon-DL1rdWJt.js +1 -0
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
- package/dist/assets/web/dist/assets/main-BsBGUyMH.css +2 -0
- package/dist/assets/web/dist/assets/main-CyM5f4JC.js +28 -0
- package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +1 -0
- package/dist/assets/web/dist/assets/seed-BglCRKLA.js +6 -0
- package/dist/assets/web/dist/assets/wallClock-Ckv3sKoR.js +1 -0
- package/dist/cli.js +34494 -0
- package/package.json +43 -10
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Redaction and display-safety helpers shared by every surface that shows text
|
|
2
|
+
* it did not write: run-visibility streams, Slack cards and replies, resident
|
|
3
|
+
* state and error bodies, GitHub error messages. Node-free and import-free so
|
|
4
|
+
* the Workers can import it by relative path. */
|
|
5
|
+
|
|
6
|
+
// Credential shapes we must never surface in a run-visibility stream (which may
|
|
7
|
+
// be shown in-channel or on a shared page). Two layers: (1) specific known
|
|
8
|
+
// formats (below), and (2) a name-gated assignment pass (redactNamedAssignments)
|
|
9
|
+
// that hides the VALUE of any `…SECRET`/`…KEY`/`…TOKEN`-style identifier. We
|
|
10
|
+
// redact recognized shapes rather than any long string, to avoid mangling
|
|
11
|
+
// legitimate output (SHAs, UUIDs, digests, version numbers all pass through).
|
|
12
|
+
const REDACT: Array<{ re: RegExp; replace: string }> = [
|
|
13
|
+
// PEM private keys — full block (incl. \n-escaped inside JSON) and a bare header.
|
|
14
|
+
{
|
|
15
|
+
re: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
|
|
16
|
+
replace: "«redacted-private-key»",
|
|
17
|
+
},
|
|
18
|
+
{ re: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g, replace: "«redacted-private-key»" },
|
|
19
|
+
// URL / connection-string basic-auth: scheme://user:password@host
|
|
20
|
+
// Anchored to the start of a scheme-character run (not `\b`): one attempt per
|
|
21
|
+
// run keeps a long pasted token linear, and `1https://u:p@h` still redacts.
|
|
22
|
+
{ re: /(?<![a-z0-9+.-])([a-z0-9+.-]+:\/\/)([^\s:/@]+):([^\s:/@]+)@/gi, replace: "$1$2:«redacted»@" },
|
|
23
|
+
// curl -u user:pass
|
|
24
|
+
{ re: /(^|\s)(-u|--user)(\s+|=)\S+:\S+/g, replace: "$1$2$3«redacted»" },
|
|
25
|
+
// HTTP auth headers (Bearer / Basic / token) and bare Bearer tokens
|
|
26
|
+
{ re: /\b(Authorization\s*:\s*)(Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, replace: "$1$2 «redacted»" },
|
|
27
|
+
{ re: /\b[Bb]earer\s+[A-Za-z0-9._~+/-]{12,}=*/g, replace: "Bearer «redacted»" },
|
|
28
|
+
// Cookies (whole header value)
|
|
29
|
+
{ re: /\b((?:Set-)?Cookie\s*:\s*)[^\r\n]+/gi, replace: "$1«redacted»" },
|
|
30
|
+
// Provider / cloud token formats
|
|
31
|
+
{ re: /xox[baprs]-[A-Za-z0-9-]{8,}/g, replace: "«redacted-slack-token»" },
|
|
32
|
+
{ re: /gh[pousr]_[A-Za-z0-9]{20,}/g, replace: "«redacted-github-token»" },
|
|
33
|
+
{ re: /github_pat_[A-Za-z0-9_]{20,}/g, replace: "«redacted-github-pat»" },
|
|
34
|
+
{ re: /x-access-token:[^@\s/'"]+/gi, replace: "x-access-token:«redacted»" },
|
|
35
|
+
{ re: /sk-ant-[A-Za-z0-9_-]{16,}/g, replace: "«redacted-anthropic-key»" },
|
|
36
|
+
{ re: /sk-(?:proj-)?[A-Za-z0-9_-]{16,}/g, replace: "«redacted-api-key»" },
|
|
37
|
+
{ re: /AKIA[0-9A-Z]{16}/g, replace: "«redacted-aws-key»" },
|
|
38
|
+
{ re: /AIza[0-9A-Za-z_-]{35}/g, replace: "«redacted-gcp-key»" },
|
|
39
|
+
{ re: /\b(?:whsec|sk_live|sk_test|rk_live|pk_live)_[A-Za-z0-9]{16,}/g, replace: "«redacted-stripe-key»" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// An identifier component (split on _ or -) that marks its assignment's value as
|
|
43
|
+
// secret. Matched case-insensitively against each component, so `AWS_SECRET_
|
|
44
|
+
// ACCESS_KEY` (…SECRET, …KEY) and `STRIPE_WEBHOOK_SECRET` are caught while
|
|
45
|
+
// `PORT`, `REACT_VERSION`, `DATABASE_URL`, `MONKEY_BARS` are not.
|
|
46
|
+
const SECRET_COMPONENT =
|
|
47
|
+
/^(secret|token|password|passwd|pwd|credential|credentials|key|apikey|auth|session|sessionid|cookie)$/i;
|
|
48
|
+
|
|
49
|
+
/** Redact the VALUE of any `<name> = value` / `<name>: value` where the name has
|
|
50
|
+
* a secret-marking component. Handles quoted values (with spaces) and unquoted,
|
|
51
|
+
* and a quoted NAME (`"password": "…"` in pasted JSON — the closing quote sits
|
|
52
|
+
* between the name and the separator). Name-gated so ordinary config
|
|
53
|
+
* assignments are untouched. */
|
|
54
|
+
function redactNamedAssignments(text: string): string {
|
|
55
|
+
return text.replace(
|
|
56
|
+
// The identifier is the WHOLE `[A-Za-z0-9_-]` run, anchored to its start by
|
|
57
|
+
// the lookbehind: without an anchor a long unbroken token (pasted base64, a
|
|
58
|
+
// minified line) is retried from every offset and each attempt backtracks
|
|
59
|
+
// the whole tail — O(n²), ~0.5 s per 20 KB. A `\b` anchor is not enough:
|
|
60
|
+
// it skips `_SECRET=`, `self._password =`, `2fa_token=` (a letter run
|
|
61
|
+
// preceded by `_`/digit), and a letter-start id (`[A-Za-z][A-Za-z0-9]*`)
|
|
62
|
+
// is still retried at every letter of a mixed alphanumeric run. Taking the
|
|
63
|
+
// maximal run means one attempt per run; the component check below still
|
|
64
|
+
// decides whether it names a secret (`_SECRET` → ["", "SECRET"]).
|
|
65
|
+
/(?<![A-Za-z0-9_-])([A-Za-z0-9_-]+)("?)(\s*[=:]\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]{4,})/g,
|
|
66
|
+
(whole, id: string, close: string, sep: string, val: string) => {
|
|
67
|
+
if (!id.split(/[_-]/).some((p) => SECRET_COMPONENT.test(p))) return whole;
|
|
68
|
+
const quote = val[0] === '"' || val[0] === "'" ? val[0] : "";
|
|
69
|
+
return `${id}${close}${sep}${quote}«redacted»${quote}`;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Strip known credential formats from text before it enters a run-visibility
|
|
75
|
+
* stream: specific shapes first, then the name-gated assignment pass. */
|
|
76
|
+
export function redactSecrets(text: string): string {
|
|
77
|
+
let out = text;
|
|
78
|
+
for (const { re, replace } of REDACT) out = out.replace(re, replace);
|
|
79
|
+
return redactNamedAssignments(out);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Terminal control sequences: CSI (`ESC [ … final`, covers SGR colors, cursor
|
|
83
|
+
// moves, erase), OSC (`ESC ] … BEL|ST`, covers hyperlinks/titles; an OSC cut
|
|
84
|
+
// off by truncation is stripped to end of line so its payload never shows),
|
|
85
|
+
// two-byte ESC sequences, plus C0 controls other than \n and \t (so \r is
|
|
86
|
+
// dropped too: CRLF becomes \n and progress-bar rewrites collapse). Tool
|
|
87
|
+
// output from vitest/git/npm carries these; a browser drops the ESC byte and
|
|
88
|
+
// shows the bare `[32m` remainder, so strip the whole sequence before display.
|
|
89
|
+
const ANSI_RE =
|
|
90
|
+
// eslint-disable-next-line no-control-regex
|
|
91
|
+
/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b\n]*(?:\x07|\x1b\\)?|\x1b[@-Z\\-_]|[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
92
|
+
|
|
93
|
+
/** Remove terminal escape/control sequences, leaving printable text, `\n`, `\t`.
|
|
94
|
+
* Callers strip BEFORE redactSecrets: an escape embedded mid-token would
|
|
95
|
+
* otherwise split a secret across the redaction regex and let it leak. */
|
|
96
|
+
export function stripAnsi(text: string): string {
|
|
97
|
+
return text.replace(ANSI_RE, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Redact THEN cap — the correct order for a length-limited display string, so a
|
|
101
|
+
* secret near a truncation boundary can never be emitted as a raw fragment. */
|
|
102
|
+
export function redactAndCap(text: string, cap = 200): string {
|
|
103
|
+
const redacted = redactSecrets(stripAnsi(text));
|
|
104
|
+
return redacted.length > cap ? `${redacted.slice(0, cap)}…` : redacted;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The first non-empty line of `text`, with runs of whitespace collapsed to one
|
|
108
|
+
* space — for titles and labels, which are one line by construction and must
|
|
109
|
+
* never carry a second line of remote text. */
|
|
110
|
+
export function oneLine(text: string): string {
|
|
111
|
+
const first = text.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
112
|
+
return first.replace(/\s+/g, " ").trim();
|
|
113
|
+
}
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
// Types only, and from the zod-free module deliberately: this file is part of
|
|
2
|
+
// the node-free contract the memory Worker and web app compile with their own
|
|
3
|
+
// tsconfigs — importing prDescription.ts would drag zod into those graphs.
|
|
4
|
+
import type { PrDescription, RenderedTourStep } from "./prDescriptionTypes.js";
|
|
5
|
+
|
|
6
|
+
/** The `pr_description` review artifact minus the event envelope
|
|
7
|
+
* (docs/reference/specs/reading-diff.md item 7). */
|
|
8
|
+
export interface PrDescriptionArtifact {
|
|
9
|
+
artifact: "pr_description";
|
|
10
|
+
/** `submitted`: the typed object a coding run submitted — exact and complete;
|
|
11
|
+
* `parsed`: read back from the PR body GitHub holds, with `problems` naming
|
|
12
|
+
* what the body did not carry in the renderer's shape. (Not named `source`:
|
|
13
|
+
* the `input` event's `source` is an object, and a literal-typed twin here
|
|
14
|
+
* would make `source` a discriminant of the whole union.) */
|
|
15
|
+
origin: "submitted" | "parsed";
|
|
16
|
+
repo: string;
|
|
17
|
+
pr: number;
|
|
18
|
+
/** The PR head the artifact was produced at: the render head for `submitted`,
|
|
19
|
+
* the reviewed head for `parsed`. Compare with each anchor's `sha` to know
|
|
20
|
+
* whether a step points into the head being looked at. */
|
|
21
|
+
headSha?: string;
|
|
22
|
+
/** The coding run a review run copied a `submitted` artifact from. */
|
|
23
|
+
fromRunId?: string;
|
|
24
|
+
title: string;
|
|
25
|
+
/** The body as rendered (submitted) or as GitHub holds it (parsed), capped. */
|
|
26
|
+
body: string;
|
|
27
|
+
tldr?: string;
|
|
28
|
+
tour: RenderedTourStep[];
|
|
29
|
+
remaining: { path: string; note: string }[];
|
|
30
|
+
decisions: { title: string; rationale: string }[];
|
|
31
|
+
/** Nothing missing or malformed — always true for `submitted`. */
|
|
32
|
+
complete: boolean;
|
|
33
|
+
problems: string[];
|
|
34
|
+
/** The body was cut at the cap before parsing. */
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The artifact on the stream. An interface, not an intersection, so the
|
|
39
|
+
* union below stays discriminable on `type` for every object literal. */
|
|
40
|
+
export interface PrDescriptionArtifactEvent extends PrDescriptionArtifact {
|
|
41
|
+
type: "review_artifact";
|
|
42
|
+
seq?: number;
|
|
43
|
+
at?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Run visibility (docs/reference/specs/run-visibility.md): a typed stream of what an agent is doing —
|
|
47
|
+
// tool calls and their (redacted, summarized) results — emitted by the runner.
|
|
48
|
+
// Today the in-channel status card consumes it live; the external live-view
|
|
49
|
+
// page (a follow-up) will consume the same stream. Keeping it a small typed
|
|
50
|
+
// seam here means neither consumer reaches into the runner's internals.
|
|
51
|
+
|
|
52
|
+
//
|
|
53
|
+
// Timing + lifecycle: every event carries an optional `at`
|
|
54
|
+
// (epoch ms, stamped by the runner's injectable clock) so the run-friction
|
|
55
|
+
// analyzer (`runFriction.ts`) can attribute delay; a `tool_result` marks exec-
|
|
56
|
+
// INFRASTRUCTURE failures (`infra: true`, an `ExecInfraError` — the sandbox, not
|
|
57
|
+
// the command) so they are never confused with an ordinary nonzero exit; and
|
|
58
|
+
// `run_note` events carry the runner's lifecycle notices (wrap-up warning, budget
|
|
59
|
+
// exhaustion, dead sandbox) as typed kinds instead of only free-text progress.
|
|
60
|
+
// All additive: consumers that only know tool_call/tool_result keep working.
|
|
61
|
+
|
|
62
|
+
/** Typed lifecycle notices the runner emits alongside its `onProgress` text.
|
|
63
|
+
* `stop_requested` is published by the registry when an operator asks the run
|
|
64
|
+
* to stop from /runs; `stopped` by the runner when it honors it. */
|
|
65
|
+
export type RunNoteKind =
|
|
66
|
+
| "wrap_up"
|
|
67
|
+
| "time_budget_exhausted"
|
|
68
|
+
| "turn_budget_exhausted"
|
|
69
|
+
| "sandbox_dead"
|
|
70
|
+
/** The sandbox fleet had no free instance for this thread within the
|
|
71
|
+
* executor's bounded wait (docs/reference/specs/execution.md item 14). Capacity, not a
|
|
72
|
+
* dead sandbox: the run goes on and the model is told to retry or finish. */
|
|
73
|
+
| "fleet_busy"
|
|
74
|
+
| "stop_requested"
|
|
75
|
+
| "stopped"
|
|
76
|
+
/** Setup spans the request's stream sink had to drop before this run was
|
|
77
|
+
* bound (docs/reference/specs/tracing.md): `summary` says how many, `from`/`to` the
|
|
78
|
+
* interval, which the partition reports as not recorded. */
|
|
79
|
+
| "spans_dropped"
|
|
80
|
+
/** The PR head moved while a review ran and the same run is re-reviewing at
|
|
81
|
+
* the new head (agent-review.md item 12). Published by the dispatcher. */
|
|
82
|
+
| "head_moved"
|
|
83
|
+
/** An MCP server configured for this agent did not answer discovery
|
|
84
|
+
* (docs/reference/specs/mcp-tools.md item 8); the run proceeds without its tools. One
|
|
85
|
+
* note per server, published by the dispatcher before the first turn. */
|
|
86
|
+
| "mcp_unavailable"
|
|
87
|
+
/** A thread follow-up steered into this run was folded into its next step
|
|
88
|
+
* (docs/reference/specs/thread-admission.md item 2). Published by the runner as it
|
|
89
|
+
* drains the inbox, beside an `input` event carrying the follow-up itself. */
|
|
90
|
+
| "follow_up"
|
|
91
|
+
/** The run was resumed by a new bot generation from its ledger transcript
|
|
92
|
+
* (docs/reference/specs/run-history.md item 37); the summary says how many calls were
|
|
93
|
+
* in flight at the kill and how each was settled. Published by the runner. */
|
|
94
|
+
| "resumed"
|
|
95
|
+
/** A coding run pushed onto a branch that already heads an open PR without
|
|
96
|
+
* resubmitting the PR description, and the same run is being given one
|
|
97
|
+
* bounded extra model turn to submit it (docs/reference/specs/pr-description.md
|
|
98
|
+
* item 5). Published by the dispatcher before that turn. */
|
|
99
|
+
| "description_turn"
|
|
100
|
+
/** The run is on a cold per-thread sandbox instead of a warm resident, and
|
|
101
|
+
* the summary says why — the resident attach failed (its steps so far are
|
|
102
|
+
* grafted under the attach span), the resident was unreachable or not
|
|
103
|
+
* serviceable, or the repo is not onboarded (docs/reference/specs/resident-repos.md
|
|
104
|
+
* item 24). The same text the card carries; published by the dispatcher
|
|
105
|
+
* after the attach, before the first turn, so the run page explains a
|
|
106
|
+
* sandbox run that shows resident steps. */
|
|
107
|
+
| "cold_sandbox";
|
|
108
|
+
|
|
109
|
+
/** Every `RunNoteKind`, as a value (a reader that filters notes by kind uses
|
|
110
|
+
* this; adding a kind to the union without adding it here is a type error). */
|
|
111
|
+
export const RUN_NOTE_KINDS = [
|
|
112
|
+
"wrap_up",
|
|
113
|
+
"time_budget_exhausted",
|
|
114
|
+
"turn_budget_exhausted",
|
|
115
|
+
"sandbox_dead",
|
|
116
|
+
"fleet_busy",
|
|
117
|
+
"stop_requested",
|
|
118
|
+
"stopped",
|
|
119
|
+
"spans_dropped",
|
|
120
|
+
"head_moved",
|
|
121
|
+
"mcp_unavailable",
|
|
122
|
+
"follow_up",
|
|
123
|
+
"resumed",
|
|
124
|
+
"description_turn",
|
|
125
|
+
"cold_sandbox",
|
|
126
|
+
] as const satisfies readonly RunNoteKind[];
|
|
127
|
+
type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
|
|
128
|
+
const _everyKindListed: _EveryKindListed = true;
|
|
129
|
+
void _everyKindListed;
|
|
130
|
+
|
|
131
|
+
/** How an operator asked a run to stop: `soft` — take no new steps and
|
|
132
|
+
* wrap up through the normal finale; `hard` — abort the in-flight call now, no
|
|
133
|
+
* finale, tear the workspace down. */
|
|
134
|
+
export type StopMode = "soft" | "hard";
|
|
135
|
+
|
|
136
|
+
/** Who asked a run to stop: the caller's surface and its platform-
|
|
137
|
+
* namespaced identity (`slack:U…`, `access:<sub>`, `mcp:<subject>`,
|
|
138
|
+
* `cli:local`). Recorded on the `stop_requested` note so the stream itself says
|
|
139
|
+
* who stopped the run; `id` is charset-restricted to `ACTOR_ID_PATTERN` and
|
|
140
|
+
* capped by `sanitizeActor` before it is published. Absent on notes published
|
|
141
|
+
* through the token-gated HTML path (the capability, not a person, is the actor). */
|
|
142
|
+
export interface RunActor {
|
|
143
|
+
kind: "access" | "mcp" | "cli" | "chat";
|
|
144
|
+
id: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The character class an `actor.id` may carry (regex body, no brackets) — the
|
|
148
|
+
* ONE source both the validating pattern and the sanitizer are built from. */
|
|
149
|
+
const ACTOR_ID_CHARS = "A-Za-z0-9:@._-";
|
|
150
|
+
const ACTOR_ID_MAX = 128;
|
|
151
|
+
/** The characters an `actor.id` may carry; anything else is dropped. */
|
|
152
|
+
export const ACTOR_ID_PATTERN = new RegExp(`^[${ACTOR_ID_CHARS}]{1,${ACTOR_ID_MAX}}$`);
|
|
153
|
+
const ACTOR_ID_FORBIDDEN = new RegExp(`[^${ACTOR_ID_CHARS}]`, "g");
|
|
154
|
+
|
|
155
|
+
/** Coerce an actor into the published shape: strip every character outside the
|
|
156
|
+
* allowed set, cap at 128, and fall back to `unknown` when nothing survives —
|
|
157
|
+
* a hostile id is neutered, never a reason to refuse the stop. */
|
|
158
|
+
export function sanitizeActor(actor: RunActor): RunActor {
|
|
159
|
+
const id = actor.id.replace(ACTOR_ID_FORBIDDEN, "").slice(0, ACTOR_ID_MAX);
|
|
160
|
+
return { kind: actor.kind, id: id.length > 0 ? id : "unknown" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** How one `agent:ship` round boundary reads (docs/reference/specs/agent-ship.md item 12).
|
|
164
|
+
* `started` marks the round's child being dispatched; the rest settle it:
|
|
165
|
+
* `pr_opened` — a coding round's post-step opened or edited the PR;
|
|
166
|
+
* `completed` — a fix round finished without a PR write (e.g. it declined
|
|
167
|
+
* everything and never resubmitted the description); `approve` /
|
|
168
|
+
* `request_changes` — a review round's verdict; `no_verdict` — the review
|
|
169
|
+
* child ended without `submit_verdict`, aborting the pipeline; `aborted` — the
|
|
170
|
+
* round ended the pipeline (a refusal, no resident worktree, a round-0
|
|
171
|
+
* terminal); `stopped` — an operator stop settled the round. A cap never
|
|
172
|
+
* settles a round: caps end the pipeline BETWEEN rounds, visible as the
|
|
173
|
+
* absence of a next `started` boundary plus the answer's cap report. */
|
|
174
|
+
export type ShipRoundOutcome =
|
|
175
|
+
"started" | "pr_opened" | "completed" | "approve" | "request_changes" | "no_verdict" | "aborted" | "stopped";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One event in a run's stream. `seq` is stamped by `RunRegistry.publish` — a
|
|
179
|
+
* monotonic, per-run 1-based position (optional on the way in, present on every
|
|
180
|
+
* event read back from the registry) so replays and history pages can resume
|
|
181
|
+
* from a point without comparing payloads.
|
|
182
|
+
*/
|
|
183
|
+
/** A span record (docs/reference/specs/tracing.md): timing, not content. The registry
|
|
184
|
+
* accepts them between a run's finish and its seal, they never repaint the
|
|
185
|
+
* index, and every reader's counts and clocks skip them. */
|
|
186
|
+
export function isSpanRecord(e: { type: string }): e is SpanStartEvent | SpanEndEvent {
|
|
187
|
+
return e.type === "span_start" || e.type === "span_end";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The protected head (docs/reference/specs/tracing.md; live-view item 2) — is this event head material? The root's start, `slack.receive` and the
|
|
191
|
+
* `dispatch.*` span pairs, `input`, `context`, `run_meta`, and the
|
|
192
|
+
* `mcp_unavailable` / `spans_dropped` notes. */
|
|
193
|
+
export function isHeadMaterial(event: RunEvent): boolean {
|
|
194
|
+
switch (event.type) {
|
|
195
|
+
case "input":
|
|
196
|
+
case "context":
|
|
197
|
+
case "run_meta":
|
|
198
|
+
return true;
|
|
199
|
+
case "run_note":
|
|
200
|
+
return event.kind === "mcp_unavailable" || event.kind === "spans_dropped" || event.kind === "cold_sandbox";
|
|
201
|
+
case "span_start":
|
|
202
|
+
return event.name === "request" || event.name === "slack.receive" || event.name.startsWith("dispatch.");
|
|
203
|
+
case "span_end":
|
|
204
|
+
return event.name === "slack.receive" || event.name.startsWith("dispatch.");
|
|
205
|
+
default:
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A span's attributes on the wire: the closed-table values `attrs.ts` admits
|
|
211
|
+
* (literal unions, numbers, booleans; a few sanitized strings). Free text
|
|
212
|
+
* rides only in `span_end.error`, redacted and capped. */
|
|
213
|
+
export type SpanEventAttrs = Readonly<Record<string, string | number | boolean>>;
|
|
214
|
+
|
|
215
|
+
/** A span opened (docs/reference/specs/tracing.md): the run-stream sink publishes one per
|
|
216
|
+
* streamed span so a live page can show the step as it runs. `at` is the
|
|
217
|
+
* span's start on the runner clock. */
|
|
218
|
+
export interface SpanStartEvent {
|
|
219
|
+
type: "span_start";
|
|
220
|
+
spanId: string;
|
|
221
|
+
parentSpanId?: string;
|
|
222
|
+
name: string;
|
|
223
|
+
attrs?: SpanEventAttrs;
|
|
224
|
+
seq?: number;
|
|
225
|
+
at?: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A span closed: its measured interval, its outcome and — for a span about
|
|
229
|
+
* this run's own work whose message we produce — the redacted, capped error
|
|
230
|
+
* text; a span whose failure came from a remote body carries the
|
|
231
|
+
* classification (`errorKind`/`errorCode`) in `attrs` and no message. `at` is
|
|
232
|
+
* the end on the runner clock (`startedAt + durationMs`). */
|
|
233
|
+
export interface SpanEndEvent {
|
|
234
|
+
type: "span_end";
|
|
235
|
+
spanId: string;
|
|
236
|
+
parentSpanId?: string;
|
|
237
|
+
name: string;
|
|
238
|
+
startedAt: number;
|
|
239
|
+
durationMs: number;
|
|
240
|
+
status: "ok" | "error";
|
|
241
|
+
error?: string;
|
|
242
|
+
attrs?: SpanEventAttrs;
|
|
243
|
+
seq?: number;
|
|
244
|
+
at?: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type RunEvent =
|
|
248
|
+
/** `callId` is the provider's tool_use id — the explicit pair key between a
|
|
249
|
+
* call and its result (live-view item 13); the runner stamps it on both. */
|
|
250
|
+
/** `command` rides on bash calls only: the FULL command (redacted, capped at
|
|
251
|
+
* `COMMAND_CAP`, far above the 200-char `summary`), for consumers that must
|
|
252
|
+
* judge what the command did — the pushed-branch tracker
|
|
253
|
+
* (docs/reference/specs/pr-description.md item 5) reads it, because an agent's chained
|
|
254
|
+
* `git add … && git commit … && git push …` routinely carries its `push`
|
|
255
|
+
* past the summary cap. The status card and friction analyzer keep reading
|
|
256
|
+
* `summary`. */
|
|
257
|
+
/** `spanId` (docs/reference/specs/tracing.md): the `tool.*` span this call ran under, once the runner emits spans. */
|
|
258
|
+
| {
|
|
259
|
+
type: "tool_call";
|
|
260
|
+
tool: string;
|
|
261
|
+
summary: string;
|
|
262
|
+
command?: string;
|
|
263
|
+
callId?: string;
|
|
264
|
+
spanId?: string;
|
|
265
|
+
seq?: number;
|
|
266
|
+
at?: number;
|
|
267
|
+
}
|
|
268
|
+
/** `ok` is "the tool succeeded": false when it threw AND (bash) when the
|
|
269
|
+
* command exited nonzero. `exitCode` rides on bash results (parsed from the
|
|
270
|
+
* executors' shared `exit N:` prefix, 0 for a clean run; absent when the code
|
|
271
|
+
* was not numeric). `output` is the tool's text — control-stripped, redacted,
|
|
272
|
+
* capped at TOOL_OUTPUT_CAP — for the run page's expandable card; the status
|
|
273
|
+
* card and the friction analyzer keep reading `summary`. */
|
|
274
|
+
| {
|
|
275
|
+
type: "tool_result";
|
|
276
|
+
tool: string;
|
|
277
|
+
ok: boolean;
|
|
278
|
+
summary: string;
|
|
279
|
+
callId?: string;
|
|
280
|
+
exitCode?: number;
|
|
281
|
+
output?: string;
|
|
282
|
+
infra?: true;
|
|
283
|
+
spanId?: string;
|
|
284
|
+
seq?: number;
|
|
285
|
+
at?: number;
|
|
286
|
+
}
|
|
287
|
+
/** `mode` rides only on the stop notes (`stop_requested` / `stopped`); `actor`
|
|
288
|
+
* only on a `stop_requested` published through `RunsService.stopRun`. */
|
|
289
|
+
| {
|
|
290
|
+
type: "run_note";
|
|
291
|
+
kind: RunNoteKind;
|
|
292
|
+
summary: string;
|
|
293
|
+
mode?: StopMode;
|
|
294
|
+
actor?: RunActor;
|
|
295
|
+
/** On a `spans_dropped` note only (docs/reference/specs/tracing.md): the runner-clock
|
|
296
|
+
* interval the dropped setup records covered — a `not recorded` loss. */
|
|
297
|
+
from?: number;
|
|
298
|
+
to?: number;
|
|
299
|
+
spanId?: string;
|
|
300
|
+
seq?: number;
|
|
301
|
+
at?: number;
|
|
302
|
+
}
|
|
303
|
+
/** The run's final answer — the same text the channel reply/PR post is
|
|
304
|
+
* projected from, redacted like every event (NOT capped: the run record is
|
|
305
|
+
* the source of truth, the summaries are). Published by the dispatcher once
|
|
306
|
+
* per run, before `finish()` and before the reply goes out; absent when an
|
|
307
|
+
* AGENT run threw (the card shows ❌). An inline command run that throws
|
|
308
|
+
* still publishes one — the `⚠️ <error>` reply — so its record explains the
|
|
309
|
+
* `failed` status. `text` is the CANONICAL Markdown (docs/reference/specs/llm-output.md
|
|
310
|
+
* item 5); `raw` is the model's own text, present only when normalization
|
|
311
|
+
* changed it (redacted too, dropped if it would blow the per-event budget). */
|
|
312
|
+
| { type: "answer"; text: string; raw?: string; seq?: number; at?: number }
|
|
313
|
+
/** The request as received (directives stripped, attachments noted as a
|
|
314
|
+
* one-line count suffix — never bytes or file bodies), redacted, uncapped. Published by the dispatcher once
|
|
315
|
+
* per run, right after the run is registered — the first event of the record,
|
|
316
|
+
* so the run page can lead with what was asked (docs/reference/specs/live-view.md item 12). */
|
|
317
|
+
| {
|
|
318
|
+
type: "input";
|
|
319
|
+
text: string;
|
|
320
|
+
/** Where the request came from, for the Request block: the channel and
|
|
321
|
+
* user display names and a link back to the triggering message —
|
|
322
|
+
* whatever the adapter supplied (all optional). */
|
|
323
|
+
source?: { url?: string; channel?: string; user?: string };
|
|
324
|
+
seq?: number;
|
|
325
|
+
at?: number;
|
|
326
|
+
}
|
|
327
|
+
/** One prior thread turn fed to the model, prefixed with its role
|
|
328
|
+
* (`user: …` / `assistant: …`), humanized and redacted like `input`, with
|
|
329
|
+
* attachments as metadata lines. Published by the dispatcher right after
|
|
330
|
+
* `input`, bounded (newest 20 turns / 256 KiB) and gated by
|
|
331
|
+
* `runHistory.includeContext`. */
|
|
332
|
+
| { type: "context"; text: string; seq?: number; at?: number }
|
|
333
|
+
/** The model's prose BETWEEN tool calls — text content that rode alongside
|
|
334
|
+
* tool_use in one completion. Emitted by the runner, redacted, uncapped. The
|
|
335
|
+
* final text-only completion is NOT one of these (that is the `answer`). */
|
|
336
|
+
| { type: "assistant"; text: string; spanId?: string; seq?: number; at?: number }
|
|
337
|
+
/** What the run is about (live-view item 19): the resolved agent and model,
|
|
338
|
+
* and — for a repo run — the repo, ref, PR number and PR head as resolved
|
|
339
|
+
* BEFORE the first model turn (`RepoContext`). Published by the dispatcher
|
|
340
|
+
* right after `input`, once per run, so the run page can head its Request
|
|
341
|
+
* block with linked `owner/repo · ref · #PR · sha`. Additive: every
|
|
342
|
+
* consumer that only knows the other types keeps working. */
|
|
343
|
+
| {
|
|
344
|
+
type: "run_meta";
|
|
345
|
+
agent: string;
|
|
346
|
+
/** Absent on a command run, which resolves no model. */
|
|
347
|
+
model?: string;
|
|
348
|
+
/** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
|
|
349
|
+
traceId?: string;
|
|
350
|
+
effort?: string;
|
|
351
|
+
repo?: string;
|
|
352
|
+
ref?: string;
|
|
353
|
+
pr?: number;
|
|
354
|
+
headSha?: string;
|
|
355
|
+
seq?: number;
|
|
356
|
+
at?: number;
|
|
357
|
+
}
|
|
358
|
+
/** A skill was loaded into the model's context (docs/reference/specs/skills.md). Emitted
|
|
359
|
+
* by the `use_skill` tool on a successful load — alongside, not instead of,
|
|
360
|
+
* its `tool_call`/`tool_result` pair — so skill use is a first-class fact in
|
|
361
|
+
* the run data with its own metadata: which skill, for which agent, from
|
|
362
|
+
* where (`source`, the pinned upstream URL when vendored), and how much
|
|
363
|
+
* context it cost (`bodyBytes`). Additive: consumers that only know the
|
|
364
|
+
* other types keep working; the friction analyzer ignores it. */
|
|
365
|
+
| {
|
|
366
|
+
type: "skill_use";
|
|
367
|
+
skill: string;
|
|
368
|
+
description: string;
|
|
369
|
+
agent: string;
|
|
370
|
+
/** The pinned upstream file URL for a vendored skill (docs/reference/specs/skills.md item 9). */
|
|
371
|
+
source?: string;
|
|
372
|
+
/** Structured vendoring provenance (`Skill.upstream`, recorded by skills:sync). */
|
|
373
|
+
upstream?: { repo: string; commit: string };
|
|
374
|
+
bodyBytes: number;
|
|
375
|
+
spanId?: string;
|
|
376
|
+
seq?: number;
|
|
377
|
+
at?: number;
|
|
378
|
+
}
|
|
379
|
+
/** A review run's reading diff (docs/reference/specs/reading-diff.md): the change as a
|
|
380
|
+
* reviewer reads it. The baseline artifact is the full `git diff`
|
|
381
|
+
* (`poweredBy: "git"`, guaranteed on every PR review); with the meat
|
|
382
|
+
* provider a SECOND artifact may follow — meat.dev's abridged reading diff
|
|
383
|
+
* (`poweredBy: "meat"`, with its one-line `summary` and the abridging
|
|
384
|
+
* call's own token usage) — iff meat finishes within the review. Published
|
|
385
|
+
* by the dispatcher straight to the registry (like `input`/`run_meta`),
|
|
386
|
+
* produced concurrently with the review by the run's own executor; readers
|
|
387
|
+
* prefer the meat artifact when both exist. Additive: unknown → ignored. */
|
|
388
|
+
| {
|
|
389
|
+
type: "review_artifact";
|
|
390
|
+
artifact: "reading_diff";
|
|
391
|
+
poweredBy: "git" | "meat";
|
|
392
|
+
baseRef: string;
|
|
393
|
+
diff: string;
|
|
394
|
+
truncated: boolean;
|
|
395
|
+
summary?: string;
|
|
396
|
+
meatTokens?: { input: number; output: number };
|
|
397
|
+
/** meat only (docs/reference/specs/reading-diff.md item 6): which diff meat
|
|
398
|
+
* read — GitHub's compare of base...head, or the recorded git artifact
|
|
399
|
+
* (whole) — and how many bytes it was; the `-model` it ran with. */
|
|
400
|
+
input?: "github-compare" | "recorded";
|
|
401
|
+
inputBytes?: number;
|
|
402
|
+
model?: string;
|
|
403
|
+
seq?: number;
|
|
404
|
+
at?: number;
|
|
405
|
+
}
|
|
406
|
+
/** The PR's description as data (docs/reference/specs/reading-diff.md item 7): the TL;DR,
|
|
407
|
+
* the Tour's steps with their anchors (each carrying the sha its permalink
|
|
408
|
+
* was rendered at), the Remaining-changes list and the decisions, so the run
|
|
409
|
+
* page's panel can render a collapsed description and a Tour that jumps to
|
|
410
|
+
* files and lines in the diff. Two sources, one shape: a coding run publishes
|
|
411
|
+
* its `submitted` object when the post-step opens or edits the PR (beside
|
|
412
|
+
* `pr_opened`); a review run copies that object when one exists for the head
|
|
413
|
+
* it reviews, else `parsed` reads the body GitHub holds back through the
|
|
414
|
+
* inverse parser. Every string is control-stripped and redacted like the
|
|
415
|
+
* reading diff. Additive: unknown → ignored. */
|
|
416
|
+
| PrDescriptionArtifactEvent
|
|
417
|
+
/** A coding run's accepted `PrDescription` (docs/reference/specs/pr-description.md): the
|
|
418
|
+
* typed object the run submitted through `submit_pr_description`, as
|
|
419
|
+
* validated — the same object the dispatcher renders the GitHub body from,
|
|
420
|
+
* so the run page's review panel can render it without a second authoring
|
|
421
|
+
* path. Published by the dispatcher once per run (the last valid submission
|
|
422
|
+
* wins), string fields redacted like every event payload. Additive: unknown
|
|
423
|
+
* → ignored. */
|
|
424
|
+
| { type: "pr_description"; description: PrDescription; seq?: number; at?: number }
|
|
425
|
+
/** The coding PR post-step's outcome (docs/reference/specs/pr-description.md item 5):
|
|
426
|
+
* the PR opened for the run's pushed branch — or, open-or-edit, the
|
|
427
|
+
* existing open PR that was edited (`created: false`). Published by the
|
|
428
|
+
* dispatcher straight to the registry BEFORE the stream finishes, so the
|
|
429
|
+
* run record carries the PR URL as a fact of the run rather than only the
|
|
430
|
+
* channel reply's projection of it. Additive: unknown → ignored. */
|
|
431
|
+
| { type: "pr_opened"; url: string; number: number; created: boolean; seq?: number; at?: number }
|
|
432
|
+
/** One `agent:ship` round boundary (docs/reference/specs/agent-ship.md item 12): the
|
|
433
|
+
* pipeline publishes a `started` event when a round's child is dispatched
|
|
434
|
+
* and one settle event when its outcome is known (`ShipRoundOutcome`), so
|
|
435
|
+
* rounds are legible on the one stream and per-round cost is derivable by
|
|
436
|
+
* slicing `model.turn` spans between boundaries. `index` is 0-based in the
|
|
437
|
+
* spec's round vocabulary — round 0 is the initial coding round; a review
|
|
438
|
+
* round and its fix round share an index. Published by the ship pipeline
|
|
439
|
+
* straight to the registry (like `pr_opened`), never through the runner.
|
|
440
|
+
* Additive: unknown → ignored. */
|
|
441
|
+
| { type: "ship_round"; index: number; agent: string; outcome: ShipRoundOutcome; seq?: number; at?: number }
|
|
442
|
+
/** The span records (docs/reference/specs/tracing.md): published, counted and stored like
|
|
443
|
+
* every other event, read as timing and never as content. */
|
|
444
|
+
| SpanStartEvent
|
|
445
|
+
| SpanEndEvent;
|
|
446
|
+
|
|
447
|
+
// Redaction helpers live in ./redact.ts and are re-exported here so every
|
|
448
|
+
// existing import site keeps working.
|
|
449
|
+
export { redactAndCap, redactSecrets, stripAnsi } from "./redact.js";
|
|
450
|
+
import { redactSecrets, stripAnsi } from "./redact.js";
|
|
451
|
+
|
|
452
|
+
const SUMMARY_CAP = 200;
|
|
453
|
+
|
|
454
|
+
/** One-line, control-stripped, redacted, length-capped summary of a tool's output for the run
|
|
455
|
+
* stream — the first non-empty line plus a size note. */
|
|
456
|
+
export function summarizeToolResult(output: string): string {
|
|
457
|
+
return summarizeClean(redactSecrets(stripAnsi(output)));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** `summarizeToolResult` for text that is ALREADY control-stripped and redacted. */
|
|
461
|
+
function summarizeClean(clean: string): string {
|
|
462
|
+
const trimmed = clean.trim();
|
|
463
|
+
if (trimmed === "") return "(no output)";
|
|
464
|
+
const firstLine =
|
|
465
|
+
trimmed
|
|
466
|
+
.split("\n")
|
|
467
|
+
.find((l) => l.trim().length > 0)
|
|
468
|
+
?.trim() ?? "";
|
|
469
|
+
const head = firstLine.length > SUMMARY_CAP ? `${firstLine.slice(0, SUMMARY_CAP)}…` : firstLine;
|
|
470
|
+
const lineCount = trimmed.split("\n").length;
|
|
471
|
+
const more =
|
|
472
|
+
trimmed.length > head.length ? ` (${trimmed.length} chars${lineCount > 1 ? `, ${lineCount} lines` : ""})` : "";
|
|
473
|
+
return head + more;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Every executor renders a nonzero command exit as an `exit <code>:` first line
|
|
477
|
+
* (`LocalExecutor.exec`, `ResidentExecutor.exec`, `CloudflareSandboxExecutor.exec`)
|
|
478
|
+
* so the model sees the status. This is the single reader of that contract.
|
|
479
|
+
* `failed` is true for any such prefix; `exitCode` is the code when numeric
|
|
480
|
+
* (execFile can report an errno string instead). Ordinary output — including
|
|
481
|
+
* text that merely mentions "exit 1:" later on — is a clean 0. */
|
|
482
|
+
export function parseExitPrefix(output: string): { failed: boolean; exitCode?: number } {
|
|
483
|
+
const m = /^\s*exit (\S+?):/.exec(stripAnsi(output));
|
|
484
|
+
if (!m) return { failed: false, exitCode: 0 };
|
|
485
|
+
return /^\d+$/.test(m[1]) ? { failed: true, exitCode: Number(m[1]) } : { failed: true };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Upper bound on a `tool_result.output` — large enough for a test run or a
|
|
489
|
+
* diff to read in full on the run page, small enough that a run of ordinary
|
|
490
|
+
* length fits the registry's backlog whole (the backlog is bounded by count AND
|
|
491
|
+
* bytes — `RunRegistry`, 5000 events / 4 MiB — so an output-heavy run trims its
|
|
492
|
+
* oldest events rather than growing without bound). */
|
|
493
|
+
export const TOOL_OUTPUT_CAP = 8_000;
|
|
494
|
+
|
|
495
|
+
/** Cap on a bash `tool_call.command` (the full command beside the 200-char
|
|
496
|
+
* `summary`). Generous — an agent's chained `checkout && add && commit && push`
|
|
497
|
+
* is a few hundred chars; a heredoc-fed script can be a few thousand — and
|
|
498
|
+
* still a small fraction of `MAX_EVENT_BYTES`. */
|
|
499
|
+
export const COMMAND_CAP = 4_000;
|
|
500
|
+
|
|
501
|
+
/** The full tool output as it may leave the process: control-stripped, then
|
|
502
|
+
* redacted, then capped (that order — see redactAndCap). Empty output → "". */
|
|
503
|
+
export function prepareToolOutput(output: string): string {
|
|
504
|
+
return capClean(redactSecrets(stripAnsi(output)));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** `prepareToolOutput` for text that is ALREADY control-stripped and redacted. */
|
|
508
|
+
function capClean(clean: string): string {
|
|
509
|
+
const text = clean.trim();
|
|
510
|
+
if (text.length <= TOOL_OUTPUT_CAP) return text;
|
|
511
|
+
return `${text.slice(0, TOOL_OUTPUT_CAP)}…[${text.length - TOOL_OUTPUT_CAP} more chars]`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Both `tool_result` display fields from ONE strip+redact pass. The redaction
|
|
515
|
+
* battery is ~20 regexes over up to 120k chars of raw tool output; paying it
|
|
516
|
+
* once per result instead of twice (summary, then output) halves the
|
|
517
|
+
* synchronous CPU the runner spends per tool call. Byte-identical to calling
|
|
518
|
+
* `summarizeToolResult` and `prepareToolOutput` separately. */
|
|
519
|
+
export function prepareToolResult(output: string): { summary: string; output: string } {
|
|
520
|
+
const clean = redactSecrets(stripAnsi(output));
|
|
521
|
+
return { summary: summarizeClean(clean), output: capClean(clean) };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** `JSON.stringify(event)`, computed once per event object however many readers
|
|
525
|
+
* it has: the registry measures each event's byte size at publish, then hands
|
|
526
|
+
* the SAME object to every subscriber (and replays the same backlog entries),
|
|
527
|
+
* so with k open tabs on one run each tool_result (up to 8 KB of output) would
|
|
528
|
+
* otherwise be serialized k+1 times. */
|
|
529
|
+
const serialized = new WeakMap<object, string>();
|
|
530
|
+
export function serializedOnce(event: object): string {
|
|
531
|
+
let s = serialized.get(event);
|
|
532
|
+
if (s === undefined) {
|
|
533
|
+
s = JSON.stringify(event);
|
|
534
|
+
serialized.set(event, s);
|
|
535
|
+
}
|
|
536
|
+
return s;
|
|
537
|
+
}
|