@cat-factory/executor-harness 1.94.0 → 1.98.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 +16 -12
- package/dist/agent-capabilities.d.ts +61 -0
- package/dist/agent-capabilities.js +113 -0
- package/dist/agent-runner.d.ts +30 -2
- package/dist/agent-runner.js +146 -47
- package/dist/bootstrap-mode.js +1 -0
- package/dist/coding-agent.d.ts +2 -1
- package/dist/embed.d.ts +2 -1
- package/dist/embed.js +2 -1
- package/dist/failure.d.ts +19 -1
- package/dist/failure.js +40 -0
- package/dist/git.d.ts +6 -0
- package/dist/git.js +16 -9
- package/dist/inline.d.ts +6 -0
- package/dist/inline.js +6 -0
- package/dist/job.d.ts +2 -1
- package/dist/jsonl-stream.d.ts +70 -0
- package/dist/jsonl-stream.js +149 -0
- package/dist/pi-reduction.d.ts +136 -0
- package/dist/pi-reduction.js +303 -0
- package/dist/pi-workspace.d.ts +2 -1
- package/dist/pi-workspace.js +11 -1
- package/dist/pi.d.ts +8 -81
- package/dist/pi.js +124 -310
- package/dist/runner.d.ts +53 -0
- package/dist/runner.js +53 -3
- package/dist/structured-output.js +2 -1
- package/dist/tool-silence.d.ts +74 -0
- package/dist/tool-silence.js +99 -0
- package/package.json +4 -4
- package/src/agent-capabilities.ts +163 -0
- package/src/agent-runner.ts +185 -47
- package/src/agent.ts +1 -1
- package/src/bootstrap-mode.ts +2 -1
- package/src/coding-agent.ts +2 -1
- package/src/embed.ts +8 -5
- package/src/failure.ts +36 -9
- package/src/git.ts +17 -9
- package/src/inline.ts +6 -0
- package/src/job.ts +2 -1
- package/src/jsonl-stream.ts +149 -0
- package/src/pi-reduction.ts +359 -0
- package/src/pi-workspace.ts +12 -3
- package/src/pi.ts +144 -349
- package/src/runner.ts +116 -4
- package/src/structured-output.ts +2 -1
- package/src/tool-silence.ts +125 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @cat-factory/executor-harness
|
|
2
2
|
|
|
3
3
|
The payload that runs **inside** a per-run Cloudflare Container (or a
|
|
4
|
-
[self-hosted runner](
|
|
4
|
+
[self-hosted runner](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/runner-pool-integration.md)) to perform real
|
|
5
5
|
repo work with the [Pi coding agent](https://github.com/earendil-works/pi).
|
|
6
6
|
|
|
7
7
|
It is a thin TypeScript wrapper (a `node:http` server on `:8080`) that the
|
|
@@ -33,7 +33,7 @@ replayed `POST` **re-attaches** to the running job rather than starting a
|
|
|
33
33
|
duplicate (the durable driver's retries/replays are safe). Pi's todo-tool counts
|
|
34
34
|
are surfaced as `progress` while a job runs. The exact request/response shapes
|
|
35
35
|
cat-factory sends are documented in
|
|
36
|
-
[`docs/runner-pool-integration.md`](
|
|
36
|
+
[`docs/runner-pool-integration.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/runner-pool-integration.md).
|
|
37
37
|
|
|
38
38
|
`GET /jobs/{id}` is also the harness's observability channel: `spans`, `followUps`
|
|
39
39
|
and `callMetrics` are **drain-on-read**; each poll returns what accumulated since
|
|
@@ -62,7 +62,7 @@ The implementation job (`POST /run`) is the canonical sequence:
|
|
|
62
62
|
body's `proxyPhasePath` says the backend serves it, which is how a repair round's model spend
|
|
63
63
|
stays distinguishable from the first pass's in telemetry; without that flag the plain path is
|
|
64
64
|
used and the calls are recorded as unattributed
|
|
65
|
-
(see [token-burn instrumentation](
|
|
65
|
+
(see [token-burn instrumentation](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/token-burn-instrumentation.md)),
|
|
66
66
|
3. **prepopulate dependencies**, when the job body carries `dependencyInstall`: the
|
|
67
67
|
service's install command is run with `sh -c` in the checkout BEFORE the agent starts, so
|
|
68
68
|
it reads real installed packages instead of inferring a library's capabilities from a
|
|
@@ -71,7 +71,7 @@ The implementation job (`POST /run`) is the canonical sequence:
|
|
|
71
71
|
steps 6 and 7, which start a fresh agent) and the run continues either way. Whatever the
|
|
72
72
|
install materialises is excluded from git first, so no later `git add -A` can sweep a
|
|
73
73
|
dependency tree into the pull request (see
|
|
74
|
-
[dependency prepopulation](
|
|
74
|
+
[dependency prepopulation](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/agent-dependency-prepopulation.md)),
|
|
75
75
|
4. **resolve the repo's pull-request template**, when this dispatch opens a PR (`src/pr-template.ts`):
|
|
76
76
|
`.github/PULL_REQUEST_TEMPLATE.md` and its root/`docs/`/multi-template-directory variants, or
|
|
77
77
|
GitLab's `.gitlab/merge_request_templates/`, read straight off the checkout (a symlinked template
|
|
@@ -85,11 +85,11 @@ The implementation job (`POST /run`) is the canonical sequence:
|
|
|
85
85
|
6. **validate** the checkout, when the job body carries `validationChecks`: the service's
|
|
86
86
|
configured check commands (install/lint/test/build) run with `sh -c` in the checkout, and
|
|
87
87
|
while they fail and the attempt budget remains the agent is re-run with the captured output
|
|
88
|
-
as its instruction (see [pre-PR validation](
|
|
88
|
+
as its instruction (see [pre-PR validation](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/pre-pr-validation.md)),
|
|
89
89
|
7. **prove the reproduction**, when the job body carries `reproduction`: the declared check is
|
|
90
90
|
run against the pre-fix tree and the tree the PR will open from, in two freshly-created
|
|
91
91
|
symmetric `git worktree` checkouts, and only red-then-green is reported as proof (see
|
|
92
|
-
[bugfix reproduction proof](
|
|
92
|
+
[bugfix reproduction proof](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/adr/0033-bugfix-reproduction-proof.md)). Unlike
|
|
93
93
|
step 6 this NEVER gates the PR: a failed verification is fed back to the agent while budget
|
|
94
94
|
remains, then recorded as `inconclusive`. It runs BEFORE step 6 so validation stays the last
|
|
95
95
|
thing to touch the tree,
|
|
@@ -120,7 +120,7 @@ PR. Blueprint **commits onto a branch** (no history reset) and returns the tree.
|
|
|
120
120
|
A job body may carry `skills[]` (procedural playbooks) and `mcpServers[]` (MCP tool servers): the
|
|
121
121
|
harness MATERIALISES both and decides nothing about them; the backend has already resolved which
|
|
122
122
|
apply and dropped what this harness cannot serve (see
|
|
123
|
-
[`backend/docs/adr/0029-agent-kind-capabilities.md`](
|
|
123
|
+
[`backend/docs/adr/0029-agent-kind-capabilities.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/adr/0029-agent-kind-capabilities.md)).
|
|
124
124
|
|
|
125
125
|
- **Skills** install natively under `CLAUDE_CONFIG_DIR/skills/<name>/` for a leased-credential
|
|
126
126
|
claude-code run (the CLI discovers and invokes them), and under
|
|
@@ -210,9 +210,12 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
210
210
|
| File | Responsibility |
|
|
211
211
|
| ------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
212
212
|
| `src/server.ts` | HTTP entry point; routes `/health`, `/run`, `/bootstrap`, `/blueprint`, `/jobs/{id}`. |
|
|
213
|
-
| `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking. |
|
|
213
|
+
| `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking, and the three per-job watchdogs (max-duration, inactivity, tool-silence). |
|
|
214
|
+
| `src/jsonl-stream.ts` | The BOUNDS on a child CLI's streams, shared by both runners: `JsonlLineReader` frames its JSONL stdout while refusing to buffer a runaway record, `BoundedTail` keeps a capped tail of raw output for failure quoting. Both watchdog timers and the poll endpoints share one event loop with this parsing, so an unbounded buffer here is how a container stops answering polls with no watchdog having fired. |
|
|
214
215
|
| `src/job.ts` | Request types + validators for the job specs. |
|
|
215
216
|
| `src/pi.ts` | Pi provider config, non-interactive run, JSON-line event + todo-progress parsing, global `AGENTS.md` guidance. |
|
|
217
|
+
| `src/pi-reduction.ts` | Reducing a Pi event stream to what the run PRODUCED (summary, stats, diagnostics, terminal failure), FOLDED as records stream rather than over a retained array — memory is O(largest record), not O(records). The array-taking entry points offline tooling uses are defined in terms of the same reducer. |
|
|
218
|
+
| `src/tool-silence.ts` | The tool-silence watchdog (F13) and the `ToolProgressWindow` an agent stream opens, beats and closes. Separate from the phase marker on purpose: a window is only meaningful while something able to reset it is running. |
|
|
216
219
|
| `src/git.ts` | clone / branch / commit / push + GitHub PR creation; bootstrap history reset + force-push. |
|
|
217
220
|
| `src/bootstrap.ts` | The `/bootstrap` handler (clone-or-empty → adapt → reinit + force-push). |
|
|
218
221
|
| `src/blueprint.ts` | The `/blueprint` handler (decompose → render `blueprints/` → commit on branch). |
|
|
@@ -240,6 +243,7 @@ runner):
|
|
|
240
243
|
| `PORT` | `8080` | HTTP port the harness listens on. |
|
|
241
244
|
| `JOB_MAX_DURATION_MS` | `3600000` (60m) | Hard ceiling on a job's wall-clock time; force-fails after. |
|
|
242
245
|
| `JOB_INACTIVITY_MS` | `600000` (10m) | Kills a hung agent that produces no output for this long. |
|
|
246
|
+
| `JOB_TOOL_SILENCE_MS` | half `JOB_MAX_DURATION_MS` (30m at its default) | Kills an agent that keeps producing output but completes no tool call for this long: the "chatty hang" neither watchdog above can see, since streamed output resets the inactivity timer on every chunk while nothing gets done (stuck-run audit F13). Armed ONLY while an agent CLI that reports completed tool calls is running (each runner opens its own window and closes it on exit), so clone / dependency install / push / a validation loop's check commands sit outside it — they are activity-silent by nature and bounded by their own per-command timeouts — and each repair pass opens a fresh window. Derived from the job ceiling rather than fixed. It fires only when output arrived during the window that elapsed, which is what leaves a genuinely quiet run to `JOB_INACTIVITY_MS` and its clearer diagnostic. `0` disables it. |
|
|
243
247
|
| `JOB_MAX_CONSECUTIVE_MCP_CALLS` | `40` | Consecutive tool-server (`mcp__*`) calls with no other tool call between before the run counts as a lookup loop. The counter-bound the no-edit exemption above owes; a per-kind `tuning.guardLimits` entry can only RAISE it. |
|
|
244
248
|
| `JOB_MAX_CONSECUTIVE_NON_ACTION_CALLS` | `200` | Consecutive calls of ANY no-edit-exempt family (reads, searches, web, tool servers, subagent dispatches) with no action call between them. The backstop above the per-family caps, since each of those resets on a call outside its own family; sized as a backstop rather than a research judgement, and reset by any `bash`/edit. |
|
|
245
249
|
| `JOB_COLD_START_MS` | `120000` (2m) | First-output window (ADR 0026 D4). A job that has produced nothing this long records a cold-start diagnostic (a likely onboarding/auth wedge) WITHOUT being killed: logged, exposed on `GET /jobs/{id}`, and folded into the failure `detail` if the job goes on to fail. `0` disables it. |
|
|
@@ -279,7 +283,7 @@ docker.io/<org>/cat-factory-executor:<version>
|
|
|
279
283
|
Each is tagged with the package `version`, the commit `sha-…`, and `latest`.
|
|
280
284
|
|
|
281
285
|
**CI** does this automatically:
|
|
282
|
-
[`.github/workflows/docker-publish.yml`](
|
|
286
|
+
[`.github/workflows/docker-publish.yml`](https://github.com/kibertoad/cat-factory/blob/main/.github/workflows/docker-publish.yml)
|
|
283
287
|
republishes on every push to `main` that touches image content (`src/**`,
|
|
284
288
|
`Dockerfile`, `tsconfig.json`, `package.json`). Docker Hub is gated on the
|
|
285
289
|
`DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` repo secrets; without them it publishes
|
|
@@ -303,9 +307,9 @@ via env vars (`REGISTRIES`, `GHCR_OWNER`, `DOCKERHUB_ORG`, `TAG`, `PUSH_LATEST`,
|
|
|
303
307
|
|
|
304
308
|
A backend deployment references the image from `wrangler.toml`
|
|
305
309
|
(`[[containers]] image = "ghcr.io/<owner>/cat-factory-executor:<version>"`: see
|
|
306
|
-
[`deploy/backend`](
|
|
307
|
-
same image (see [`docs/runner-pool-integration.md`](
|
|
310
|
+
[`deploy/backend`](https://github.com/kibertoad/cat-factory/tree/main/deploy/backend)); a self-hosted runner pool pulls the
|
|
311
|
+
same image (see [`docs/runner-pool-integration.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/runner-pool-integration.md)).
|
|
308
312
|
The worker library's own test/dev `wrangler.toml` still references this
|
|
309
313
|
`Dockerfile` by local path so the acceptance suite can build it. Because the
|
|
310
314
|
version is the image tag, **bump this package via a changeset whenever you change
|
|
311
|
-
image content** (see [`CONTRIBUTING.md`](
|
|
315
|
+
image content** (see [`CONTRIBUTING.md`](https://github.com/kibertoad/cat-factory/blob/main/CONTRIBUTING.md)).
|
|
@@ -42,6 +42,67 @@ export interface McpServerSpec {
|
|
|
42
42
|
*/
|
|
43
43
|
secretKeys?: string[];
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* What the agent's CLI reported about ONE wired tool server when it started up.
|
|
47
|
+
*
|
|
48
|
+
* This is the OBSERVED half of the run's tool-server record, and it answers a question the
|
|
49
|
+
* backend's own half structurally cannot: the dispatch record says why the platform WITHHELD a
|
|
50
|
+
* tool, while this says a server the platform wired failed to start anyway. A vendor endpoint
|
|
51
|
+
* that 500s, an `npx` package that no longer resolves, a credential the vendor has since revoked
|
|
52
|
+
* — every one of those leaves the prompt promising a tool the agent then cannot call, and before
|
|
53
|
+
* this the only evidence was the agent saying so in prose, if it noticed at all.
|
|
54
|
+
*
|
|
55
|
+
* OBSERVED, never decided: nothing here changes what the run does. The harness reports what the
|
|
56
|
+
* CLI said and the backend records it beside what it decided; no code path branches on it.
|
|
57
|
+
*/
|
|
58
|
+
export interface ObservedMcpServer {
|
|
59
|
+
/** The server id the CLI named — the same id the backend declared (`--strict-mcp-config`). */
|
|
60
|
+
id: string;
|
|
61
|
+
status: ObservedMcpStatus;
|
|
62
|
+
/**
|
|
63
|
+
* How many of the CLI's exposed tools belong to this server (`mcp__<id>__…`).
|
|
64
|
+
*
|
|
65
|
+
* ABSENT and `0` are different facts and both are worth having: absent means this image counted
|
|
66
|
+
* nothing for the server, while `0` means it counted and the server contributed none: a server
|
|
67
|
+
* that connected and exposes nothing, which reads to the agent exactly like a server that was
|
|
68
|
+
* never wired. Never defaulted to 0.
|
|
69
|
+
*
|
|
70
|
+
* Two things leave it absent, and neither is "the server has no tools": the CLI listed no tools
|
|
71
|
+
* at all, or the tool namespace cannot say which of two declared servers a name belongs to (see
|
|
72
|
+
* {@link tallyToolsByServer}).
|
|
73
|
+
*/
|
|
74
|
+
toolCount?: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The status vocabulary of {@link ObservedMcpServer}, normalised from the CLI's own word.
|
|
78
|
+
*
|
|
79
|
+
* A CLOSED list mapped from an OPEN one, which is why `unknown` is a member rather than a reason
|
|
80
|
+
* to drop the row. The CLI's status strings are a third party's vocabulary and it may add to them;
|
|
81
|
+
* a server whose status this image cannot name is still a server the CLI knows about, and the
|
|
82
|
+
* honest report is "it was there, this image could not read its state" rather than silence
|
|
83
|
+
* (which reads as a server the CLI never mentioned) or a guess at `ready` (which would report a
|
|
84
|
+
* dead tool as a live one, the precise failure the whole unavailability vocabulary exists to
|
|
85
|
+
* prevent).
|
|
86
|
+
*
|
|
87
|
+
* `unknown` covers two causes that share one remedy, which is why they share one member: a word
|
|
88
|
+
* this image cannot map, and a word the CLI uses for a state that is not resolved YET. Neither
|
|
89
|
+
* says anything about the server, and the surface paints neither as a fault.
|
|
90
|
+
*/
|
|
91
|
+
export type ObservedMcpStatus = 'ready' | 'failed' | 'needs_auth' | 'unknown';
|
|
92
|
+
/**
|
|
93
|
+
* Read the claude-code CLI's startup report (`{"type":"system","subtype":"init"}`) into one
|
|
94
|
+
* {@link ObservedMcpServer} per server the CLI knows about.
|
|
95
|
+
*
|
|
96
|
+
* The CLI announces its resolved session ONCE, before the first model call: which MCP servers it
|
|
97
|
+
* loaded and with what status, and the flat list of tool names it will expose. Both halves are
|
|
98
|
+
* read here because neither answers the question alone — a `ready` server exposing no tools is as
|
|
99
|
+
* useless to the agent as a failed one, and a tool count with no status cannot say why.
|
|
100
|
+
*
|
|
101
|
+
* Returns `undefined` when the event names no servers at all, which keeps "this run wired none"
|
|
102
|
+
* and "this image observed none" from collapsing into an empty list on the backend's record.
|
|
103
|
+
* Pure, so the parsing is testable without a CLI: {@link runClaudeCode} feeds it the raw event.
|
|
104
|
+
*/
|
|
105
|
+
export declare function observeClaudeMcpInit(event: Record<string, unknown>): ObservedMcpServer[] | undefined;
|
|
45
106
|
/**
|
|
46
107
|
* The credential values carried by a run's tool servers, for {@link registerKnownSecrets}. An MCP
|
|
47
108
|
* server that fails to start routinely echoes its own argv or request headers into stderr, and
|
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Map one status word from the CLI onto {@link ObservedMcpStatus}.
|
|
5
|
+
*
|
|
6
|
+
* The synonyms are grouped rather than listed one-to-one because the CLI has spelled the same
|
|
7
|
+
* two states more than one way across versions (`connected`/`ready`, `failed`/`error`), and an
|
|
8
|
+
* image that pinned the exact spelling would silently start reporting `unknown` for every server
|
|
9
|
+
* on a CLI upgrade — a regression that looks identical to a genuine outage.
|
|
10
|
+
*/
|
|
11
|
+
function normalizeMcpStatus(value) {
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return 'unknown';
|
|
14
|
+
const status = value.trim().toLowerCase();
|
|
15
|
+
if (status === 'connected' || status === 'ready' || status === 'ok')
|
|
16
|
+
return 'ready';
|
|
17
|
+
if (status === 'failed' || status === 'error')
|
|
18
|
+
return 'failed';
|
|
19
|
+
// The vendor spells the OAuth-required state with a hyphen; the underscore form costs nothing
|
|
20
|
+
// to accept and is what a JSON-ish vocabulary tends to drift toward.
|
|
21
|
+
if (status === 'needs-auth' || status === 'needs_auth')
|
|
22
|
+
return 'needs_auth';
|
|
23
|
+
// Everything else, INCLUDING the CLI's `pending`. A server still handshaking when the session
|
|
24
|
+
// was announced has no resolved state, which is exactly what `unknown` says, and `needs_auth` is
|
|
25
|
+
// the tempting wrong guess for it: the surface paints that one amber as "waiting for you to
|
|
26
|
+
// authorize it", sending an operator to re-issue a working credential for a server that was
|
|
27
|
+
// merely slow and came up a second later.
|
|
28
|
+
return 'unknown';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read the claude-code CLI's startup report (`{"type":"system","subtype":"init"}`) into one
|
|
32
|
+
* {@link ObservedMcpServer} per server the CLI knows about.
|
|
33
|
+
*
|
|
34
|
+
* The CLI announces its resolved session ONCE, before the first model call: which MCP servers it
|
|
35
|
+
* loaded and with what status, and the flat list of tool names it will expose. Both halves are
|
|
36
|
+
* read here because neither answers the question alone — a `ready` server exposing no tools is as
|
|
37
|
+
* useless to the agent as a failed one, and a tool count with no status cannot say why.
|
|
38
|
+
*
|
|
39
|
+
* Returns `undefined` when the event names no servers at all, which keeps "this run wired none"
|
|
40
|
+
* and "this image observed none" from collapsing into an empty list on the backend's record.
|
|
41
|
+
* Pure, so the parsing is testable without a CLI: {@link runClaudeCode} feeds it the raw event.
|
|
42
|
+
*/
|
|
43
|
+
export function observeClaudeMcpInit(event) {
|
|
44
|
+
if (event.type !== 'system' || event.subtype !== 'init')
|
|
45
|
+
return undefined;
|
|
46
|
+
const reported = event.mcp_servers;
|
|
47
|
+
if (!Array.isArray(reported) || reported.length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
const rows = [];
|
|
50
|
+
const declared = new Set();
|
|
51
|
+
for (const entry of reported) {
|
|
52
|
+
if (typeof entry !== 'object' || entry === null)
|
|
53
|
+
continue;
|
|
54
|
+
const record = entry;
|
|
55
|
+
const id = sanitizeServerId(record.name);
|
|
56
|
+
// An id this image cannot hold is dropped rather than reported under a mangled name: the
|
|
57
|
+
// whole row is only useful if it JOINS the backend's declaration, and `--strict-mcp-config`
|
|
58
|
+
// means every server the CLI loaded came from the config this harness wrote.
|
|
59
|
+
if (!id || declared.has(id))
|
|
60
|
+
continue;
|
|
61
|
+
declared.add(id);
|
|
62
|
+
rows.push({ id, status: normalizeMcpStatus(record.status) });
|
|
63
|
+
}
|
|
64
|
+
if (rows.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
// Counted from the CLI's own tool list rather than from a per-server field, because there is no
|
|
67
|
+
// per-server field: the CLI flattens every server's tools into one array namespaced by server
|
|
68
|
+
// id. A missing/non-array list leaves every count ABSENT rather than 0 (see `toolCount`).
|
|
69
|
+
const tally = tallyToolsByServer(event.tools, declared);
|
|
70
|
+
return rows.map((row) => ({
|
|
71
|
+
...row,
|
|
72
|
+
...(tally && !tally.ambiguous.has(row.id) ? { toolCount: tally.counts.get(row.id) ?? 0 } : {}),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Tally the CLI's flat tool list (`mcp__<id>__<tool>`) against the servers the SAME event
|
|
77
|
+
* declared, or `undefined` when it carried no list, which is the distinction
|
|
78
|
+
* {@link ObservedMcpServer.toolCount} preserves.
|
|
79
|
+
*
|
|
80
|
+
* Matched against the declared ids rather than split on the first `__`, because the id vocabulary
|
|
81
|
+
* ({@link MCP_SERVER_ID_PATTERN}) permits an underscore: a server named `code__search` owns
|
|
82
|
+
* `mcp__code__search__query`, which a first-separator split files under a server called `code`,
|
|
83
|
+
* leaving the real one reporting `toolCount: 0`. That is the single most diagnostic value on the
|
|
84
|
+
* field, so the mis-split renders a fully healthy server as one that started and exposes nothing.
|
|
85
|
+
*
|
|
86
|
+
* The same underscore makes genuine ambiguity representable: with both `code` and `code__search`
|
|
87
|
+
* declared, `mcp__code__search__query` is a name either could own and nothing in the report says
|
|
88
|
+
* which. Neither server is counted then, and both are named `ambiguous` so their count stays
|
|
89
|
+
* absent. Guessing an owner would move a real tool onto the wrong server and take the other's
|
|
90
|
+
* count to a `0` that reads as a fault.
|
|
91
|
+
*/
|
|
92
|
+
function tallyToolsByServer(tools, declared) {
|
|
93
|
+
if (!Array.isArray(tools))
|
|
94
|
+
return undefined;
|
|
95
|
+
const counts = new Map();
|
|
96
|
+
const ambiguous = new Set();
|
|
97
|
+
for (const tool of tools) {
|
|
98
|
+
if (typeof tool !== 'string' || !tool.startsWith('mcp__'))
|
|
99
|
+
continue;
|
|
100
|
+
const owners = [];
|
|
101
|
+
for (const id of declared) {
|
|
102
|
+
const prefix = `mcp__${id}__`;
|
|
103
|
+
// The tool name after the prefix must be non-empty: `mcp__slack__` names no tool.
|
|
104
|
+
if (tool.length > prefix.length && tool.startsWith(prefix))
|
|
105
|
+
owners.push(id);
|
|
106
|
+
}
|
|
107
|
+
const [owner] = owners;
|
|
108
|
+
if (owners.length === 1 && owner)
|
|
109
|
+
counts.set(owner, (counts.get(owner) ?? 0) + 1);
|
|
110
|
+
else
|
|
111
|
+
for (const id of owners)
|
|
112
|
+
ambiguous.add(id);
|
|
113
|
+
}
|
|
114
|
+
return { counts, ambiguous };
|
|
115
|
+
}
|
|
3
116
|
/**
|
|
4
117
|
* The credential values carried by a run's tool servers, for {@link registerKnownSecrets}. An MCP
|
|
5
118
|
* server that fails to start routinely echoes its own argv or request headers into stderr, and
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Logger } from './logger.js';
|
|
2
|
+
import { type ToolProgressWindow } from './tool-silence.js';
|
|
2
3
|
import { type HarnessCallMetric, type PiRunOutcome, type TodoProgress, type ToolSpan } from './pi.js';
|
|
3
|
-
import { type McpServerSpec, type SkillSpec } from './agent-capabilities.js';
|
|
4
|
+
import { type McpServerSpec, type ObservedMcpServer, type SkillSpec } from './agent-capabilities.js';
|
|
4
5
|
import { type ProgressGuardLimits } from './progress-guard.js';
|
|
5
6
|
import { type SliceReview } from './subagents.js';
|
|
6
7
|
/** Which subscription harness to run (the Pi harness uses `runPi` directly). */
|
|
@@ -79,6 +80,18 @@ export interface SubscriptionRunOptions {
|
|
|
79
80
|
* DID dies with the container.
|
|
80
81
|
*/
|
|
81
82
|
onSpan?: (span: ToolSpan) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Opens this stream's tool-silence window (see `RunOptions.beginToolWindow`), closed when the
|
|
85
|
+
* CLI exits. Both subscription CLIs report tool activity — claude-code on the `tool_result`
|
|
86
|
+
* turn that answers each call, codex on its tool/command/exec events — so a window either
|
|
87
|
+
* opens is one the run can beat. It is deliberately NOT tied to {@link onSpan}: the trajectory
|
|
88
|
+
* is an observability opt-in, and the codex stream produces none at all while still doing tool
|
|
89
|
+
* work, which a span-keyed window would have read as a run making no progress.
|
|
90
|
+
*
|
|
91
|
+
* A caller with no tool loop (the inline one-shot completion) passes nothing; see the note at
|
|
92
|
+
* `handleInline`.
|
|
93
|
+
*/
|
|
94
|
+
beginToolWindow?: () => ToolProgressWindow;
|
|
82
95
|
/**
|
|
83
96
|
* Called with the FULL set of per-slice reviews each time one lands, so the backend can persist
|
|
84
97
|
* a parallel review's completed work as it happens instead of only from the terminal result.
|
|
@@ -92,6 +105,21 @@ export interface SubscriptionRunOptions {
|
|
|
92
105
|
* same row still rides the result, so a lost poll response costs nothing.
|
|
93
106
|
*/
|
|
94
107
|
onCallMetric?: (call: HarnessCallMetric) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Called once with what the CLI reported about the tool servers it loaded, the moment it
|
|
110
|
+
* announces its resolved session (see {@link observeClaudeMcpInit}).
|
|
111
|
+
*
|
|
112
|
+
* The one thing the backend's own dispatch record cannot answer: it knows why it WITHHELD a
|
|
113
|
+
* tool, and this says a server it wired failed to start anyway. Reported even when every server
|
|
114
|
+
* came up, because "observed, all healthy" and "this image observed nothing" are different
|
|
115
|
+
* facts about a run and only the first one clears a wired server of suspicion.
|
|
116
|
+
*
|
|
117
|
+
* Whole-value latest-wins, not a delta — the CLI announces its session once, so a second call
|
|
118
|
+
* would only ever be a re-announcement of the same set. A harness whose CLI reports nothing
|
|
119
|
+
* (codex today) never calls this, which is what leaves the backend's record honestly empty
|
|
120
|
+
* rather than claiming every server failed.
|
|
121
|
+
*/
|
|
122
|
+
onToolServers?: (observed: ObservedMcpServer[]) => void;
|
|
95
123
|
/**
|
|
96
124
|
* The per-job child logger (jobId/repo/branch correlation). Threaded so the retained
|
|
97
125
|
* session-transcript path is logged for the run when the isolated config home is torn down.
|