tumwater 0.1.1 → 0.1.2
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 +47 -19
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +34 -0
- package/dist/src/agent/claude/claude-account.js +243 -0
- package/dist/src/agent/claude/claude-args.js +104 -0
- package/dist/src/agent/claude/claude-backend.js +162 -0
- package/dist/src/agent/claude/claude-env.js +84 -0
- package/dist/src/agent/claude/claude-stream.js +384 -0
- package/dist/src/agent/mcp/role-notes-server.js +160 -0
- package/dist/src/agent/role-notes-write.js +65 -0
- package/dist/src/agent/run-agent.js +439 -0
- package/dist/src/agent/session-ledger.js +169 -0
- package/dist/src/backlog/backlog-eligibility.js +66 -12
- package/dist/src/backlog/backlog-md.js +34 -15
- package/dist/src/backlog/backlog-render.js +11 -20
- package/dist/src/backlog/backlog-structure.js +68 -63
- package/dist/src/backlog/backlog-write.js +488 -39
- package/dist/src/backlog/backlog.js +5 -16
- package/dist/src/baseline/main-baseline.js +152 -15
- package/dist/src/baseline/main-red.js +21 -18
- package/dist/src/brief.js +1 -1
- package/dist/src/build/build-check-counts.js +15 -3
- package/dist/src/build/build-check-detect.js +6 -3
- package/dist/src/build/build-check-events.js +51 -6
- package/dist/src/build/build-check-scoped.js +26 -27
- package/dist/src/build/build-check.js +5 -4
- package/dist/src/build/build-stage.js +41 -2
- package/dist/src/build/dep-install.js +5 -1
- package/dist/src/build/host-sleep.js +2 -0
- package/dist/src/cli/cli-args.js +30 -2
- package/dist/src/cli/cli-command-args.js +51 -59
- package/dist/src/cli/cli-flag-specs.js +16 -4
- package/dist/src/cli/cli-marker-commands.js +6 -5
- package/dist/src/cli/cli-query-commands.js +89 -26
- package/dist/src/cli/cli-repo-gate.js +15 -0
- package/dist/src/cli/cli-run.js +25 -4
- package/dist/src/cli/config-commands.js +72 -38
- package/dist/src/cli/help.js +116 -29
- package/dist/src/cli/log-commands.js +3 -8
- package/dist/src/cli/notify-command.js +68 -0
- package/dist/src/cli/question-commands.js +55 -18
- package/dist/src/cli.js +89 -32
- package/dist/src/collections.js +8 -2
- package/dist/src/concurrency/check-permit.js +2 -2
- package/dist/src/concurrency/semaphore.js +26 -4
- package/dist/src/config/config-editable-keys.js +13 -2
- package/dist/src/config/config-example.js +2 -2
- package/dist/src/config/config-field-checks.js +23 -6
- package/dist/src/config/config-live.js +4 -14
- package/dist/src/config/config-schema.js +18 -7
- package/dist/src/config/config-validation.js +57 -16
- package/dist/src/config/config-views.js +117 -46
- package/dist/src/config/config-write.js +282 -24
- package/dist/src/config/config.js +56 -15
- package/dist/src/config/model-selector.js +32 -1
- package/dist/src/doctor/doctor-backlog.js +22 -27
- package/dist/src/doctor/doctor-checks.js +180 -116
- package/dist/src/doctor/doctor-holds.js +122 -0
- package/dist/src/doctor/doctor-launch-services.js +5 -7
- package/dist/src/doctor/doctor-model-checks.js +228 -86
- package/dist/src/doctor/doctor-orphans.js +7 -15
- package/dist/src/doctor/doctor.js +13 -3
- package/dist/src/doctor/outcome.js +16 -0
- package/dist/src/events/event-format.js +60 -6
- package/dist/src/events/event-read.js +4 -2
- package/dist/src/events/event-window.js +82 -3
- package/dist/src/events/events.js +27 -35
- package/dist/src/events/notify.js +18 -8
- package/dist/src/failure/failure-cluster.js +25 -11
- package/dist/src/failure/failure-data.js +122 -16
- package/dist/src/failure/failure-render.js +29 -11
- package/dist/src/failure/failure-state-change.js +30 -3
- package/dist/src/failure/time-spend.js +54 -15
- package/dist/src/files/files.js +107 -1
- package/dist/src/files/json-files.js +5 -4
- package/dist/src/files/json-object.js +34 -17
- package/dist/src/files/stat-cache.js +15 -2
- package/dist/src/files/tail.js +2 -2
- package/dist/src/fleet/error-storm.js +2 -2
- package/dist/src/fleet/fleet-hold.js +21 -3
- package/dist/src/fleet/fleet-polls.js +95 -21
- package/dist/src/fleet/fleet-state.js +30 -6
- package/dist/src/fleet/orchestrator-info.js +18 -0
- package/dist/src/fleet/reclaim.js +28 -28
- package/dist/src/gates/bootstrap-gates.js +5 -3
- package/dist/src/gates/budget-gates.js +34 -21
- package/dist/src/gates/gate-polls.js +18 -11
- package/dist/src/gates/maintenance-quota.js +2 -2
- package/dist/src/gates/role-cap-gates.js +73 -30
- package/dist/src/gates/startup-gate.js +44 -11
- package/dist/src/git/commit-message.js +5 -4
- package/dist/src/git/git-diff.js +41 -44
- package/dist/src/git/git-run.js +2 -0
- package/dist/src/git/git.js +207 -5
- package/dist/src/git/slots-state.js +7 -4
- package/dist/src/git/worktree-pool.js +18 -14
- package/dist/src/git/worktree-use.js +19 -2
- package/dist/src/git/worktree.js +15 -15
- package/dist/src/gui/gui-args.js +49 -0
- package/dist/src/gui/gui-endpoint-commands.js +416 -54
- package/dist/src/gui/gui-endpoints.js +181 -26
- package/dist/src/gui/gui-server.js +16 -2
- package/dist/src/history/history-data.js +124 -20
- package/dist/src/history/history.js +54 -50
- package/dist/src/inbox/inbox-attachments.js +126 -7
- package/dist/src/inbox/inbox-cancel.js +20 -9
- package/dist/src/inbox/inbox-edit.js +100 -20
- package/dist/src/inbox/inbox-submit.js +13 -9
- package/dist/src/inbox/inbox.js +110 -27
- package/dist/src/inbox/pending-prompt.js +71 -19
- package/dist/src/inbox/prompt-commands.js +50 -106
- package/dist/src/init/init.js +18 -16
- package/dist/src/landing/landing-batch.js +23 -7
- package/dist/src/landing/landing-check-failures.js +3 -3
- package/dist/src/landing/landing-core.js +9 -5
- package/dist/src/landing/landing-drain.js +54 -9
- package/dist/src/landing/landing-merge.js +96 -43
- package/dist/src/landing/landing-pipeline.js +14 -0
- package/dist/src/landing/landing-queue-render.js +31 -0
- package/dist/src/landing/landing-queue.js +31 -5
- package/dist/src/landing/landing-slot.js +24 -7
- package/dist/src/landing/landing-vetting.js +6 -5
- package/dist/src/loop/leftover.js +8 -7
- package/dist/src/loop/loop-fallback.js +66 -0
- package/dist/src/loop/loop-pi.js +121 -33
- package/dist/src/loop/loop-recovery.js +90 -0
- package/dist/src/loop/loop-state.js +42 -18
- package/dist/src/loop/loop-tick.js +539 -0
- package/dist/src/loop/loop.js +85 -667
- package/dist/src/operator/operator-commands.js +27 -17
- package/dist/src/operator/operator-intent.js +53 -8
- package/dist/src/operator/operator-requests.js +129 -15
- package/dist/src/orchestrator/orchestrator-launch.js +1 -1
- package/dist/src/orchestrator/orchestrator-scheduling.js +41 -7
- package/dist/src/orchestrator/orchestrator.js +41 -24
- package/dist/src/orchestrator/retention.js +10 -1
- package/dist/src/paths.js +31 -2
- package/dist/src/pi/pi-args.js +3 -3
- package/dist/src/pi/pi-backend.js +90 -0
- package/dist/src/pi/pi-bin.js +83 -23
- package/dist/src/pi/pi-event-line.js +14 -12
- package/dist/src/pi/pi-models.js +65 -21
- package/dist/src/pi/pi-run-result.js +7 -1
- package/dist/src/pi/pi-stream.js +170 -44
- package/dist/src/pi/pi-watchdogs.js +31 -14
- package/dist/src/pi/pi.js +9 -304
- package/dist/src/pi-extension/bounded-output.js +1 -1
- package/dist/src/pi-extension/context-shake.js +8 -2
- package/dist/src/pi-extension/role-notes.js +11 -61
- package/dist/src/process/process.js +6 -4
- package/dist/src/process/run-marker.js +22 -9
- package/dist/src/process/supervisor.js +86 -29
- package/dist/src/prompt/dialect.js +35 -0
- package/dist/src/{gates → prompt}/gate-prompts.js +66 -21
- package/dist/src/prompt/prompt-followup.js +30 -8
- package/dist/src/prompt/prompt.js +32 -15
- package/dist/src/redeploy/redeploy.js +1 -1
- package/dist/src/redeploy/redeployer.js +5 -3
- package/dist/src/redeploy/self-reload.js +1 -2
- package/dist/src/report/report-data.js +148 -48
- package/dist/src/report/report-render.js +42 -17
- package/dist/src/report/report.js +22 -9
- package/dist/src/review/review-followup.js +12 -7
- package/dist/src/review/review-precheck.js +2 -2
- package/dist/src/review/review-verdict.js +103 -6
- package/dist/src/review/review.js +109 -24
- package/dist/src/review/suite-rerun.js +1 -1
- package/dist/src/roles/loop-ids.js +2 -3
- package/dist/src/roles/role-render.js +40 -13
- package/dist/src/roles/role-view.js +17 -3
- package/dist/src/roles/roles.js +2 -2
- package/dist/src/scheduling/backoff.js +1 -1
- package/dist/src/scheduling/claims.js +52 -7
- package/dist/src/scheduling/scheduling.js +3 -3
- package/dist/src/scheduling/work-landed-cache.js +1 -1
- package/dist/src/status/status-data.js +67 -20
- package/dist/src/text/datetime.js +9 -0
- package/dist/src/text/markdown.js +2 -2
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/tick/qa-coverage.js +11 -3
- package/dist/src/tick/stage-check.js +25 -3
- package/dist/src/tick/tick-apply.js +21 -9
- package/dist/src/tick/tick-finalize.js +20 -4
- package/dist/src/tick/tick-prompt.js +23 -11
- package/dist/src/tick/tick-resume.js +71 -10
- package/dist/src/tick/tick-stage.js +21 -9
- package/dist/src/tick/tick-timing.js +23 -13
- package/dist/src/tick/tick-usage.js +34 -4
- package/dist/src/tick/tick-verdict.js +47 -2
- package/dist/src/ui/badges.js +82 -6
- package/dist/src/ui/gui/gui-client-boot.js +93 -5
- package/dist/src/ui/gui/gui-client-composer.js +13 -8
- package/dist/src/ui/gui/gui-client-drawer.js +141 -20
- package/dist/src/ui/gui/gui-client-fleet.js +263 -18
- package/dist/src/ui/gui/gui-client-history.js +84 -37
- package/dist/src/ui/gui/gui-client-loops.js +83 -9
- package/dist/src/ui/gui/gui-client-model.js +1 -1
- package/dist/src/ui/gui/gui-client-operator.js +90 -10
- package/dist/src/ui/gui/gui-client-pending.js +39 -7
- package/dist/src/ui/gui/gui-client-report.js +190 -50
- package/dist/src/ui/gui/gui-client-settings.js +69 -19
- package/dist/src/ui/gui/gui-client.js +44 -11
- package/dist/src/ui/gui/gui-page.js +7 -1
- package/dist/src/ui/gui/gui-styles.js +1 -0
- package/dist/src/ui/progress-data.js +32 -26
- package/dist/src/ui/status-model.js +22 -10
- package/dist/src/ui/status-payload.js +29 -16
- package/dist/src/ui/status-render.js +14 -6
- package/dist/src/ui/tone.js +1 -1
- package/dist/src/ui/transcript.js +4 -4
- package/dist/src/ui/tui/tui-backlog.js +8 -6
- package/dist/src/ui/tui/tui-frame.js +49 -9
- package/dist/src/ui/tui/tui-history.js +29 -0
- package/dist/src/ui/tui/tui-input.js +84 -6
- package/dist/src/ui/tui/tui-keys.js +1382 -50
- package/dist/src/ui/tui/tui-pane.js +132 -23
- package/dist/src/ui/tui/tui.js +8 -5
- package/dist/src/verdict/fix-claim.js +18 -31
- package/dist/src/verdict/reply-contract.js +16 -15
- package/package.json +3 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** The claude adapter for the agent seam (plans/agent-backends.md "The seam", "Sessions",
|
|
2
|
+
* "The claude invocation"). It shapes one run's argv/stdin/env, translates claude's
|
|
3
|
+
* `stream-json` into the canonical stream, and classifies the exit's stderr — everything
|
|
4
|
+
* backend-specific. src/agent/run-agent.ts owns the spawn and records this run in the session
|
|
5
|
+
* ledger; claude mints its own session id (a fresh UUID, or the resumed record's id), which
|
|
6
|
+
* `invocation` returns so the ledger can carry it.
|
|
7
|
+
*
|
|
8
|
+
* Sessions resume by id from the cwd the session started in, so the adapter consults the
|
|
9
|
+
* ledger's newest record of this run's kind and only passes `--resume` when that record names
|
|
10
|
+
* claude, this cwd, and a transcript file that exists. Otherwise it mints a fresh id — a
|
|
11
|
+
* missing transcript, a different cwd, or another kind's newer session all review fresh rather
|
|
12
|
+
* than resuming the wrong conversation.
|
|
13
|
+
*
|
|
14
|
+
* Registration lives in src/agent/run-agent.ts beside the pi adapter, so a test can register a
|
|
15
|
+
* fake under its own id; a test selects claude by setting `config.agent = "claude"` on a run. */
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { resolvedAgentConfig } from "../../config/config-views.js";
|
|
22
|
+
import { MODEL_TIERS } from "../../config/config-schema.js";
|
|
23
|
+
import { routeModel } from "../../config/model-selector.js";
|
|
24
|
+
import { agentBaseEnv } from "../../process/run-marker.js";
|
|
25
|
+
import { resolveBackendBin } from "../../pi/pi-bin.js";
|
|
26
|
+
import { claudeArgs } from "./claude-args.js";
|
|
27
|
+
import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
|
|
28
|
+
import { claudeTranslator } from "./claude-stream.js";
|
|
29
|
+
import { resumableSession } from "../session-ledger.js";
|
|
30
|
+
/** claude's per-cwd project directory name under `~/.claude/projects/`: every character that
|
|
31
|
+
* is not a letter or digit becomes `-`. Exported with `claudeTranscriptPath` so the PATH fake
|
|
32
|
+
* and the tests create and look for exactly the file the resume check reads. */
|
|
33
|
+
export function claudeProjectSlug(cwd) {
|
|
34
|
+
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
35
|
+
}
|
|
36
|
+
/** The transcript file claude writes for a session started in `cwd`: its resume check must find
|
|
37
|
+
* a real file before passing `--resume`. claude's own layout is
|
|
38
|
+
* `~/.claude/projects/<slug>/<sessionId>.jsonl`; exported so the fake and the resume logic
|
|
39
|
+
* cannot drift. */
|
|
40
|
+
export function claudeTranscriptPath(cwd, id) {
|
|
41
|
+
return path.join(os.homedir(), ".claude", "projects", claudeProjectSlug(cwd), `${id}.jsonl`);
|
|
42
|
+
}
|
|
43
|
+
/** The CLI version every gated claude model needs (plans/agent-backends.md finding 9): on
|
|
44
|
+
* 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and fleet runs
|
|
45
|
+
* set DISABLE_AUTOUPDATER so they never update themselves. The gated models are the three
|
|
46
|
+
* tier defaults, derived from AGENT_DEFAULT_MODELS through the same routing the config uses
|
|
47
|
+
* rather than listed again, so this table cannot drift from the models a seam resolves. */
|
|
48
|
+
const CLAUDE_MIN_VERSION = "2.1.280";
|
|
49
|
+
const CLAUDE_GATED_MODELS = new Set(MODEL_TIERS.map((tier) => routeModel(undefined, undefined, "claude", tier).model).filter((model) => model !== undefined));
|
|
50
|
+
/** The minimum CLI version `model` needs, or undefined when this adapter gates no floor for
|
|
51
|
+
* it (a model outside the three tier defaults). Doctor reads it through `claudeBackend`. */
|
|
52
|
+
function claudeMinVersion(model) {
|
|
53
|
+
return CLAUDE_GATED_MODELS.has(model) ? CLAUDE_MIN_VERSION : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Classify claude's stderr: a nonzero exit naming a login or model problem is a permanent
|
|
56
|
+
* configuration error. The stream translator classifies the structured result signals; this is
|
|
57
|
+
* the stderr half, for a CLI that died before emitting one. A run the harness killed has its
|
|
58
|
+
* own cause and is never classified here. */
|
|
59
|
+
function classifyClaudeExit(info) {
|
|
60
|
+
if (info.killed)
|
|
61
|
+
return {};
|
|
62
|
+
if (info.code !== 0 &&
|
|
63
|
+
/Not logged in|issue with the selected model|does not support this model/i.test(info.stderr)) {
|
|
64
|
+
return { configError: true };
|
|
65
|
+
}
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
/** The inline `--mcp-config` JSON that runs the bundled role-notes server (part 5/7): the
|
|
69
|
+
* same dist module pi's extension resolves to, spawned by the same node running this harness
|
|
70
|
+
* so it needs no PATH lookup. */
|
|
71
|
+
function roleNotesMcpConfig() {
|
|
72
|
+
const server = fileURLToPath(new URL("../mcp/role-notes-server.js", import.meta.url));
|
|
73
|
+
return JSON.stringify({ mcpServers: { tumwater: { command: process.execPath, args: [server] } } });
|
|
74
|
+
}
|
|
75
|
+
/** The ledger record this run may resume, or null: the newest record of `opts.kind` for
|
|
76
|
+
* claude, at `opts.cwd` when one is given, whose transcript file exists. Shared by
|
|
77
|
+
* `resolveClaudeSession` and the backend's `resumable` hook, so the resume guard and the
|
|
78
|
+
* invocation can never drift on what counts as resumable. */
|
|
79
|
+
function claudeResumableRecord(dir, opts) {
|
|
80
|
+
const record = resumableSession(dir, opts);
|
|
81
|
+
if (record === null)
|
|
82
|
+
return null;
|
|
83
|
+
if (opts.cwd !== undefined && record.cwd !== opts.cwd)
|
|
84
|
+
return null;
|
|
85
|
+
if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id)))
|
|
86
|
+
return null;
|
|
87
|
+
return record;
|
|
88
|
+
}
|
|
89
|
+
/** The session id this run uses: for a continue, the ledger's newest record of this kind when
|
|
90
|
+
* it is claude's, this cwd's, and its transcript file exists; otherwise a fresh UUID. Returns
|
|
91
|
+
* the id and the `--resume` id when resuming. */
|
|
92
|
+
function resolveClaudeSession(run) {
|
|
93
|
+
if (run.continueSession) {
|
|
94
|
+
const record = claudeResumableRecord(run.sessionDir, {
|
|
95
|
+
kind: run.kind,
|
|
96
|
+
agent: "claude",
|
|
97
|
+
cwd: run.cwd,
|
|
98
|
+
});
|
|
99
|
+
if (record !== null)
|
|
100
|
+
return { sessionId: record.id, resumeSessionId: record.id };
|
|
101
|
+
}
|
|
102
|
+
return { sessionId: randomUUID() };
|
|
103
|
+
}
|
|
104
|
+
/** The agent-backend seam's claude adapter. */
|
|
105
|
+
export const claudeBackend = {
|
|
106
|
+
id: "claude",
|
|
107
|
+
label: "Claude Code",
|
|
108
|
+
defaultBin: "claude",
|
|
109
|
+
binEnv: "TUMWATER_CLAUDE_BIN",
|
|
110
|
+
minVersion: claudeMinVersion,
|
|
111
|
+
resolveBin(config) {
|
|
112
|
+
return resolveBackendBin(config, this);
|
|
113
|
+
},
|
|
114
|
+
invocation(run) {
|
|
115
|
+
// Part 4b/7's config routing fills a claude tier default before the seam, so a nameless
|
|
116
|
+
// run here is a caller bug (a config that named claude without going through the views).
|
|
117
|
+
if (run.config.model === undefined) {
|
|
118
|
+
throw new Error("claude backend: no model resolved for this run");
|
|
119
|
+
}
|
|
120
|
+
const { sessionId, resumeSessionId } = resolveClaudeSession(run);
|
|
121
|
+
const { args, stdin } = claudeArgs({
|
|
122
|
+
cwd: run.cwd,
|
|
123
|
+
model: run.config.model,
|
|
124
|
+
thinking: run.config.thinking,
|
|
125
|
+
...(resumeSessionId !== undefined ? { resumeSessionId } : { sessionId }),
|
|
126
|
+
// The operator's `agents.claude.args` passthrough, appended after the harness flags (the
|
|
127
|
+
// builder rejects any entry that repeats a harness-managed flag). resolvedAgentConfig
|
|
128
|
+
// owns the view so a caller cannot mutate the loaded config through the returned array.
|
|
129
|
+
args: resolvedAgentConfig(run.config, "claude").args,
|
|
130
|
+
prompt: run.prompt,
|
|
131
|
+
// The caller computes the remaining daily budget for an API-billed claude run and the
|
|
132
|
+
// adapter forwards it; a subscription run or the director passes none (part 7a/7).
|
|
133
|
+
...(run.maxBudgetUsd !== undefined ? { maxBudgetUsd: run.maxBudgetUsd } : {}),
|
|
134
|
+
// claude does not load the bundled pi extensions, so a run carrying a notebook gets the
|
|
135
|
+
// role_notes MCP server instead. Passed on EVERY claude invocation that carries a
|
|
136
|
+
// notebook — fresh or resumed, a transient retry, a SUMMARY follow-up — because claude
|
|
137
|
+
// keeps no flags across --resume, so a resume without this would lose the tool.
|
|
138
|
+
...(run.notesPath !== undefined ? { mcpConfig: roleNotesMcpConfig() } : {}),
|
|
139
|
+
});
|
|
140
|
+
// agentBaseEnv applies the run marker and the macOS preload (shared with the pi adapter);
|
|
141
|
+
// claudeEnv layers the backend's own scrub-and-set on top. The notebook path rides beside
|
|
142
|
+
// the marker the same way pi's adapter carries it (pi-backend.ts withNotesPath), so the
|
|
143
|
+
// spawned MCP server — which inherits this environment — reads the same notes file.
|
|
144
|
+
const base = run.notesPath === undefined
|
|
145
|
+
? agentBaseEnv(run.runMarker)
|
|
146
|
+
: { ...agentBaseEnv(run.runMarker), TUMWATER_NOTES_PATH: run.notesPath };
|
|
147
|
+
const env = claudeEnv(base, {
|
|
148
|
+
quietTimeoutSeconds: run.config.quietTimeoutSeconds,
|
|
149
|
+
});
|
|
150
|
+
return { args, stdin, env, sessionId };
|
|
151
|
+
},
|
|
152
|
+
translator() {
|
|
153
|
+
// A third-party provider (Bedrock/Vertex/Foundry) bills as API even when claude's
|
|
154
|
+
// `system/init` omits a usable `apiKeySource`; seedBilling reads the same environment to
|
|
155
|
+
// seed the account file, so both must classify it the same way.
|
|
156
|
+
return claudeTranslator({ thirdPartyProvider: claudeThirdPartyProvider(process.env) });
|
|
157
|
+
},
|
|
158
|
+
resumable(dir, opts) {
|
|
159
|
+
return claudeResumableRecord(dir, opts) !== null;
|
|
160
|
+
},
|
|
161
|
+
classifyExit: classifyClaudeExit,
|
|
162
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Building the environment for one claude run (plans/agent-backends.md "The claude
|
|
2
|
+
* invocation", "Findings" 2 and 4) — pure scrub-and-set over a base environment, with no
|
|
3
|
+
* subprocess. The caller layers the run-marker, the macOS LaunchServices preload, and the
|
|
4
|
+
* notebook on first (src/pi/pi-backend.ts's `withNotesPath` shows the pi adapter doing the
|
|
5
|
+
* same), then hands the result here.
|
|
6
|
+
*
|
|
7
|
+
* Inherited Claude Code session variables break auth: spawned from inside a Claude Code
|
|
8
|
+
* session, a child that keeps `CLAUDECODE`, `CLAUDE_CODE_*`, `CLAUDE_AGENT_SDK_*` or the host's
|
|
9
|
+
* `ANTHROPIC_BASE_URL` reports "Not logged in" (finding 2). Everything the host session set is
|
|
10
|
+
* scrubbed, except the operator-intent allowlist; `ANTHROPIC_BASE_URL` is dropped only when
|
|
11
|
+
* `CLAUDECODE` was set, so an operator's own proxy still reaches a run started outside a
|
|
12
|
+
* session.
|
|
13
|
+
*
|
|
14
|
+
* The set half disables auto-memory, backgrounded Bash tasks (finding 4: claude refuses a long
|
|
15
|
+
* foreground command, backgrounds it, and abandons it when `-p` ends, which would report work
|
|
16
|
+
* never verified), the auto-updater (a mid-fleet self-update must not swap the binary under a
|
|
17
|
+
* run), telemetry, and error reporting; and raises the Bash timeouts so a slow suite runs in
|
|
18
|
+
* the foreground. `BASH_MAX_TIMEOUT_MS` is derived from `quietTimeoutSeconds` so claude's own
|
|
19
|
+
* Bash timeout fires before tumwater's quiet kill and the model sees a tool error rather than a
|
|
20
|
+
* dead run. */
|
|
21
|
+
/** The cloud-provider switches that make a claude run bill as API rather than a claude.ai
|
|
22
|
+
* subscription: Bedrock, Vertex, and Azure Foundry. They carry operator intent, so the run
|
|
23
|
+
* environment keeps them (the allowlist below); claude-account's seedBilling and this module's
|
|
24
|
+
* `claudeThirdPartyProvider` classify all three as API billing. */
|
|
25
|
+
const THIRD_PARTY_PROVIDER_VARS = [
|
|
26
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
27
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
28
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
29
|
+
];
|
|
30
|
+
/** True when `base` carries one of the third-party provider switches. seedBilling
|
|
31
|
+
* (claude-account.ts) and the run's stream translator (claude-backend.ts) both answer billing
|
|
32
|
+
* through this, so a provider run agrees with the account file about its own billing even when
|
|
33
|
+
* claude's `system/init` omits a usable `apiKeySource`. */
|
|
34
|
+
export function claudeThirdPartyProvider(base) {
|
|
35
|
+
return THIRD_PARTY_PROVIDER_VARS.some((name) => base[name] !== undefined);
|
|
36
|
+
}
|
|
37
|
+
/** The host-session variables that carry operator intent rather than the session's state, so a
|
|
38
|
+
* run inherits them: which cloud provider to authenticate through (Bedrock, Vertex, or Azure
|
|
39
|
+
* Foundry), an OAuth token, and an output cap. Every other `CLAUDE_CODE_*` is the session's
|
|
40
|
+
* own. The three provider switches must all survive together: seeds that classify billing
|
|
41
|
+
* treat any of them as API, so a run that dropped one would disagree with the account file
|
|
42
|
+
* about its own provider. */
|
|
43
|
+
const CLAUDE_CODE_ALLOWLIST = new Set([
|
|
44
|
+
...THIRD_PARTY_PROVIDER_VARS,
|
|
45
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
46
|
+
"CLAUDE_CODE_MAX_OUTPUT_TOKENS",
|
|
47
|
+
]);
|
|
48
|
+
/** claude's default foreground Bash ceiling, matching the design's `BASH_DEFAULT_TIMEOUT_MS`
|
|
49
|
+
* (finding 4: claude's own default is 2 min, too short for a real suite). */
|
|
50
|
+
const BASH_DEFAULT_TIMEOUT_MS = 600_000;
|
|
51
|
+
/** The floor for the derived `BASH_MAX_TIMEOUT_MS`. A positive `quietTimeoutSeconds` below 60
|
|
52
|
+
* would derive a non-positive cap, which claude rejects (or reads as unlimited); clamp to one
|
|
53
|
+
* second so a very short quiet window still hands claude a usable, positive ceiling. */
|
|
54
|
+
const MIN_BASH_MAX_TIMEOUT_MS = 1_000;
|
|
55
|
+
/** Build the claude process environment from `base`. Never mutates `base`. */
|
|
56
|
+
export function claudeEnv(base, opts) {
|
|
57
|
+
const hadClaudeCode = base.CLAUDECODE !== undefined;
|
|
58
|
+
const env = {};
|
|
59
|
+
for (const [key, value] of Object.entries(base)) {
|
|
60
|
+
if (key === "CLAUDECODE" || key === "CLAUDE_PID" || key === "CLAUDE_EFFORT")
|
|
61
|
+
continue;
|
|
62
|
+
if (key.startsWith("CLAUDE_CODE_") && !CLAUDE_CODE_ALLOWLIST.has(key))
|
|
63
|
+
continue;
|
|
64
|
+
if (key.startsWith("CLAUDE_AGENT_SDK_"))
|
|
65
|
+
continue;
|
|
66
|
+
// The host session's proxy, not the operator's: only when CLAUDECODE says we are inside one.
|
|
67
|
+
if (key === "ANTHROPIC_BASE_URL" && hadClaudeCode)
|
|
68
|
+
continue;
|
|
69
|
+
env[key] = value;
|
|
70
|
+
}
|
|
71
|
+
env.CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
|
|
72
|
+
env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
|
|
73
|
+
env.BASH_DEFAULT_TIMEOUT_MS = String(BASH_DEFAULT_TIMEOUT_MS);
|
|
74
|
+
if (opts.quietTimeoutSeconds > 0) {
|
|
75
|
+
// 60 s under the quiet kill, so claude's tool error arrives before the run is declared hung;
|
|
76
|
+
// floored so a quiet window under 60 s never derives a non-positive cap.
|
|
77
|
+
const derived = opts.quietTimeoutSeconds * 1000 - 60_000;
|
|
78
|
+
env.BASH_MAX_TIMEOUT_MS = String(Math.max(derived, MIN_BASH_MAX_TIMEOUT_MS));
|
|
79
|
+
}
|
|
80
|
+
env.DISABLE_AUTOUPDATER = "1";
|
|
81
|
+
env.DISABLE_TELEMETRY = "1";
|
|
82
|
+
env.DISABLE_ERROR_REPORTING = "1";
|
|
83
|
+
return env;
|
|
84
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/** The claude adapter's native-to-canonical stream translator (plans/agent-backends.md
|
|
2
|
+
* "Canonical event stream"). Claude Code's `--output-format stream-json` vocabulary differs
|
|
3
|
+
* from pi's in every dimension the readers care about: one content block per `assistant`
|
|
4
|
+
* event (with the same `message.id` and its usage repeated on each), tool calls named `Bash`/
|
|
5
|
+
* `Read`/`File`-style, `file_path` rather than `path`, cost only in the final `result`, and
|
|
6
|
+
* errors carried as `is_error` + `api_error_status` + `terminal_reason` rather than as prose
|
|
7
|
+
* in a message. Rather than teach ~fifteen readers a second dialect, every native line is
|
|
8
|
+
* translated here into pi's vocabulary — the canonical stream — so `PiStreamParser`, the raw
|
|
9
|
+
* log, the transcript, progress, and the GUI all stay unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
|
|
12
|
+
* state (the pending split-block assistant message, the unmapped native types seen, whether a
|
|
13
|
+
* `run_cost` arrived), so call it once per run. Unmapped native types are recorded once each
|
|
14
|
+
* on the seam's optional `warnings()` — src/agent/run-agent.ts forwards each as an
|
|
15
|
+
* `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
|
|
16
|
+
* run produced no authoritative `run_cost`. */
|
|
17
|
+
import { isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
|
|
18
|
+
/** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
|
|
19
|
+
* lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
|
|
20
|
+
* lower-cased so a future tool still reads as a name rather than as `undefined`. */
|
|
21
|
+
const TOOL_NAMES = {
|
|
22
|
+
Bash: "bash",
|
|
23
|
+
Read: "read",
|
|
24
|
+
Edit: "edit",
|
|
25
|
+
MultiEdit: "edit",
|
|
26
|
+
Write: "write",
|
|
27
|
+
Glob: "glob",
|
|
28
|
+
Grep: "grep",
|
|
29
|
+
};
|
|
30
|
+
/** claude's HTTP status classes that mean a transient backend failure. 529 is Anthropic's
|
|
31
|
+
* "overloaded" spelling. */
|
|
32
|
+
const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
|
|
33
|
+
/** claude's HTTP status classes that mean a permanent configuration error. */
|
|
34
|
+
const CONFIG_STATUS = new Set([400, 401, 403, 404]);
|
|
35
|
+
function str(value) {
|
|
36
|
+
return typeof value === "string" ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
/** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
|
|
39
|
+
* authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
|
|
40
|
+
* on every event, so summing per event would double-count — the fold takes it once). */
|
|
41
|
+
function canonicalUsage(usage) {
|
|
42
|
+
const input = nonNegativeNumber(usage?.input_tokens, 0);
|
|
43
|
+
const cacheRead = nonNegativeNumber(usage?.cache_read_input_tokens, 0);
|
|
44
|
+
const cacheWrite = nonNegativeNumber(usage?.cache_creation_input_tokens, 0);
|
|
45
|
+
const output = nonNegativeNumber(usage?.output_tokens, 0);
|
|
46
|
+
return {
|
|
47
|
+
input,
|
|
48
|
+
cacheRead,
|
|
49
|
+
cacheWrite,
|
|
50
|
+
output,
|
|
51
|
+
totalTokens: input + cacheRead + cacheWrite + output,
|
|
52
|
+
cost: { total: 0 },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** One claude content block → one pi content block, or undefined for a block pi has no
|
|
56
|
+
* vocabulary for (dropped, like any other unmapped native shape). `tool_use` becomes pi's
|
|
57
|
+
* `toolCall` with the canonical name and an `arguments` object carrying claude's `input` plus
|
|
58
|
+
* a `path` alias of `file_path` — `describeToolCall` reads either. */
|
|
59
|
+
function canonicalBlock(block) {
|
|
60
|
+
switch (block.type) {
|
|
61
|
+
case "text":
|
|
62
|
+
return { type: "text", text: str(block.text) ?? "" };
|
|
63
|
+
case "thinking":
|
|
64
|
+
return { type: "thinking", thinking: str(block.thinking) ?? str(block.text) ?? "" };
|
|
65
|
+
case "tool_use": {
|
|
66
|
+
const input = objectOrEmpty(block.input);
|
|
67
|
+
const args = { ...input };
|
|
68
|
+
if (typeof input.file_path === "string")
|
|
69
|
+
args.path = input.file_path;
|
|
70
|
+
const name = str(block.name) ?? "";
|
|
71
|
+
// Own-property test, not a bare index: a tool name like `constructor`, `toString`, or
|
|
72
|
+
// `__proto__` would otherwise inherit a function (or Object.prototype) from the table's
|
|
73
|
+
// prototype chain and bypass the lower-cased fallback — JSON.stringify then drops the
|
|
74
|
+
// canonical event's toolName or turns it into an empty object. The same guard claude-args
|
|
75
|
+
// applies to EFFORT_BY_THINKING.
|
|
76
|
+
return {
|
|
77
|
+
type: "toolCall",
|
|
78
|
+
id: str(block.id) ?? "",
|
|
79
|
+
name: Object.hasOwn(TOOL_NAMES, name) ? TOOL_NAMES[name] : name.toLowerCase(),
|
|
80
|
+
arguments: args,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** How a `result` event's error signals classify into the canonical `tumwaterKind` (and, for
|
|
88
|
+
* a transient backend, its kind), trusted by `PiStreamParser` before its text regexes. Empty
|
|
89
|
+
* when the result is not an error or carries no signal the vocabulary names — a SIGTERM
|
|
90
|
+
* (`terminal_reason: "aborted_tools"`) is deliberate: the harness killed the run, so no
|
|
91
|
+
* provider classification applies. `status` is `api_error_status`, which is null on the
|
|
92
|
+
* not-logged-in result, so the text checks carry that case. */
|
|
93
|
+
function classifyResult(result) {
|
|
94
|
+
const terminal = str(result.terminal_reason);
|
|
95
|
+
if (terminal === "aborted_tools")
|
|
96
|
+
return {};
|
|
97
|
+
if (terminal === "error_max_budget_usd")
|
|
98
|
+
return { kind: "budgetCapped" };
|
|
99
|
+
const text = [str(result.result), str(result.error)].filter(Boolean).join(" ");
|
|
100
|
+
if (/error_max_budget_usd/i.test(text))
|
|
101
|
+
return { kind: "budgetCapped" };
|
|
102
|
+
const status = nonNegativeNumber(result.api_error_status, undefined);
|
|
103
|
+
if (status === 429)
|
|
104
|
+
return { kind: "transientRateLimit" };
|
|
105
|
+
if (status !== undefined && SERVER_STATUS.has(status))
|
|
106
|
+
return { kind: "transientBackend", backendKind: "server" };
|
|
107
|
+
// The context-overflow text wins over a configuration status: claude reports an oversized
|
|
108
|
+
// prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError.
|
|
109
|
+
if (/prompt is too long/i.test(text))
|
|
110
|
+
return { kind: "contextExceeded" };
|
|
111
|
+
if (status !== undefined && CONFIG_STATUS.has(status))
|
|
112
|
+
return { kind: "configError" };
|
|
113
|
+
// Text fallbacks, for the statuses claude leaves null or omits.
|
|
114
|
+
if (/No conversation found with session ID/i.test(text))
|
|
115
|
+
return { kind: "resumeMissing" };
|
|
116
|
+
if (/Not logged in|issue with the selected model|does not support this model/i.test(text))
|
|
117
|
+
return { kind: "configError" };
|
|
118
|
+
if (/\b429\b|rate limit/i.test(text))
|
|
119
|
+
return { kind: "transientRateLimit" };
|
|
120
|
+
if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
|
|
121
|
+
return { kind: "transientBackend", backendKind: "server" };
|
|
122
|
+
if (/connection|econn|refused|reset|terminated/i.test(text))
|
|
123
|
+
return { kind: "transientBackend", backendKind: "connection" };
|
|
124
|
+
if (/timed out|timeout/i.test(text))
|
|
125
|
+
return { kind: "transientBackend", backendKind: "timeout" };
|
|
126
|
+
if (/\b(400|401|403|404)\b/i.test(text))
|
|
127
|
+
return { kind: "configError" };
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
/** Build a fresh per-run claude translator. */
|
|
131
|
+
export function claudeTranslator(opts = {}) {
|
|
132
|
+
/** The split-block assistant message being folded: claude emits one event per content
|
|
133
|
+
* block, so a text+tool_use turn arrives as two events with the same `message.id` and the
|
|
134
|
+
* full usage repeated on each. */
|
|
135
|
+
let pending;
|
|
136
|
+
let anonymous = 0;
|
|
137
|
+
let sawRunCost = false;
|
|
138
|
+
/** True once the run produced real work (an assistant turn or a result) — the signal that a
|
|
139
|
+
* missing `run_cost` is a gap rather than the quiet of an eventless stream. */
|
|
140
|
+
let sawActivity = false;
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
const warnings = new Set();
|
|
143
|
+
/** Emit the pending assistant message (if any) as ONE canonical `message_end`. Tool calls
|
|
144
|
+
* open separately, the moment their `tool_use` block is folded (see `startToolCall`): claude
|
|
145
|
+
* emits the assistant message before it runs the tool, and the `tool_result` only after, so
|
|
146
|
+
* opening the call here at message close would expose it to the open-call trackers only when
|
|
147
|
+
* the closing `user` event carries the result — never while the tool is actually running. */
|
|
148
|
+
const flush = () => {
|
|
149
|
+
const message = pending;
|
|
150
|
+
if (!message)
|
|
151
|
+
return [];
|
|
152
|
+
pending = undefined;
|
|
153
|
+
return [
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
type: "message_end",
|
|
156
|
+
message: {
|
|
157
|
+
role: "assistant",
|
|
158
|
+
content: message.content,
|
|
159
|
+
usage: message.usage,
|
|
160
|
+
...(message.stopReason !== undefined ? { stopReason: message.stopReason } : {}),
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
];
|
|
164
|
+
};
|
|
165
|
+
/** One canonical `tool_execution_start`, emitted as soon as a `tool_use` block is folded —
|
|
166
|
+
* while claude is still running the tool, before the `tool_result` arrives. The open-call
|
|
167
|
+
* trackers (runPi's stall warning, the dashboard flag) only see a call while it is open, so
|
|
168
|
+
* this is what lets them judge a claude call by its duration instead of its silence. */
|
|
169
|
+
const startToolCall = (block) => JSON.stringify({
|
|
170
|
+
type: "tool_execution_start",
|
|
171
|
+
toolCallId: block.id,
|
|
172
|
+
toolName: block.name,
|
|
173
|
+
args: block.arguments,
|
|
174
|
+
});
|
|
175
|
+
const record = (kind) => {
|
|
176
|
+
if (seen.has(kind))
|
|
177
|
+
return;
|
|
178
|
+
seen.add(kind);
|
|
179
|
+
warnings.add(kind);
|
|
180
|
+
};
|
|
181
|
+
const translate = (line) => {
|
|
182
|
+
const parsed = parseJsonObject(line);
|
|
183
|
+
if (!parsed)
|
|
184
|
+
return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
|
|
185
|
+
const event = parsed;
|
|
186
|
+
const type = str(event.type);
|
|
187
|
+
switch (type) {
|
|
188
|
+
case "system": {
|
|
189
|
+
const subtype = str(event.subtype);
|
|
190
|
+
if (subtype === "init") {
|
|
191
|
+
const out = [];
|
|
192
|
+
const cwd = str(event.cwd);
|
|
193
|
+
const id = str(event.session_id);
|
|
194
|
+
out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
|
|
195
|
+
out.push(JSON.stringify({ type: "agent_start" }));
|
|
196
|
+
const billing = billingOf(str(event.apiKeySource), opts.thirdPartyProvider === true);
|
|
197
|
+
if (billing)
|
|
198
|
+
out.push(JSON.stringify({ type: "billing", billing }));
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
if (subtype === "api_retry") {
|
|
202
|
+
const attempt = nonNegativeNumber(event.attempt, undefined);
|
|
203
|
+
const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
|
|
204
|
+
const status = nonNegativeNumber(event.error_status, undefined);
|
|
205
|
+
const error = str(event.error) ?? "";
|
|
206
|
+
const errorMessage = [status !== undefined ? `status ${status}` : "", error].filter(Boolean).join(": ");
|
|
207
|
+
return [
|
|
208
|
+
JSON.stringify({
|
|
209
|
+
type: "auto_retry_start",
|
|
210
|
+
...(attempt !== undefined ? { attempt } : {}),
|
|
211
|
+
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
212
|
+
errorMessage: errorMessage || "api retry",
|
|
213
|
+
}),
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
if (subtype === "compact_boundary")
|
|
217
|
+
return [JSON.stringify({ type: "compaction_start" })];
|
|
218
|
+
// A thinking-token estimate during extended thinking: known, carries nothing canonical.
|
|
219
|
+
if (subtype === "thinking_tokens")
|
|
220
|
+
return [];
|
|
221
|
+
record(`system:${subtype ?? "unknown"}`);
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
case "assistant": {
|
|
225
|
+
sawActivity = true;
|
|
226
|
+
const message = objectOrEmpty(event.message);
|
|
227
|
+
const id = str(message.id) ?? `anon-${anonymous++}`;
|
|
228
|
+
// A new id flushes the open message; a continuing one flushes nothing. Either way the
|
|
229
|
+
// fresh or open pending then folds this event's content, usage, and stop reason the
|
|
230
|
+
// same way — that shared fold is why both branches live on one path.
|
|
231
|
+
let out = [];
|
|
232
|
+
if (pending?.id !== id) {
|
|
233
|
+
out = flush();
|
|
234
|
+
pending = { id, content: [], usage: canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined) };
|
|
235
|
+
}
|
|
236
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
237
|
+
for (const raw of blocks) {
|
|
238
|
+
if (!isJsonObject(raw))
|
|
239
|
+
continue;
|
|
240
|
+
const block = canonicalBlock(raw);
|
|
241
|
+
if (!block)
|
|
242
|
+
continue;
|
|
243
|
+
pending.content.push(block);
|
|
244
|
+
if (block.type === "toolCall")
|
|
245
|
+
out.push(startToolCall(block));
|
|
246
|
+
}
|
|
247
|
+
// Usage repeats on every event of the same message; the last read wins and is counted
|
|
248
|
+
// once because the fold emits one message_end per id.
|
|
249
|
+
pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
|
|
250
|
+
const stopReason = str(message.stop_reason);
|
|
251
|
+
if (stopReason !== undefined)
|
|
252
|
+
pending.stopReason = stopReason;
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
case "user": {
|
|
256
|
+
const out = flush();
|
|
257
|
+
const message = objectOrEmpty(event.message);
|
|
258
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
259
|
+
for (const raw of blocks) {
|
|
260
|
+
if (!isJsonObject(raw) || raw.type !== "tool_result")
|
|
261
|
+
continue;
|
|
262
|
+
out.push(JSON.stringify({
|
|
263
|
+
type: "tool_execution_end",
|
|
264
|
+
toolCallId: str(raw.tool_use_id) ?? "",
|
|
265
|
+
isError: raw.is_error === true,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
case "rate_limit_event": {
|
|
271
|
+
const info = objectOrEmpty(event.rate_limit_info);
|
|
272
|
+
const rateLimitType = str(info.rateLimitType);
|
|
273
|
+
const status = str(info.status);
|
|
274
|
+
// claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
|
|
275
|
+
// keyed by window type, and only the binding window's status at the top level.
|
|
276
|
+
const windows = [];
|
|
277
|
+
for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
|
|
278
|
+
if (type === rateLimitType || !isJsonObject(raw))
|
|
279
|
+
continue;
|
|
280
|
+
const reading = { type: "rate_limit", rateLimitType: type };
|
|
281
|
+
const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
|
|
282
|
+
if (windowResets !== undefined)
|
|
283
|
+
reading.resetsAt = windowResets;
|
|
284
|
+
const windowUtil = nonNegativeNumber(raw.utilization, undefined);
|
|
285
|
+
if (windowUtil !== undefined)
|
|
286
|
+
reading.utilization = windowUtil;
|
|
287
|
+
windows.push(JSON.stringify(reading));
|
|
288
|
+
}
|
|
289
|
+
const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
|
|
290
|
+
const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
|
|
291
|
+
const out = { type: "rate_limit" };
|
|
292
|
+
if (rateLimitType !== undefined)
|
|
293
|
+
out.rateLimitType = rateLimitType;
|
|
294
|
+
if (status !== undefined)
|
|
295
|
+
out.status = status;
|
|
296
|
+
if (resetsAt !== undefined)
|
|
297
|
+
out.resetsAt = resetsAt;
|
|
298
|
+
const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
299
|
+
if (utilization !== undefined)
|
|
300
|
+
out.utilization = utilization;
|
|
301
|
+
const overageStatus = str(info.overageStatus);
|
|
302
|
+
if (overageStatus !== undefined)
|
|
303
|
+
out.overageStatus = overageStatus;
|
|
304
|
+
if (info.isUsingOverage === true)
|
|
305
|
+
out.isUsingOverage = true;
|
|
306
|
+
if (status === "rejected" && resetsAt !== undefined) {
|
|
307
|
+
// resetsAt is a unix-seconds stamp; retryAfterSeconds is the wait the loop honors.
|
|
308
|
+
out.retryAfterSeconds = Math.max(0, Math.round(resetsAt - Date.now() / 1000));
|
|
309
|
+
}
|
|
310
|
+
return [JSON.stringify(out), ...windows];
|
|
311
|
+
}
|
|
312
|
+
case "result": {
|
|
313
|
+
sawActivity = true;
|
|
314
|
+
const out = flush();
|
|
315
|
+
// The assistant events' usage is the message-start snapshot, so the result's total
|
|
316
|
+
// is the run's real output-token count (thinking included).
|
|
317
|
+
const outputTokens = nonNegativeNumber(objectOrEmpty(event.usage).output_tokens, undefined);
|
|
318
|
+
if (outputTokens !== undefined)
|
|
319
|
+
out.push(JSON.stringify({ type: "run_usage", outputTokens }));
|
|
320
|
+
const total = nonNegativeNumber(event.total_cost_usd, undefined);
|
|
321
|
+
if (total !== undefined) {
|
|
322
|
+
sawRunCost = true;
|
|
323
|
+
out.push(JSON.stringify({ type: "run_cost", total }));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
warnings.add("cost_unknown");
|
|
327
|
+
}
|
|
328
|
+
if (event.is_error === true) {
|
|
329
|
+
const { kind, backendKind } = classifyResult(event);
|
|
330
|
+
const message = {
|
|
331
|
+
role: "assistant",
|
|
332
|
+
content: [],
|
|
333
|
+
stopReason: "error",
|
|
334
|
+
};
|
|
335
|
+
const errorMessage = str(event.result) ?? str(event.error);
|
|
336
|
+
if (errorMessage !== undefined)
|
|
337
|
+
message.errorMessage = errorMessage;
|
|
338
|
+
// tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
|
|
339
|
+
// reads event.tumwaterKind and trusts it before its text regexes.
|
|
340
|
+
out.push(JSON.stringify({
|
|
341
|
+
type: "message_end",
|
|
342
|
+
message,
|
|
343
|
+
...(kind !== undefined ? { tumwaterKind: kind } : {}),
|
|
344
|
+
...(backendKind !== undefined ? { backendKind } : {}),
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
case "tool_progress":
|
|
350
|
+
// A 30 s heartbeat of a running tool call: known, and the open call already tracks liveness.
|
|
351
|
+
return [];
|
|
352
|
+
default:
|
|
353
|
+
record(type ?? "unknown");
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
translate,
|
|
359
|
+
finalize() {
|
|
360
|
+
// The stream ended (a completed run's result already flushed, a killed one did not):
|
|
361
|
+
// emit whatever assistant message is still pending so the last turn is not lost.
|
|
362
|
+
return flush();
|
|
363
|
+
},
|
|
364
|
+
warnings() {
|
|
365
|
+
// A claude run's authoritative cost is the result's total_cost_usd; a stream that did
|
|
366
|
+
// real work yet never produced that total has no trustworthy cost. Recorded lazily so a
|
|
367
|
+
// killed run's translator still reports it.
|
|
368
|
+
if (!sawRunCost && sawActivity)
|
|
369
|
+
warnings.add("cost_unknown");
|
|
370
|
+
return [...warnings];
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** The run's billing, from `system/init.apiKeySource`: claude.ai OAuth reports `"none"`, an
|
|
375
|
+
* environment key reports `ANTHROPIC_API_KEY`, a helper reports `apiKeyHelper`. A
|
|
376
|
+
* third-party provider overrides both — it is API billing even when the init omitted a
|
|
377
|
+
* source. Undefined when the init named no source and no third-party flag applied. */
|
|
378
|
+
function billingOf(apiKeySource, thirdPartyProvider) {
|
|
379
|
+
if (thirdPartyProvider)
|
|
380
|
+
return "api";
|
|
381
|
+
if (apiKeySource === undefined || apiKeySource === "")
|
|
382
|
+
return undefined;
|
|
383
|
+
return apiKeySource === "none" ? "subscription" : "api";
|
|
384
|
+
}
|