@cat-factory/executor-harness 1.78.0 → 1.82.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 +1 -0
- package/dist/agent-capabilities.d.ts +130 -0
- package/dist/agent-runner.d.ts +114 -0
- package/dist/agent-runner.js +15 -1
- package/dist/agent-shared.d.ts +18 -0
- package/dist/agent.d.ts +66 -0
- package/dist/bootstrap-mode.d.ts +20 -0
- package/dist/captured-command.d.ts +58 -0
- package/dist/claude-call-aggregator.d.ts +164 -0
- package/dist/claude-call-aggregator.js +123 -17
- package/dist/claude-stream.d.ts +56 -0
- package/dist/claude-stream.js +23 -0
- package/dist/coding-agent.d.ts +263 -0
- package/dist/dependency-install.d.ts +111 -0
- package/dist/effort.d.ts +19 -0
- package/dist/embed.d.ts +4 -0
- package/dist/failure.d.ts +42 -0
- package/dist/follow-ups.d.ts +28 -0
- package/dist/frontend-infra.d.ts +25 -0
- package/dist/fs-utils.d.ts +2 -0
- package/dist/git.d.ts +394 -0
- package/dist/host-markdown.d.ts +28 -0
- package/dist/inline.d.ts +10 -0
- package/dist/job.d.ts +666 -0
- package/dist/logger.d.ts +16 -0
- package/dist/onboarding-preseed.d.ts +24 -0
- package/dist/package-registries.d.ts +32 -0
- package/dist/pi-workspace.d.ts +194 -0
- package/dist/pi-workspace.js +4 -0
- package/dist/pi.d.ts +475 -0
- package/dist/pr-description.d.ts +85 -0
- package/dist/pr-template.d.ts +101 -0
- package/dist/process-exit.d.ts +7 -0
- package/dist/process.d.ts +19 -0
- package/dist/progress-guard.d.ts +88 -0
- package/dist/progress.d.ts +87 -0
- package/dist/redact.d.ts +31 -0
- package/dist/reproduction-proof.d.ts +224 -0
- package/dist/runner.d.ts +282 -0
- package/dist/runner.js +3 -0
- package/dist/server.d.ts +3 -0
- package/dist/structured-output.d.ts +75 -0
- package/dist/subagents.d.ts +88 -0
- package/dist/subagents.js +74 -4
- package/dist/transcript-retention.d.ts +21 -0
- package/dist/validation-checks.d.ts +159 -0
- package/dist/vcs-api.d.ts +73 -0
- package/dist/version.d.ts +2 -0
- package/package.json +9 -5
- package/src/agent-runner.ts +21 -2
- package/src/claude-call-aggregator.ts +181 -32
- package/src/claude-stream.ts +21 -0
- package/src/pi-workspace.ts +4 -0
- package/src/runner.ts +24 -0
- package/src/subagents.ts +57 -3
package/README.md
CHANGED
|
@@ -206,6 +206,7 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
206
206
|
| `src/embed.ts` | Bundled assets/templates written into the workspace. |
|
|
207
207
|
| `src/package-registries.ts` | Private-registry (npm) auth: renders the job's allowlisted entries into an npmrc — the user `~/.npmrc` in a container, a per-job file pointed at by `npm_config_userconfig` for a native job. |
|
|
208
208
|
| `src/agent-runner.ts` | The subscription-harness runners (`runClaudeCode` / `runCodex`) — talk direct to the vendor with a leased OAuth token, lift per-turn usage/telemetry off the CLI event stream. |
|
|
209
|
+
| `src/claude-call-aggregator.ts` | Folds Claude Code's per-CONTENT-BLOCK `stream-json` envelopes back into the model calls they belong to (by `message.id`), reconstructs each call's request transcript, and routes subagent turns off the parent's chain. **Exported as the `./claude-call-aggregator` subpath and driven by the BACKEND too** (`runtimes/local`, for an inline step running on the developer's host `claude`), so it stays the ONE implementation — the per-envelope over-count it fixes inflated a measured 1.47M tokens to 5.53M, and both drivers have to learn that only once. That second driver is why the transcript is retained only to `MAX_TRANSCRIPT_CHARS` (stating what it stopped retaining) and why assembling bodies at all is a `bodies` switch: in a container the reconstruction is one job's memory in a box sized for it, in the backend it is per concurrent inline step in the orchestrator process. Unlike the compile-only `./embed`, this subpath is a `dist` import, which is why the package emits declarations — and why a consumer's typecheck depends on Turbo's `^build` edge having built this package first (see `tsconfig.json`'s `comment:buildOrder`). |
|
|
209
210
|
| `src/transcript-retention.ts` | Lifts the CLI session transcripts (`projects/` / `sessions/`) out of the isolated, credential-bearing config home before it is deleted, and prunes them on a TTL (debugging artifact retention). |
|
|
210
211
|
| `src/captured-command.ts` | The one way the harness runs a declared shell command on its own behalf: `sh -c` with a per-command watchdog, abort handling, conventional exit codes (124/127/130) and a scrub-then-bound output capture. Shared by both pre-PR verification phases so a fix to one cannot miss the other. |
|
|
211
212
|
| `src/dependency-install.ts` | Dependency prepopulation: `prepopulateDependencies` is the ONE seam every checkout-having mode calls — it runs the service's install command before the agent's first turn, excludes what the install materialised from git so no `git add -A` can sweep a dependency tree into the PR, and builds the prompt note describing the outcome. Best-effort — every failure shape becomes a note, never a failed job. Generic — keyed off the job body, never the agent kind. |
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** One materialisable resource file of a skill. */
|
|
2
|
+
export interface SkillResourceSpec {
|
|
3
|
+
/** Path within the skill directory, e.g. `templates/report.md` (subdirs preserved, no traversal). */
|
|
4
|
+
relPath: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A skill to make available for a run. Materialised HARNESS-AWARE:
|
|
9
|
+
* `CLAUDE_CONFIG_DIR/skills/<name>/SKILL.md` (+ resources) for the claude-code CLI to load
|
|
10
|
+
* natively, or `.cat-context/skill/<name>/<relPath>` for the Pi/codex checkout (their prompt
|
|
11
|
+
* carries the instructions). A dedicated top-level body field, never a context file.
|
|
12
|
+
*/
|
|
13
|
+
export interface SkillSpec {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
instructions: string;
|
|
17
|
+
resources: SkillResourceSpec[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A tool server (MCP) to wire into the agent CLI for this run, with its transport and any
|
|
21
|
+
* credentials the backend resolved. Only the CLIs that speak MCP receive these; the backend has
|
|
22
|
+
* already dropped anything this harness cannot serve, so the harness never has to decide.
|
|
23
|
+
*
|
|
24
|
+
* The values here are SECRET-BEARING (`env` / `headers` carry resolved credentials), which is why
|
|
25
|
+
* the config files this module writes always live outside the checkout and are never logged.
|
|
26
|
+
*/
|
|
27
|
+
export interface McpServerSpec {
|
|
28
|
+
/** The server name the CLI exposes tools under (`mcp__<id>__<tool>`). Id-safe by construction. */
|
|
29
|
+
id: string;
|
|
30
|
+
transport: 'stdio' | 'http';
|
|
31
|
+
command?: string;
|
|
32
|
+
args?: string[];
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
url?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
/** Bare tool names the agent may call. Absent ⇒ every tool the server exposes. */
|
|
37
|
+
allowedTools?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Which keys of `env` / `headers` hold a RESOLVED CREDENTIAL rather than declared configuration.
|
|
40
|
+
* {@link mcpServerSecretValues} reads exactly these for redaction — scrubbing the whole map
|
|
41
|
+
* instead would turn every later occurrence of an ordinary config string into `***`.
|
|
42
|
+
*/
|
|
43
|
+
secretKeys?: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The credential values carried by a run's tool servers, for {@link registerKnownSecrets}. An MCP
|
|
47
|
+
* server that fails to start routinely echoes its own argv or request headers into stderr, and
|
|
48
|
+
* that tail reaches the step's diagnostics — so these have to be scrubbed exactly like the leased
|
|
49
|
+
* subscription token. Only the keys the backend MARKED as secret are read (see `secretKeys`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function mcpServerSecretValues(servers: readonly McpServerSpec[]): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Validate the optional `skills` field. Names are de-duplicated: two skills sharing a directory
|
|
54
|
+
* name would overwrite each other's `SKILL.md`, leaving the agent pointed at whichever landed
|
|
55
|
+
* last — so the first wins and the collision is dropped rather than silently mixing two playbooks.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseSkillSpecs(value: unknown): SkillSpec[] | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* A safe MCP server id: it becomes a tool-name fragment AND a TOML table key.
|
|
60
|
+
*
|
|
61
|
+
* Kept byte-identical to kernel's `MCP_SERVER_ID_PATTERN` (the harness image is built from `src/`
|
|
62
|
+
* plus typescript alone, so it can carry no runtime dependency on a workspace package) and pinned
|
|
63
|
+
* against it by `test/agent-capabilities.conformity.test.ts` — the same copy-plus-pin arrangement
|
|
64
|
+
* `src/host-markdown.ts` uses.
|
|
65
|
+
*/
|
|
66
|
+
export declare const MCP_SERVER_ID_PATTERN: RegExp;
|
|
67
|
+
/**
|
|
68
|
+
* Whether an HTTP tool server's URL may be started. Mirrors kernel's `isAllowedMcpHttpUrl` (see
|
|
69
|
+
* {@link MCP_SERVER_ID_PATTERN} for why it is a copy, and the conformity suite that pins it):
|
|
70
|
+
* `https` anywhere, plain `http` only on loopback, since the headers carry a resolved credential.
|
|
71
|
+
* The backend refuses the same URLs at registration — this is the boundary check, so a body that
|
|
72
|
+
* reached the container by any other route is held to the rule too.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isAllowedMcpHttpUrl(raw: string): boolean;
|
|
75
|
+
/** Validate the optional `mcpServers` field, dropping malformed entries and duplicate ids. */
|
|
76
|
+
export declare function parseMcpServerSpecs(value: unknown): McpServerSpec[] | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The `--mcp-config` document Claude Code reads: `{ "mcpServers": { "<id>": {...} } }`. An `http`
|
|
79
|
+
* server declares `type: "http"` with its headers; a `stdio` one declares its command/args/env.
|
|
80
|
+
*/
|
|
81
|
+
export declare function claudeMcpConfig(servers: McpServerSpec[]): {
|
|
82
|
+
mcpServers: Record<string, Record<string, unknown>>;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The claude-code CLI's own tools, named so an `--allowedTools` list can never take them away.
|
|
86
|
+
*
|
|
87
|
+
* An allow-list is whole-session: it does not scope itself to MCP just because every entry we
|
|
88
|
+
* generate happens to be an `mcp__*` pattern. So the moment one tool server narrows its tools, the
|
|
89
|
+
* list has to re-grant the agent's built-in file/bash/search tools or the run is handed a narrowed
|
|
90
|
+
* MCP surface AND no way to read, edit or build anything.
|
|
91
|
+
*
|
|
92
|
+
* Bias this list toward OVER-inclusion. A name the CLI does not have is inert; a name it has and
|
|
93
|
+
* this list lacks is a tool silently removed from a run — which surfaces as an agent that cannot
|
|
94
|
+
* do its work, far from the registration that caused it. Historical/renamed spellings are kept for
|
|
95
|
+
* the same reason: the harness image is pinned per workspace, so one image faces several CLI
|
|
96
|
+
* versions. When the CLI gains a tool, add it here.
|
|
97
|
+
*/
|
|
98
|
+
export declare const CLAUDE_BUILT_IN_TOOLS: readonly string[];
|
|
99
|
+
/**
|
|
100
|
+
* The tool-name list for `--allowedTools`: every declared server's tools in the CLI's
|
|
101
|
+
* `mcp__<server>__<tool>` convention, PLUS {@link CLAUDE_BUILT_IN_TOOLS}. A server with no
|
|
102
|
+
* restriction contributes the whole-server pattern, so an allow-list stays one entry per server.
|
|
103
|
+
*
|
|
104
|
+
* Returns undefined when NO server restricts its tools — there is then nothing to narrow, and the
|
|
105
|
+
* safest list is the one we never send.
|
|
106
|
+
*
|
|
107
|
+
* Whether the CLI ENFORCES this list is permission-mode dependent and not a contract we control:
|
|
108
|
+
* the run uses `--permission-mode bypassPermissions` (the container is the sandbox and no human is
|
|
109
|
+
* there to approve a call), under which an allow-list grants rather than gates. So this is written
|
|
110
|
+
* to be correct under BOTH readings — if the list gates, the narrowing is real and the built-ins
|
|
111
|
+
* survive it; if it is inert, sending it costs nothing. The always-present channel is the PROMPT,
|
|
112
|
+
* which states each server's permitted tool names on every harness. Treat `allowedTools` as
|
|
113
|
+
* scoping, not as a security boundary: a server the agent must not reach fully should not be
|
|
114
|
+
* wired for that kind at all.
|
|
115
|
+
*/
|
|
116
|
+
export declare function claudeAllowedToolPatterns(servers: McpServerSpec[]): string[] | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* The `[mcp_servers.<id>]` TOML block Codex reads from its `CODEX_HOME/config.toml`. Codex's MCP
|
|
119
|
+
* client is stdio-only, so an `http` server is skipped here — the backend states such a server as
|
|
120
|
+
* unavailable when it declares `harnesses: ['claude-code']`, and a deployment that wires an HTTP
|
|
121
|
+
* server for Codex gets a no-op rather than a malformed config.
|
|
122
|
+
*/
|
|
123
|
+
export declare function codexMcpConfigToml(servers: McpServerSpec[]): string;
|
|
124
|
+
/**
|
|
125
|
+
* Write the Claude Code MCP config for this run and return its path, or undefined when there are
|
|
126
|
+
* no servers. The file is written into the caller's PER-RUN directory (an isolated config home, or
|
|
127
|
+
* an ambient job's own scratch dir) — never the checkout (it would land in a commit) and never a
|
|
128
|
+
* HOME-global path (a second concurrent job would clobber it, and it carries this job's credentials).
|
|
129
|
+
*/
|
|
130
|
+
export declare function writeClaudeMcpConfig(dir: string, servers: McpServerSpec[]): Promise<string | undefined>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Logger } from './logger.js';
|
|
2
|
+
import { type HarnessCallMetric, type PiRunOutcome, type TodoProgress } from './pi.js';
|
|
3
|
+
import { type McpServerSpec, type SkillSpec } from './agent-capabilities.js';
|
|
4
|
+
import { type ProgressGuardLimits } from './progress-guard.js';
|
|
5
|
+
import { type SliceReview } from './subagents.js';
|
|
6
|
+
/** Which subscription harness to run (the Pi harness uses `runPi` directly). */
|
|
7
|
+
export type SubscriptionHarness = 'claude-code' | 'codex';
|
|
8
|
+
export interface SubscriptionRunOptions {
|
|
9
|
+
/** Prepared working directory (cloned/scaffolded by the caller). */
|
|
10
|
+
cwd: string;
|
|
11
|
+
/** Real vendor model id, e.g. `claude-opus-4-8` / `gpt-5.5-codex`. */
|
|
12
|
+
model: string;
|
|
13
|
+
/** Composed role + best-practice fragments, supplied as the system prompt. */
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
/** The concrete task prompt handed to the CLI over stdin. */
|
|
16
|
+
userPrompt: string;
|
|
17
|
+
/**
|
|
18
|
+
* The decrypted subscription credential: an OAuth token (claude) or auth.json blob
|
|
19
|
+
* (codex). Omitted when `ambientAuth` is set — the CLI uses the developer's own login.
|
|
20
|
+
*/
|
|
21
|
+
subscriptionToken?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Anthropic-compatible base URL for a non-Anthropic Claude-Code vendor (GLM/Kimi).
|
|
24
|
+
* Present ⇒ ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN; absent ⇒ CLAUDE_CODE_OAUTH_TOKEN.
|
|
25
|
+
*/
|
|
26
|
+
subscriptionBaseUrl?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Native local execution: run the developer's ALREADY-INSTALLED CLI with its OWN
|
|
29
|
+
* ambient login (`~/.claude` / `~/.codex`) — no leased credential, no isolated config
|
|
30
|
+
* home. Set ONLY by the local native transport (which runs the harness as a host
|
|
31
|
+
* process); a no-op everywhere else. The agent then runs with the user's personal
|
|
32
|
+
* subscription, unsandboxed, on their own machine — the explicit trade for skipping the
|
|
33
|
+
* container.
|
|
34
|
+
*/
|
|
35
|
+
ambientAuth?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The skills to install natively before launch. The claude-code runner writes each to
|
|
38
|
+
* `CLAUDE_CONFIG_DIR/skills/<name>/SKILL.md` (+ resource files) so the CLI loads them — but ONLY
|
|
39
|
+
* when it owns an isolated config home, i.e. NOT under `ambientAuth`. The codex runner ignores
|
|
40
|
+
* them outright. Every case that skips the native install reads the checkout's
|
|
41
|
+
* `.cat-context/skill/<name>/`, materialised by the caller.
|
|
42
|
+
*/
|
|
43
|
+
skills?: SkillSpec[];
|
|
44
|
+
/**
|
|
45
|
+
* Tool servers (MCP) to wire into the CLI for this run. Written to a PER-RUN config the CLI is
|
|
46
|
+
* pointed at — never a HOME-global one, which a second concurrent job would clobber and which
|
|
47
|
+
* carries this job's credentials. Absent ⇒ the CLI's built-in tools only.
|
|
48
|
+
*/
|
|
49
|
+
mcpServers?: McpServerSpec[];
|
|
50
|
+
/**
|
|
51
|
+
* Extra environment for the CLI child, scoped to this job (the tester's secrets, a
|
|
52
|
+
* private-registry npmrc pointer). Merged over the inherited `process.env` at spawn, so the
|
|
53
|
+
* agent and its shell tools see them without the harness mutating its OWN environment — which
|
|
54
|
+
* is shared by every concurrent job under the native host-process transport. See
|
|
55
|
+
* `RunOptions.agentEnv`.
|
|
56
|
+
*/
|
|
57
|
+
extraEnv?: Record<string, string>;
|
|
58
|
+
/** Aborting this kills the CLI (the job's inactivity/max-duration watchdog). */
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
/**
|
|
61
|
+
* Fully-resolved no-progress guard limits (env defaults merged loosen-only with the kind's
|
|
62
|
+
* tuning + any complexity-scaled allowance). When set, the claude-code runner runs the SAME
|
|
63
|
+
* {@link ProgressGuard} as Pi over the CLI's tool stream and kills a run that has plainly
|
|
64
|
+
* stopped making progress (no-edit probing, error-retry loop, web rabbit-hole) rather than
|
|
65
|
+
* letting it burn the whole wall-clock budget. Omitted ⇒ the guard is disabled for this run
|
|
66
|
+
* (only the external watchdog bounds it), preserving the pre-guard behaviour.
|
|
67
|
+
*/
|
|
68
|
+
guardLimits?: ProgressGuardLimits;
|
|
69
|
+
/** Whether this run is expected to edit files (false for assess-only runs); gates the no-edit bound. */
|
|
70
|
+
expectsEdits?: boolean;
|
|
71
|
+
/** Called on every chunk of CLI output, so the watchdog sees the agent is alive. */
|
|
72
|
+
onActivity?: () => void;
|
|
73
|
+
/** Called with the latest subtask counts each time the CLI updates its todo/plan list. */
|
|
74
|
+
onProgress?: (progress: TodoProgress) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Called with the FULL set of per-slice reviews each time one lands, so the backend can persist
|
|
77
|
+
* a parallel review's completed work as it happens instead of only from the terminal result.
|
|
78
|
+
* A whole value rather than a delta: the set only grows and losing a finished slice's report to
|
|
79
|
+
* a dropped poll would defeat the point (see `SliceTracker.sliceReviews`).
|
|
80
|
+
*/
|
|
81
|
+
onSliceReviews?: (reviews: SliceReview[]) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Called with each per-call telemetry row as the CLI stream yields it, so the backend can
|
|
84
|
+
* record the run's model calls WHILE it runs instead of only from its terminal result. The
|
|
85
|
+
* same row still rides the result, so a lost poll response costs nothing.
|
|
86
|
+
*/
|
|
87
|
+
onCallMetric?: (call: HarnessCallMetric) => void;
|
|
88
|
+
/**
|
|
89
|
+
* The per-job child logger (jobId/repo/branch correlation). Threaded so the retained
|
|
90
|
+
* session-transcript path is logged for the run when the isolated config home is torn down.
|
|
91
|
+
*/
|
|
92
|
+
log?: Logger;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Decide how the Claude Code runner carries the composed system prompt. Small prompts ride
|
|
96
|
+
* `--append-system-prompt` (a real system turn, cacheable) as before; a prompt too large for a
|
|
97
|
+
* single argv string is instead folded into the stdin task prompt (like the Codex runner), which
|
|
98
|
+
* has no size ceiling. Pure so the branch is unit-testable without spawning the CLI.
|
|
99
|
+
*/
|
|
100
|
+
export declare function carryClaudeSystemPrompt(systemPrompt: string, userPrompt: string): {
|
|
101
|
+
appendArgs: string[];
|
|
102
|
+
prompt: string;
|
|
103
|
+
folded: boolean;
|
|
104
|
+
};
|
|
105
|
+
export declare function runClaudeCode(opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
|
|
106
|
+
/**
|
|
107
|
+
* Run the Codex CLI headlessly against `opts.cwd`, authenticated with the leased
|
|
108
|
+
* ChatGPT `auth.json` bundle written to an isolated CODEX_HOME, talking direct to
|
|
109
|
+
* the ChatGPT backend. Streams `codex exec --json`, mapping plan/todo updates onto
|
|
110
|
+
* subtask progress and the running cumulative token usage onto the outcome.
|
|
111
|
+
*/
|
|
112
|
+
export declare function runCodex(opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
|
|
113
|
+
/** Dispatch to the configured subscription harness runner. */
|
|
114
|
+
export declare function runSubscriptionHarness(harness: SubscriptionHarness, opts: SubscriptionRunOptions): Promise<PiRunOutcome>;
|
package/dist/agent-runner.js
CHANGED
|
@@ -384,7 +384,7 @@ export async function runClaudeCode(opts) {
|
|
|
384
384
|
// either/or; the plan then MERGES with the dispatch view (`mergeProgress`) rather than
|
|
385
385
|
// competing with it — picking the further-along view collapsed the list to the dispatched
|
|
386
386
|
// slices alone the moment the first subagent returned. See ./progress.ts.
|
|
387
|
-
const sliceTracker = createSliceTracker();
|
|
387
|
+
const sliceTracker = createSliceTracker(secrets);
|
|
388
388
|
const planTracker = createTaskPlanTracker();
|
|
389
389
|
let lastTodo;
|
|
390
390
|
const emitProgress = () => {
|
|
@@ -394,6 +394,17 @@ export async function runClaudeCode(opts) {
|
|
|
394
394
|
if (progress)
|
|
395
395
|
opts.onProgress(progress);
|
|
396
396
|
};
|
|
397
|
+
// Publish the per-slice reviews the tracker has captured. Separate from `emitProgress` because
|
|
398
|
+
// the two answer different questions and have different lifetimes: progress is a disposable
|
|
399
|
+
// count the UI renders, while these carry the slices' actual review WORK and are persisted so a
|
|
400
|
+
// run that dies before its aggregation can be resumed from them.
|
|
401
|
+
const emitSliceReviews = () => {
|
|
402
|
+
if (!opts.onSliceReviews)
|
|
403
|
+
return;
|
|
404
|
+
const reviews = sliceTracker.sliceReviews();
|
|
405
|
+
if (reviews.length > 0)
|
|
406
|
+
opts.onSliceReviews(reviews);
|
|
407
|
+
};
|
|
397
408
|
// No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
398
409
|
// absent on this path until now. Claude Code reports a tool CALL (its name) on the `assistant`
|
|
399
410
|
// turn and that call's RESULT (`is_error`) on the following `user` turn, so correlate them by
|
|
@@ -471,6 +482,9 @@ export async function runClaudeCode(opts) {
|
|
|
471
482
|
sliceTracker.onUser(content);
|
|
472
483
|
planTracker.onUser(content);
|
|
473
484
|
emitProgress();
|
|
485
|
+
// A slice's report lands on exactly this turn, so publish here: waiting for the next
|
|
486
|
+
// progress tick would risk the job dying with the report captured but never surfaced.
|
|
487
|
+
emitSliceReviews();
|
|
474
488
|
// Not on the at-close flush: the CLI has already exited, so tripping the guard there
|
|
475
489
|
// would kill nothing and only convert a clean exit into a spurious failure.
|
|
476
490
|
if (!meta?.final)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentJob, AgentResult, McpServerSpec, SkillSpec } from './job.js';
|
|
2
|
+
import type { EffortReport } from './effort.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fold an agent's effort self-assessment (lifted from its sentinel file by `runAgentInWorkspace`)
|
|
5
|
+
* onto its final result. Every container mode routes its result through this so the report reaches
|
|
6
|
+
* the backend uniformly. A run that wrote no report passes through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mergeEffort(result: AgentResult, effortReport: EffortReport | undefined): AgentResult;
|
|
9
|
+
/**
|
|
10
|
+
* The agent-capability fields (skills + tool servers) every agent-running flow forwards to
|
|
11
|
+
* {@link runAgentInWorkspace}. One helper rather than a per-flow spread, so a flow cannot silently
|
|
12
|
+
* be the one that drops a kind's declared playbook or tool server — the failure mode is invisible
|
|
13
|
+
* (the agent simply works without it) and would only show up as degraded output.
|
|
14
|
+
*/
|
|
15
|
+
export declare function agentCapabilities(job: AgentJob): {
|
|
16
|
+
skills?: SkillSpec[];
|
|
17
|
+
mcpServers?: McpServerSpec[];
|
|
18
|
+
};
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AgentJob, AgentResult, TestSecretSpec } from './job.js';
|
|
2
|
+
import { runCodingAgent } from './coding-agent.js';
|
|
3
|
+
import type { RunOptions } from './runner.js';
|
|
4
|
+
/**
|
|
5
|
+
* Build the dynamic infra notes appended to the agent's user prompt from a stand-up outcome.
|
|
6
|
+
* A stand-up problem (a failed build / compose) is flagged as a concern to test around; a
|
|
7
|
+
* frontend serve URL points the UI tester at the app that was just built + served and pre-empts
|
|
8
|
+
* a live-backend CORS failure being mis-reported as an app defect. Pure (no IO) so the exact
|
|
9
|
+
* wording + ordering is unit-tested; returns the notes in order (problem first, serve URL next).
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildInfraNotes(managed: {
|
|
12
|
+
note?: string;
|
|
13
|
+
serveUrl?: string;
|
|
14
|
+
}): string[];
|
|
15
|
+
/** Run one generic agent job end to end, dispatching on `mode`. */
|
|
16
|
+
export declare function handleAgent(job: AgentJob, opts?: RunOptions): Promise<AgentResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Decide a preview stand-up's outcome from its result (pure, so the success/failure boundary
|
|
19
|
+
* is unit-tested without spawning a build). A preview must actually come up: unlike the tester's
|
|
20
|
+
* "test what you can" fallback, a stand-up that produced no reachable serve URL (failed build /
|
|
21
|
+
* server never bound) is a hard failure and its `note` becomes the failure reason. When the app
|
|
22
|
+
* is up but WireMock is not, the `note` rides along as a non-fatal warning.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildPreviewOutcome(standUp: {
|
|
25
|
+
serveUrl?: string;
|
|
26
|
+
note?: string;
|
|
27
|
+
}): {
|
|
28
|
+
ok: true;
|
|
29
|
+
url: string;
|
|
30
|
+
note?: string;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
error: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Build the env carrying the tester's sensitive secrets, so the agent's shell tools (spawned as
|
|
37
|
+
* child processes that inherit it) can read `$KEY` — the out-of-band delivery channel. Each value
|
|
38
|
+
* is registered for redaction so it can't leak into captured output/logs. Reserved/toolchain env
|
|
39
|
+
* names were already dropped at parse. No secrets ⇒ an empty env.
|
|
40
|
+
*
|
|
41
|
+
* Returned as EXPLICIT child env rather than written onto `process.env`: a process-global
|
|
42
|
+
* set/restore is only safe when the process runs one job, which the native host-process transport
|
|
43
|
+
* breaks (it serves every concurrent ambient job from one process). There, two overlapping tester
|
|
44
|
+
* runs would read each other's secrets, and whichever finished first would delete the other's
|
|
45
|
+
* mid-run. Scoping them to the spawn env makes the delivery correct under concurrency and drops
|
|
46
|
+
* the restore step entirely.
|
|
47
|
+
*/
|
|
48
|
+
export declare function testSecretEnv(secrets: TestSecretSpec[] | undefined): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Whether a Ralph iteration ({@link AgentJob.validation} set) landed on a MULTI-REPO job (writable
|
|
51
|
+
* peer repos or read-only reference repos). The post-commit validation command is only wired into
|
|
52
|
+
* the single-repo flow, so a multi-repo run would silently skip it and degenerate the loop into a
|
|
53
|
+
* one-shot with no completion gate — multi-repo ralph is out of scope for v1 (see
|
|
54
|
+
* backend/docs/ralph-loop.md), so {@link runCodingMode} fails loudly on this instead.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ralphUnsupportedOnMultiRepo(job: Pick<AgentJob, 'validation' | 'peerRepos' | 'referenceRepos'>): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Assemble the {@link runCodingAgent} spec for the ordinary single-repo coding flow. Extracted
|
|
59
|
+
* from {@link runSingleRepoCoding} so the many optional-field spreads don't inflate that
|
|
60
|
+
* function's cyclomatic complexity; the mapping is a straight field copy off `job`.
|
|
61
|
+
*
|
|
62
|
+
* Exported for the `opensPr` assertion: whether a dispatch fills the repo's PR template turns on
|
|
63
|
+
* this one spread, and the in-place fixers reach it through the SAME function as the implementer,
|
|
64
|
+
* so no structural guard can tell their cases apart.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildSingleRepoCodingSpec(job: AgentJob, pushBranch: string): Parameters<typeof runCodingAgent>[0];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentJob, AgentResult } from './job.js';
|
|
2
|
+
import type { RunOptions } from './runner.js';
|
|
3
|
+
/**
|
|
4
|
+
* Repo-bootstrap coding flow (the bootstrapper): with a reference architecture, clone it →
|
|
5
|
+
* the agent adapts it in place per the instructions; without one (`fromScratch`), start from
|
|
6
|
+
* an empty directory → the agent scaffolds the new service. Either way the result's history
|
|
7
|
+
* is reset to a single commit and force-pushed to the SEPARATE, pre-created target repo's
|
|
8
|
+
* default branch. Diverges from the ordinary coding flow in pushing to a different repo with
|
|
9
|
+
* a reinitialised history rather than a work branch + PR on the cloned repo.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runBootstrap(job: AgentJob, opts: RunOptions): Promise<AgentResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the bootstrapper actually produced repository content, so a no-op run (the agent
|
|
14
|
+
* never reached the model / never wrote anything) is failed rather than force-pushed as an
|
|
15
|
+
* empty repo. With a reference architecture, "produced content" means the agent changed the
|
|
16
|
+
* clone; scaffolding from scratch, it means at least one file now exists in the working
|
|
17
|
+
* directory. (The harness writes its prompt context to Pi's global `~/.pi/agent/AGENTS.md`,
|
|
18
|
+
* never into `dir`, so nothing here needs to be filtered out as harness boilerplate.)
|
|
19
|
+
*/
|
|
20
|
+
export declare function producedRepoContent(dir: string, hasReference: boolean, signal?: AbortSignal): Promise<boolean>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RunOptions } from './runner.js';
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
/** What one harness-spawned command did, as both phases record it. */
|
|
4
|
+
export interface CapturedCommandResult {
|
|
5
|
+
/** Exit code (0 = pass); 124 on watchdog timeout, 127 on spawn failure, 130 on abort. */
|
|
6
|
+
exitCode: number;
|
|
7
|
+
passed: boolean;
|
|
8
|
+
/** Scrubbed output bounded to the caller's REPORT budget (what crosses the wire). */
|
|
9
|
+
outputTail?: string;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
timedOut?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The FULL scrubbed tail (up to {@link MAX_CAPTURED_OUTPUT_CHARS}) for a repair prompt. Never
|
|
14
|
+
* leaves the container — the agent needs the whole failure to act on it, the wire does not.
|
|
15
|
+
*/
|
|
16
|
+
fullTail?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run ONE command as `sh -c` in `cwd`, capturing a bounded, secret-scrubbed tail of its combined
|
|
20
|
+
* stdout+stderr. The exit code is the verdict — computed here by the harness, never self-reported
|
|
21
|
+
* by the model, which is the whole point of a programmatic phase. A watchdog kills the process
|
|
22
|
+
* tree on timeout and an aborted run resolves non-zero, so a phase is never what blocks a job
|
|
23
|
+
* from settling.
|
|
24
|
+
*
|
|
25
|
+
* The child inherits the JOB's environment (`RunOptions.agentEnv` layered over the process env),
|
|
26
|
+
* not a mutated global: the harness spawns this itself rather than through the agent, so without
|
|
27
|
+
* the explicit merge a native-mode job would run without the private-registry npmrc pointer (and,
|
|
28
|
+
* had this been staged in `process.env`, against a sibling job's state).
|
|
29
|
+
*
|
|
30
|
+
* `logLabel`/`logFields` shape only the two warnings this runner emits itself (the watchdog kill
|
|
31
|
+
* and a spawn failure); the caller keeps its own start/finish logging, which knows what the
|
|
32
|
+
* command MEANS.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runCapturedCommand(args: {
|
|
35
|
+
cwd: string;
|
|
36
|
+
command: string;
|
|
37
|
+
timeoutMs: number;
|
|
38
|
+
/** Bound for {@link CapturedCommandResult.outputTail} — the caller's per-report budget. */
|
|
39
|
+
reportTailChars: number;
|
|
40
|
+
logLabel: string;
|
|
41
|
+
logFields?: Record<string, unknown>;
|
|
42
|
+
logger: Logger;
|
|
43
|
+
opts: RunOptions;
|
|
44
|
+
}): Promise<CapturedCommandResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Wrap captured command output in a fenced block that the output itself cannot break out of.
|
|
47
|
+
*
|
|
48
|
+
* Every consumer of a captured tail embeds it in markdown a MODEL then reads — a repair prompt,
|
|
49
|
+
* the dependency-install note — and a package manager legitimately prints backticks (a linter
|
|
50
|
+
* quoting a template literal, a test echoing a fenced snippet from a fixture). A fixed three-tick
|
|
51
|
+
* fence closes on the first such run, and everything after it reads as prose: the remaining
|
|
52
|
+
* output, and worse, the INSTRUCTIONS that follow the block. Sizing the fence one tick longer than
|
|
53
|
+
* the longest run in the body is what CommonMark specifies for exactly this, so the block always
|
|
54
|
+
* spans the whole tail.
|
|
55
|
+
*/
|
|
56
|
+
export declare function fencedOutput(text: string): string;
|
|
57
|
+
/** Bound an already-scrubbed output tail to what a REPORT carries, saying what it dropped. */
|
|
58
|
+
export declare function boundTail(scrubbed: string, maxChars: number): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { HarnessCallMetric } from './pi.js';
|
|
2
|
+
/** One model call, assembled from every stream envelope that carried a piece of it. */
|
|
3
|
+
export interface AggregatedClaudeCall {
|
|
4
|
+
model?: string;
|
|
5
|
+
/** Every content block of the response, in arrival order. */
|
|
6
|
+
content: unknown[];
|
|
7
|
+
text: string;
|
|
8
|
+
reasoning: string;
|
|
9
|
+
stopReason: string | null;
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
cacheReadTokens: number;
|
|
12
|
+
cacheWriteTokens: number;
|
|
13
|
+
outputTokens: number;
|
|
14
|
+
/** The `user` turns carrying this call's tool_result blocks, in arrival order. */
|
|
15
|
+
toolResults: unknown[][];
|
|
16
|
+
/** tool_use blocks across the whole response (the run's `stats.toolCalls` term). */
|
|
17
|
+
toolUses: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ClaudeCallAggregator {
|
|
20
|
+
/**
|
|
21
|
+
* Fold one `assistant` envelope in. A new `message.id` completes the call in flight first, so
|
|
22
|
+
* `onCallStart` for the new call always runs after `onCall` for the previous one.
|
|
23
|
+
*/
|
|
24
|
+
onAssistant(message: Record<string, unknown>): void;
|
|
25
|
+
/** Buffer a `user` turn's content against the call in flight (dropped when none is). */
|
|
26
|
+
onToolResult(content: unknown[]): void;
|
|
27
|
+
/** Complete the call still in flight, if any. Call once the stream has ended. */
|
|
28
|
+
flush(): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Assemble per-call telemetry out of Claude Code's per-block stream envelopes.
|
|
32
|
+
*
|
|
33
|
+
* `onCallStart` fires when a call's FIRST envelope arrives, which is the moment the caller must
|
|
34
|
+
* snapshot the prompt: the history at that point is what produced the response. `onCall` fires
|
|
35
|
+
* once the call is complete (a different `message.id` began, or the stream ended).
|
|
36
|
+
*
|
|
37
|
+
* Usage is merged as the MAXIMUM of each bucket across the call's envelopes rather than the last
|
|
38
|
+
* one seen. The envelopes carry a snapshot of the same call's usage, and which of them holds the
|
|
39
|
+
* final output count is a CLI detail we should not depend on; a max is right whether the value is
|
|
40
|
+
* repeated verbatim or grows.
|
|
41
|
+
*
|
|
42
|
+
* An envelope with no `message.id` cannot be attributed, so it is treated as a call of its own —
|
|
43
|
+
* the pre-aggregation behaviour, kept so a CLI build (or a transcript) that omits the id degrades
|
|
44
|
+
* to over-counting rather than to silently merging unrelated calls.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createClaudeCallAggregator(handlers: {
|
|
47
|
+
onCallStart?: () => void;
|
|
48
|
+
onCall: (call: AggregatedClaudeCall) => void;
|
|
49
|
+
}): ClaudeCallAggregator;
|
|
50
|
+
/** One turn of the reconstructed request transcript, in the proxy's chat-array shape. */
|
|
51
|
+
interface TranscriptTurn {
|
|
52
|
+
role: string;
|
|
53
|
+
content: unknown;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How much reconstructed transcript ONE conversation may retain.
|
|
57
|
+
*
|
|
58
|
+
* The stream feeds this without limit — a tool loop that reads large files grows the history by
|
|
59
|
+
* every one of them — and the reconstruction is held in the driver's own process. In the container
|
|
60
|
+
* that is a box sized for one job; in the BACKEND it is the orchestrator, where `streamCli` already
|
|
61
|
+
* refuses to retain the raw stream for exactly this reason (`harnessInline.ts` →
|
|
62
|
+
* `OUTPUT_TAIL_RETAIN_CHARS`: "a stalled tool-using run would otherwise park hundreds of MB in the
|
|
63
|
+
* orchestrator process — precisely on the runs worth diagnosing").
|
|
64
|
+
*
|
|
65
|
+
* 512 KiB because that is `LlmObservabilityService.MAX_BODY_CHARS`, the point past which the store
|
|
66
|
+
* truncates a body anyway: retaining more can only ever be thrown away. Deliberately NOT a
|
|
67
|
+
* per-deployment knob — it bounds a memory fault, and a number an operator can raise is one an
|
|
68
|
+
* operator can raise until the process dies.
|
|
69
|
+
*/
|
|
70
|
+
export declare const MAX_TRANSCRIPT_CHARS: number;
|
|
71
|
+
/** The per-call telemetry the Claude Code stream yields, assembled behind one small surface. */
|
|
72
|
+
export interface ClaudeStreamTelemetry {
|
|
73
|
+
/** Fold an `assistant` envelope in (parent-loop turns only — see {@link isSubagentEvent}). */
|
|
74
|
+
onAssistant(message: Record<string, unknown>): void;
|
|
75
|
+
/** Fold a `user` turn's tool_result content in, against the call in flight. */
|
|
76
|
+
onToolResult(content: unknown[]): void;
|
|
77
|
+
/** Publish the call still in flight. Idempotent; safe to call on both the clean and error path. */
|
|
78
|
+
flush(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Assemble ONE conversation's per-call telemetry from the CLI stream: the growing request
|
|
82
|
+
* transcript and the per-call token/body metrics.
|
|
83
|
+
*
|
|
84
|
+
* Owns the transcript because the two are one concern — a call's `promptText` is the transcript as
|
|
85
|
+
* of that call, and its turns may only be appended once the call that produced them is complete.
|
|
86
|
+
* `seed` is what the harness supplied and the stream therefore never shows (the system + first user
|
|
87
|
+
* message, or the single folded user turn), so the reconstruction never claims a system turn that
|
|
88
|
+
* was not sent. A subagent's conversation seeds EMPTY — its prompt was minted by the CLI and never
|
|
89
|
+
* crosses this stream — which is also why its first call carries `messageCount: 0` (the backend's
|
|
90
|
+
* `latestChainTip` skips those on purpose: there is no re-sendable chain to delta against).
|
|
91
|
+
* Bodies are credential-scrubbed; they can echo the leased token — and assembled at all only when
|
|
92
|
+
* {@link ClaudeStreamTelemetryOptions.bodies} says a driver has somewhere to put them. The
|
|
93
|
+
* transcript is bounded either way ({@link MAX_TRANSCRIPT_CHARS}).
|
|
94
|
+
*
|
|
95
|
+
* Deliberately does NOT touch {@link PiRunStats}: the run's tool/output counters describe whether
|
|
96
|
+
* the agent ACTED at all (`agentNeverActed`), which is true of a subagent's turns whichever channel
|
|
97
|
+
* ends up owning their telemetry rows. The caller accumulates them off the raw stream instead.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createClaudeStreamTelemetry(opts: ClaudeStreamTelemetryOptions): ClaudeStreamTelemetry;
|
|
100
|
+
/** How one conversation's per-call telemetry is assembled. */
|
|
101
|
+
export interface ClaudeStreamTelemetryOptions {
|
|
102
|
+
seed: TranscriptTurn[];
|
|
103
|
+
secrets: string[];
|
|
104
|
+
publish: (metric: HarnessCallMetric) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Whether to assemble the prompt/response BODIES at all. Absent ⇒ true (the container harness,
|
|
107
|
+
* whose job result carries them).
|
|
108
|
+
*
|
|
109
|
+
* `false` for a driver whose store will drop them — the backend with `LLM_RECORD_PROMPTS` off —
|
|
110
|
+
* where reconstructing a transcript per call is pure cost. Token counts, `messageCount` and
|
|
111
|
+
* finish reasons are unaffected: only the bodies go.
|
|
112
|
+
*/
|
|
113
|
+
bodies?: boolean;
|
|
114
|
+
/** Retention bound override (tests). Absent ⇒ {@link MAX_TRANSCRIPT_CHARS}. */
|
|
115
|
+
maxTranscriptChars?: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The dispatch (`Agent`/`Task` tool_use) id a stream envelope is tagged with, or `undefined` for a
|
|
119
|
+
* parent-loop turn.
|
|
120
|
+
*
|
|
121
|
+
* Claude Code streams the turns of the subagents it dispatches onto the parent's stdout, tagged
|
|
122
|
+
* with the tool_use id that spawned them. Those same turns are also written to the per-session
|
|
123
|
+
* `subagents/*.jsonl` transcripts the watcher reads, so recording both channels counted every
|
|
124
|
+
* subagent call twice — and splicing them into the parent's message reconstruction produced a
|
|
125
|
+
* `promptText` chain that interleaves several conversations and therefore matches no real request.
|
|
126
|
+
*
|
|
127
|
+
* The id is what makes the fallback below possible: concurrent subagents interleave on one stdout,
|
|
128
|
+
* so it is the ONLY thing separating their conversations.
|
|
129
|
+
*/
|
|
130
|
+
export declare function subagentDispatchId(event: Record<string, unknown>): string | undefined;
|
|
131
|
+
/** Whether a stream envelope describes a SUBAGENT's turn rather than the parent loop's. */
|
|
132
|
+
export declare function isSubagentEvent(event: Record<string, unknown>): boolean;
|
|
133
|
+
/** All per-call telemetry for ONE claude-code run: the parent loop, and whoever bills the subagents. */
|
|
134
|
+
export interface ClaudeRunTelemetry {
|
|
135
|
+
/** Fold an `assistant` envelope in, routed by its dispatch tag (`undefined` ⇒ the parent loop). */
|
|
136
|
+
onAssistant(dispatchId: string | undefined, message: Record<string, unknown>): void;
|
|
137
|
+
/** Fold a `user` turn's tool_result content in, against the same conversation. */
|
|
138
|
+
onToolResult(dispatchId: string | undefined, content: unknown[]): void;
|
|
139
|
+
/** Publish every conversation's call in flight. Idempotent; safe on the clean and error paths. */
|
|
140
|
+
flush(): void;
|
|
141
|
+
/**
|
|
142
|
+
* Subagent turns crossed the stream AND the watcher was the channel meant to record them — so a
|
|
143
|
+
* watcher that captured nothing means this run's subagent rows are simply missing.
|
|
144
|
+
*/
|
|
145
|
+
expectsWatcherCalls(): boolean;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Assemble a run's per-call telemetry, routing each envelope to the conversation it belongs to.
|
|
149
|
+
*
|
|
150
|
+
* The routing is the whole point. A subagent's turns ride the parent's stdout tagged with the
|
|
151
|
+
* dispatch that spawned them, and they must never join the PARENT's chain — that splice produced a
|
|
152
|
+
* `promptText` interleaving several conversations, matching no request that was ever sent.
|
|
153
|
+
*
|
|
154
|
+
* Who RECORDS them is a separate question, decided once per run rather than per event:
|
|
155
|
+
* `watcherOwnsSubagents` says a `subagents/*.jsonl` watcher will run, and it is the better source
|
|
156
|
+
* (it reads the settled transcript, so its usage and stop reason are final). With no watcher — an
|
|
157
|
+
* `ambientAuth` run has no isolated config home to watch — the tagged turns are recorded here
|
|
158
|
+
* instead, on per-dispatch transcripts of their own. Dropping them in that case would leave the run
|
|
159
|
+
* billed by neither channel, and an under-count reads as a cheap run rather than as an error.
|
|
160
|
+
*/
|
|
161
|
+
export declare function createClaudeRunTelemetry(opts: ClaudeStreamTelemetryOptions & {
|
|
162
|
+
watcherOwnsSubagents: boolean;
|
|
163
|
+
}): ClaudeRunTelemetry;
|
|
164
|
+
export {};
|