@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.9445952e33ba
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 +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1563 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +67 -20
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +28 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +17 -8
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +132 -103
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +485 -31
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +126 -17
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +57 -9
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
|
@@ -19,6 +19,8 @@ export interface ProviderCliDraftContext {
|
|
|
19
19
|
}
|
|
20
20
|
export interface ProviderCliRunResult {
|
|
21
21
|
readonly summary: string;
|
|
22
|
+
readonly resultCommitSha?: string | null;
|
|
23
|
+
readonly publishedCommitSha?: string | null;
|
|
22
24
|
readonly spec?: RunnerDispatchSpec | null;
|
|
23
25
|
readonly artifact?: RunnerDispatchArtifact | null;
|
|
24
26
|
readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
|
|
@@ -4,6 +4,8 @@ export declare function messageFromError(error: unknown): string;
|
|
|
4
4
|
export declare function clip(value: string, maxLength: number): string;
|
|
5
5
|
export interface ProviderOutputState {
|
|
6
6
|
readonly sessionLabel: string | null;
|
|
7
|
+
/** Stable provider conversation identifier used for an in-place repair turn. */
|
|
8
|
+
readonly providerThreadId: string | null;
|
|
7
9
|
finalMessage: string | null;
|
|
8
10
|
/** True only while the provider has reported a tool/command start without a matching completion. */
|
|
9
11
|
readonly hasActiveToolCall: boolean;
|
|
@@ -13,6 +15,14 @@ export interface ProviderOutputState {
|
|
|
13
15
|
}
|
|
14
16
|
export declare function createProviderOutputState(provider: string): ProviderOutputState;
|
|
15
17
|
export declare function parseJsonLine(line: string): Record<string, unknown> | null;
|
|
18
|
+
/**
|
|
19
|
+
* Claude Code exposes MCP tools as `mcp__<server>__<tool>` and replaces characters outside
|
|
20
|
+
* `[A-Za-z0-9_-]` with `_`, so `forge.complete_qa` arrives as `mcp__forge__forge_complete_qa`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseClaudeMcpToolName(name: string): {
|
|
23
|
+
readonly server: string;
|
|
24
|
+
readonly tool: string;
|
|
25
|
+
} | null;
|
|
16
26
|
export declare function formatCodexMcpToolCallEvents(type: string, event: Record<string, unknown>): readonly RunnerDispatchEvent[];
|
|
17
27
|
export declare function objectField(source: Record<string, unknown>, key: string): Record<string, unknown>;
|
|
18
28
|
export declare function stringField(source: Record<string, unknown>, key: string): string | null;
|
|
@@ -16,9 +16,14 @@ export function clip(value, maxLength) {
|
|
|
16
16
|
return `${value.slice(0, maxLength - ellipsis.length)}${ellipsis}`;
|
|
17
17
|
}
|
|
18
18
|
export function createProviderOutputState(provider) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const normalized = provider.trim().toLowerCase();
|
|
20
|
+
if (normalized === "cursor") {
|
|
21
|
+
return new CursorStreamJsonState();
|
|
22
|
+
}
|
|
23
|
+
if (normalized === "claude") {
|
|
24
|
+
return new ClaudeStreamJsonState();
|
|
25
|
+
}
|
|
26
|
+
return new CodexJsonState();
|
|
22
27
|
}
|
|
23
28
|
class CursorStreamJsonState {
|
|
24
29
|
sessionId = null;
|
|
@@ -29,6 +34,9 @@ class CursorStreamJsonState {
|
|
|
29
34
|
get sessionLabel() {
|
|
30
35
|
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
31
36
|
}
|
|
37
|
+
get providerThreadId() {
|
|
38
|
+
return this.sessionId;
|
|
39
|
+
}
|
|
32
40
|
get usage() {
|
|
33
41
|
return this.accumulatedUsage;
|
|
34
42
|
}
|
|
@@ -49,7 +57,8 @@ class CursorStreamJsonState {
|
|
|
49
57
|
role: "runner",
|
|
50
58
|
kind: "checkpoint",
|
|
51
59
|
content: `Cursor session started: ${this.sessionId}.`,
|
|
52
|
-
eventType: "provider_thread_started"
|
|
60
|
+
eventType: "provider_thread_started",
|
|
61
|
+
data: JSON.stringify({ threadId: this.sessionId })
|
|
53
62
|
}];
|
|
54
63
|
}
|
|
55
64
|
}
|
|
@@ -116,13 +125,19 @@ class CursorStreamJsonState {
|
|
|
116
125
|
if (Object.keys(usage).length === 0 && durationMs === 0) {
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
128
|
+
const uncachedInputTokens = readUsageNumber(usage, "inputTokens");
|
|
129
|
+
const cacheReadTokens = readUsageNumber(usage, "cacheReadTokens");
|
|
130
|
+
const cacheCreationTokens = readUsageNumber(usage, "cacheWriteTokens");
|
|
119
131
|
this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
|
|
120
|
-
inputTokens:
|
|
132
|
+
inputTokens: uncachedInputTokens + cacheReadTokens + cacheCreationTokens,
|
|
133
|
+
uncachedInputTokens,
|
|
121
134
|
outputTokens: readUsageNumber(usage, "outputTokens"),
|
|
122
|
-
cacheReadTokens
|
|
123
|
-
cacheCreationTokens
|
|
135
|
+
cacheReadTokens,
|
|
136
|
+
cacheCreationTokens,
|
|
124
137
|
providerDurationMs: durationMs > 0 ? durationMs : null,
|
|
125
|
-
source: "provider_reported"
|
|
138
|
+
source: "provider_reported",
|
|
139
|
+
inputTokenSemantics: "excludes_cache_tokens",
|
|
140
|
+
rawUsage: [{ provider: "cursor", usage }]
|
|
126
141
|
});
|
|
127
142
|
}
|
|
128
143
|
}
|
|
@@ -225,6 +240,292 @@ export function parseJsonLine(line) {
|
|
|
225
240
|
? parsed
|
|
226
241
|
: null;
|
|
227
242
|
}
|
|
243
|
+
const claudeFileEditTools = new Set(["edit", "multiedit", "write", "notebookedit"]);
|
|
244
|
+
/**
|
|
245
|
+
* Parses `claude -p --output-format stream-json --verbose` output. Each line is one of:
|
|
246
|
+
* `system/init`, `assistant` (text + tool_use blocks), `user` (tool_result blocks), or the
|
|
247
|
+
* terminal `result` event that carries the aggregated usage for the whole run.
|
|
248
|
+
*/
|
|
249
|
+
class ClaudeStreamJsonState {
|
|
250
|
+
sessionId = null;
|
|
251
|
+
finalMessage = null;
|
|
252
|
+
emittedSession = false;
|
|
253
|
+
accumulatedUsage = null;
|
|
254
|
+
activeToolUses = new Map();
|
|
255
|
+
get sessionLabel() {
|
|
256
|
+
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
257
|
+
}
|
|
258
|
+
get providerThreadId() {
|
|
259
|
+
return this.sessionId;
|
|
260
|
+
}
|
|
261
|
+
get usage() {
|
|
262
|
+
return this.accumulatedUsage;
|
|
263
|
+
}
|
|
264
|
+
get hasActiveToolCall() {
|
|
265
|
+
return this.activeToolUses.size > 0;
|
|
266
|
+
}
|
|
267
|
+
consume(line) {
|
|
268
|
+
const event = parseJsonLine(line);
|
|
269
|
+
if (!event) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const type = stringField(event, "type");
|
|
273
|
+
if (type === "system" && stringField(event, "subtype") === "init") {
|
|
274
|
+
return this.consumeInit(event);
|
|
275
|
+
}
|
|
276
|
+
if (type === "assistant") {
|
|
277
|
+
return this.consumeAssistant(event);
|
|
278
|
+
}
|
|
279
|
+
if (type === "user") {
|
|
280
|
+
return this.consumeToolResults(event);
|
|
281
|
+
}
|
|
282
|
+
if (type === "result") {
|
|
283
|
+
return this.consumeResult(event, line);
|
|
284
|
+
}
|
|
285
|
+
if (type === "error") {
|
|
286
|
+
return [providerCliError(line)];
|
|
287
|
+
}
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
consumeInit(event) {
|
|
291
|
+
const events = [];
|
|
292
|
+
this.sessionId = stringField(event, "session_id") ?? this.sessionId;
|
|
293
|
+
if (this.sessionId && !this.emittedSession) {
|
|
294
|
+
this.emittedSession = true;
|
|
295
|
+
events.push({
|
|
296
|
+
role: "runner",
|
|
297
|
+
kind: "checkpoint",
|
|
298
|
+
content: `Claude session started: ${this.sessionId}.`,
|
|
299
|
+
eventType: "provider_thread_started"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const servers = Array.isArray(event.mcp_servers) ? event.mcp_servers : [];
|
|
303
|
+
for (const entry of servers) {
|
|
304
|
+
if (!entry || typeof entry !== "object") {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const server = entry;
|
|
308
|
+
const name = stringField(server, "name");
|
|
309
|
+
const status = stringField(server, "status");
|
|
310
|
+
if (name && status && status !== "connected") {
|
|
311
|
+
events.push({
|
|
312
|
+
role: "runner",
|
|
313
|
+
kind: "checkpoint",
|
|
314
|
+
content: `Claude MCP server ${name} is ${status}.`,
|
|
315
|
+
eventType: "provider_mcp_server_status",
|
|
316
|
+
data: JSON.stringify({ server_name: name, status })
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return events;
|
|
321
|
+
}
|
|
322
|
+
consumeAssistant(event) {
|
|
323
|
+
const events = [];
|
|
324
|
+
const text = assistantMessageText(event);
|
|
325
|
+
if (text) {
|
|
326
|
+
this.finalMessage = text;
|
|
327
|
+
events.push({
|
|
328
|
+
role: "assistant",
|
|
329
|
+
kind: "message",
|
|
330
|
+
content: clip(text, 4000),
|
|
331
|
+
eventType: "provider_message"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
for (const block of messageContentBlocks(event)) {
|
|
335
|
+
if (stringField(block, "type") !== "tool_use") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const id = stringField(block, "id");
|
|
339
|
+
const name = stringField(block, "name");
|
|
340
|
+
if (!id || !name) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const toolUse = { name, input: objectField(block, "input") };
|
|
344
|
+
this.activeToolUses.set(id, toolUse);
|
|
345
|
+
events.push(...formatClaudeToolUseStarted(toolUse));
|
|
346
|
+
}
|
|
347
|
+
return events;
|
|
348
|
+
}
|
|
349
|
+
consumeToolResults(event) {
|
|
350
|
+
const events = [];
|
|
351
|
+
for (const block of messageContentBlocks(event)) {
|
|
352
|
+
if (stringField(block, "type") !== "tool_result") {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const id = stringField(block, "tool_use_id");
|
|
356
|
+
const toolUse = id ? this.activeToolUses.get(id) : undefined;
|
|
357
|
+
if (!id || !toolUse) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
this.activeToolUses.delete(id);
|
|
361
|
+
events.push(...formatClaudeToolUseCompleted(toolUse, block.is_error === true, toolResultText(block)));
|
|
362
|
+
}
|
|
363
|
+
return events;
|
|
364
|
+
}
|
|
365
|
+
consumeResult(event, line) {
|
|
366
|
+
this.activeToolUses.clear();
|
|
367
|
+
this.accumulateUsage(event);
|
|
368
|
+
const result = stringField(event, "result");
|
|
369
|
+
const subtype = stringField(event, "subtype") ?? "";
|
|
370
|
+
const failed = event.is_error === true || subtype.startsWith("error");
|
|
371
|
+
if (!failed) {
|
|
372
|
+
if (result) {
|
|
373
|
+
this.finalMessage = result;
|
|
374
|
+
}
|
|
375
|
+
return [{
|
|
376
|
+
role: "runner",
|
|
377
|
+
kind: "checkpoint",
|
|
378
|
+
content: "Claude turn completed.",
|
|
379
|
+
eventType: "provider_turn_completed"
|
|
380
|
+
}];
|
|
381
|
+
}
|
|
382
|
+
return [providerCliError(line)];
|
|
383
|
+
}
|
|
384
|
+
accumulateUsage(event) {
|
|
385
|
+
const usage = objectField(event, "usage");
|
|
386
|
+
const durationMs = readUsageNumber(event, "duration_ms") || readUsageNumber(event, "duration_api_ms");
|
|
387
|
+
if (Object.keys(usage).length === 0 && durationMs === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// Anthropic's input_tokens excludes cache reads and writes; normalize to a cache-inclusive total.
|
|
391
|
+
const uncachedInputTokens = readUsageNumber(usage, "input_tokens");
|
|
392
|
+
const cacheReadTokens = readUsageNumber(usage, "cache_read_input_tokens");
|
|
393
|
+
const cacheCreationTokens = readUsageNumber(usage, "cache_creation_input_tokens");
|
|
394
|
+
this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
|
|
395
|
+
inputTokens: uncachedInputTokens + cacheReadTokens + cacheCreationTokens,
|
|
396
|
+
uncachedInputTokens,
|
|
397
|
+
outputTokens: readUsageNumber(usage, "output_tokens"),
|
|
398
|
+
cacheReadTokens,
|
|
399
|
+
cacheCreationTokens,
|
|
400
|
+
providerDurationMs: durationMs > 0 ? durationMs : null,
|
|
401
|
+
source: "provider_reported",
|
|
402
|
+
inputTokenSemantics: "excludes_cache_tokens",
|
|
403
|
+
rawUsage: [{ provider: "claude", usage }]
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function providerCliError(line) {
|
|
408
|
+
return {
|
|
409
|
+
role: "runner",
|
|
410
|
+
kind: "error",
|
|
411
|
+
content: clip(line.trim(), 1000),
|
|
412
|
+
eventType: "provider_cli_error"
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function messageContentBlocks(event) {
|
|
416
|
+
const content = objectField(event, "message").content;
|
|
417
|
+
if (!Array.isArray(content)) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
return content.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry));
|
|
421
|
+
}
|
|
422
|
+
function toolResultText(block) {
|
|
423
|
+
const content = block.content;
|
|
424
|
+
if (typeof content === "string") {
|
|
425
|
+
return content.trim() || null;
|
|
426
|
+
}
|
|
427
|
+
if (!Array.isArray(content)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
const text = content
|
|
431
|
+
.map((entry) => entry && typeof entry === "object" ? stringField(entry, "text") : null)
|
|
432
|
+
.filter((part) => Boolean(part))
|
|
433
|
+
.join("\n")
|
|
434
|
+
.trim();
|
|
435
|
+
return text || null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Claude Code exposes MCP tools as `mcp__<server>__<tool>` and replaces characters outside
|
|
439
|
+
* `[A-Za-z0-9_-]` with `_`, so `forge.complete_qa` arrives as `mcp__forge__forge_complete_qa`.
|
|
440
|
+
*/
|
|
441
|
+
export function parseClaudeMcpToolName(name) {
|
|
442
|
+
const match = /^mcp__(.+?)__(.+)$/u.exec(name);
|
|
443
|
+
if (!match) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const server = match[1] ?? "";
|
|
447
|
+
const rawTool = match[2] ?? "";
|
|
448
|
+
const tool = server === "forge" && rawTool.startsWith("forge_") ? `forge.${rawTool.slice("forge_".length)}` : rawTool;
|
|
449
|
+
return server && tool ? { server, tool } : null;
|
|
450
|
+
}
|
|
451
|
+
function claudeMcpEvents(toolUse, phase, errorText) {
|
|
452
|
+
const mcp = parseClaudeMcpToolName(toolUse.name);
|
|
453
|
+
if (!mcp) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const item = {
|
|
457
|
+
type: "mcp_tool_call",
|
|
458
|
+
server: mcp.server,
|
|
459
|
+
tool: mcp.tool,
|
|
460
|
+
status: phase
|
|
461
|
+
};
|
|
462
|
+
if (phase === "failed") {
|
|
463
|
+
item.error = errorText ? clip(errorText, 600) : "MCP tool call failed.";
|
|
464
|
+
}
|
|
465
|
+
return formatCodexMcpToolCallEvents(phase === "started" ? "item.started" : "item.completed", { item });
|
|
466
|
+
}
|
|
467
|
+
function formatClaudeToolUseStarted(toolUse) {
|
|
468
|
+
const mcp = claudeMcpEvents(toolUse, "started", null);
|
|
469
|
+
if (mcp) {
|
|
470
|
+
return mcp;
|
|
471
|
+
}
|
|
472
|
+
const normalized = toolUse.name.toLowerCase();
|
|
473
|
+
const filePath = stringField(toolUse.input, "file_path") ?? stringField(toolUse.input, "notebook_path") ?? stringField(toolUse.input, "path");
|
|
474
|
+
let content;
|
|
475
|
+
if (normalized === "bash") {
|
|
476
|
+
const command = stringField(toolUse.input, "command");
|
|
477
|
+
content = command ? `Shell command started: ${clip(command, 500)}` : "Shell command started.";
|
|
478
|
+
}
|
|
479
|
+
else if (normalized === "read" && filePath) {
|
|
480
|
+
content = `Reading ${filePath}`;
|
|
481
|
+
}
|
|
482
|
+
else if (claudeFileEditTools.has(normalized) && filePath) {
|
|
483
|
+
content = `Writing ${filePath}`;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
content = `Tool call started: ${toolUse.name}`;
|
|
487
|
+
}
|
|
488
|
+
return [{
|
|
489
|
+
role: "worker",
|
|
490
|
+
kind: "checkpoint",
|
|
491
|
+
content,
|
|
492
|
+
eventType: normalized === "bash" ? "provider_command" : "provider_tool_call"
|
|
493
|
+
}];
|
|
494
|
+
}
|
|
495
|
+
function formatClaudeToolUseCompleted(toolUse, isError, resultText) {
|
|
496
|
+
const mcp = claudeMcpEvents(toolUse, isError ? "failed" : "completed", resultText);
|
|
497
|
+
if (mcp) {
|
|
498
|
+
return mcp;
|
|
499
|
+
}
|
|
500
|
+
const normalized = toolUse.name.toLowerCase();
|
|
501
|
+
const filePath = stringField(toolUse.input, "file_path") ?? stringField(toolUse.input, "notebook_path") ?? stringField(toolUse.input, "path");
|
|
502
|
+
if (claudeFileEditTools.has(normalized) && filePath && !isError) {
|
|
503
|
+
return [{
|
|
504
|
+
role: "worker",
|
|
505
|
+
kind: "file_edit",
|
|
506
|
+
content: `Wrote ${filePath}`,
|
|
507
|
+
eventType: "provider_file_edit"
|
|
508
|
+
}];
|
|
509
|
+
}
|
|
510
|
+
const status = isError ? "failed" : "completed";
|
|
511
|
+
let content;
|
|
512
|
+
if (normalized === "bash") {
|
|
513
|
+
const command = stringField(toolUse.input, "command");
|
|
514
|
+
content = command ? `Shell command ${status}: ${clip(command, 500)}` : `Shell command ${status}.`;
|
|
515
|
+
}
|
|
516
|
+
else if (normalized === "read" && filePath) {
|
|
517
|
+
content = isError ? `Read failed: ${filePath}` : `Read ${filePath}`;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
content = `Tool call ${status}: ${toolUse.name}`;
|
|
521
|
+
}
|
|
522
|
+
return [{
|
|
523
|
+
role: "worker",
|
|
524
|
+
kind: "checkpoint",
|
|
525
|
+
content,
|
|
526
|
+
eventType: normalized === "bash" ? "provider_command" : "provider_tool_call"
|
|
527
|
+
}];
|
|
528
|
+
}
|
|
228
529
|
class CodexJsonState {
|
|
229
530
|
threadId = null;
|
|
230
531
|
finalMessage = null;
|
|
@@ -234,6 +535,9 @@ class CodexJsonState {
|
|
|
234
535
|
get sessionLabel() {
|
|
235
536
|
return this.threadId ? `thread ${this.threadId}` : null;
|
|
236
537
|
}
|
|
538
|
+
get providerThreadId() {
|
|
539
|
+
return this.threadId;
|
|
540
|
+
}
|
|
237
541
|
get usage() {
|
|
238
542
|
return this.accumulatedUsage;
|
|
239
543
|
}
|
|
@@ -255,7 +559,8 @@ class CodexJsonState {
|
|
|
255
559
|
role: "runner",
|
|
256
560
|
kind: "checkpoint",
|
|
257
561
|
content: `Codex thread started: ${this.threadId}.`,
|
|
258
|
-
eventType: "provider_thread_started"
|
|
562
|
+
eventType: "provider_thread_started",
|
|
563
|
+
data: JSON.stringify({ threadId: this.threadId })
|
|
259
564
|
}];
|
|
260
565
|
}
|
|
261
566
|
}
|
|
@@ -332,12 +637,17 @@ class CodexJsonState {
|
|
|
332
637
|
if (Object.keys(usage).length === 0) {
|
|
333
638
|
return;
|
|
334
639
|
}
|
|
640
|
+
const inputTokens = readUsageNumber(usage, "input_tokens");
|
|
641
|
+
const cacheReadTokens = readUsageNumber(usage, "cached_input_tokens");
|
|
335
642
|
this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
|
|
336
|
-
inputTokens
|
|
643
|
+
inputTokens,
|
|
644
|
+
uncachedInputTokens: Math.max(0, inputTokens - cacheReadTokens),
|
|
337
645
|
outputTokens: readUsageNumber(usage, "output_tokens"),
|
|
338
|
-
cacheReadTokens
|
|
646
|
+
cacheReadTokens,
|
|
339
647
|
reasoningOutputTokens: readUsageNumber(usage, "reasoning_output_tokens"),
|
|
340
|
-
source: "provider_reported"
|
|
648
|
+
source: "provider_reported",
|
|
649
|
+
inputTokenSemantics: "includes_cached_input",
|
|
650
|
+
rawUsage: [{ provider: "codex", usage }]
|
|
341
651
|
});
|
|
342
652
|
}
|
|
343
653
|
}
|
|
@@ -425,11 +735,45 @@ function mcpErrorSummary(item) {
|
|
|
425
735
|
if (direct) {
|
|
426
736
|
return clip(direct, 600);
|
|
427
737
|
}
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
738
|
+
const messages = uniqueErrorMessages([
|
|
739
|
+
item.error,
|
|
740
|
+
item.result,
|
|
741
|
+
item.content,
|
|
742
|
+
]);
|
|
743
|
+
return messages.length > 0 ? clip(messages.join(" Caused by: "), 600) : null;
|
|
744
|
+
}
|
|
745
|
+
function uniqueErrorMessages(values) {
|
|
746
|
+
const messages = [];
|
|
747
|
+
const seenMessages = new Set();
|
|
748
|
+
const seenObjects = new Set();
|
|
749
|
+
const visit = (value, depth) => {
|
|
750
|
+
if (depth > 8 || value === null || value === undefined)
|
|
751
|
+
return;
|
|
752
|
+
if (typeof value === "string") {
|
|
753
|
+
const message = value.trim();
|
|
754
|
+
if (message.length > 0 && !seenMessages.has(message)) {
|
|
755
|
+
seenMessages.add(message);
|
|
756
|
+
messages.push(message);
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (typeof value !== "object" || seenObjects.has(value))
|
|
761
|
+
return;
|
|
762
|
+
seenObjects.add(value);
|
|
763
|
+
if (Array.isArray(value)) {
|
|
764
|
+
for (const entry of value)
|
|
765
|
+
visit(entry, depth + 1);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const record = value;
|
|
769
|
+
for (const key of ["message", "summary", "error", "cause", "data", "details", "result", "content"]) {
|
|
770
|
+
if (key in record)
|
|
771
|
+
visit(record[key], depth + 1);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
for (const value of values)
|
|
775
|
+
visit(value, 0);
|
|
776
|
+
return messages;
|
|
433
777
|
}
|
|
434
778
|
function safeProviderMcpData(item, serverName, toolName, status) {
|
|
435
779
|
const elapsedMs = numericField(item, "elapsed_ms") ?? numericField(item, "duration_ms");
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import type { RunOnceDispatchPacket, RunnerDispatchEvent } from "../session.types.js";
|
|
2
2
|
export type UsageSource = "provider_reported" | "cli_reported" | "estimated" | "unknown";
|
|
3
|
+
export type InputTokenSemantics = "includes_cached_input" | "excludes_cache_tokens" | "unknown";
|
|
4
|
+
export declare const PROVIDER_USAGE_SCHEMA_VERSION = 1;
|
|
3
5
|
/**
|
|
4
6
|
* Normalized, provider-agnostic token usage accumulated for a dispatch. Cost is never computed
|
|
5
7
|
* here; the API recomputes cost from these tokens x model rates (ADR-0006).
|
|
6
8
|
*/
|
|
7
9
|
export interface ProviderUsage {
|
|
10
|
+
/** Normalized total input. Cached input is a subset, never an additive peer. */
|
|
8
11
|
readonly inputTokens: number;
|
|
12
|
+
readonly uncachedInputTokens: number | null;
|
|
9
13
|
readonly outputTokens: number;
|
|
10
14
|
readonly cacheReadTokens: number;
|
|
11
15
|
readonly cacheCreationTokens: number;
|
|
12
16
|
readonly reasoningOutputTokens: number;
|
|
13
17
|
readonly providerDurationMs: number | null;
|
|
14
18
|
readonly source: UsageSource;
|
|
19
|
+
readonly inputTokenSemantics: InputTokenSemantics;
|
|
20
|
+
/** Exact provider usage objects retained for audit/re-normalization. */
|
|
21
|
+
readonly rawUsage: readonly Record<string, unknown>[];
|
|
15
22
|
}
|
|
16
23
|
export declare function emptyUsage(source?: UsageSource): ProviderUsage;
|
|
17
24
|
export declare function addUsage(base: ProviderUsage, next: Partial<ProviderUsage>): ProviderUsage;
|
|
@@ -1,27 +1,38 @@
|
|
|
1
|
+
export const PROVIDER_USAGE_SCHEMA_VERSION = 1;
|
|
1
2
|
export function emptyUsage(source = "unknown") {
|
|
2
3
|
return {
|
|
3
4
|
inputTokens: 0,
|
|
5
|
+
uncachedInputTokens: 0,
|
|
4
6
|
outputTokens: 0,
|
|
5
7
|
cacheReadTokens: 0,
|
|
6
8
|
cacheCreationTokens: 0,
|
|
7
9
|
reasoningOutputTokens: 0,
|
|
8
10
|
providerDurationMs: null,
|
|
9
|
-
source
|
|
11
|
+
source,
|
|
12
|
+
inputTokenSemantics: "unknown",
|
|
13
|
+
rawUsage: []
|
|
10
14
|
};
|
|
11
15
|
}
|
|
12
16
|
export function addUsage(base, next) {
|
|
17
|
+
const nextUncached = next.uncachedInputTokens;
|
|
18
|
+
const uncachedInputTokens = base.uncachedInputTokens === null || nextUncached === null
|
|
19
|
+
? null
|
|
20
|
+
: base.uncachedInputTokens + nonNegative(nextUncached);
|
|
13
21
|
return {
|
|
14
22
|
inputTokens: base.inputTokens + nonNegative(next.inputTokens),
|
|
23
|
+
uncachedInputTokens,
|
|
15
24
|
outputTokens: base.outputTokens + nonNegative(next.outputTokens),
|
|
16
25
|
cacheReadTokens: base.cacheReadTokens + nonNegative(next.cacheReadTokens),
|
|
17
26
|
cacheCreationTokens: base.cacheCreationTokens + nonNegative(next.cacheCreationTokens),
|
|
18
27
|
reasoningOutputTokens: base.reasoningOutputTokens + nonNegative(next.reasoningOutputTokens),
|
|
19
28
|
providerDurationMs: next.providerDurationMs ?? base.providerDurationMs,
|
|
20
|
-
source: next.source ?? base.source
|
|
29
|
+
source: next.source ?? base.source,
|
|
30
|
+
inputTokenSemantics: mergeSemantics(base.inputTokenSemantics, next.inputTokenSemantics),
|
|
31
|
+
rawUsage: [...base.rawUsage, ...(next.rawUsage ?? [])]
|
|
21
32
|
};
|
|
22
33
|
}
|
|
23
34
|
export function totalTokens(usage) {
|
|
24
|
-
return usage.inputTokens + usage.outputTokens
|
|
35
|
+
return usage.inputTokens + usage.outputTokens;
|
|
25
36
|
}
|
|
26
37
|
export function hasUsage(usage) {
|
|
27
38
|
return usage !== null && totalTokens(usage) > 0;
|
|
@@ -37,20 +48,26 @@ export function buildProviderUsageEvent(dispatch, usage) {
|
|
|
37
48
|
lane: dispatch.execution.lane,
|
|
38
49
|
phase: dispatch.cycleRun.phase,
|
|
39
50
|
attempt: dispatch.cycleRun.attempt,
|
|
51
|
+
usage_schema_version: PROVIDER_USAGE_SCHEMA_VERSION,
|
|
52
|
+
input_token_semantics: usage.inputTokenSemantics,
|
|
40
53
|
input_tokens: usage.inputTokens,
|
|
54
|
+
uncached_input_tokens: usage.uncachedInputTokens,
|
|
41
55
|
output_tokens: usage.outputTokens,
|
|
42
56
|
cache_read_tokens: usage.cacheReadTokens,
|
|
43
57
|
cache_creation_tokens: usage.cacheCreationTokens,
|
|
44
58
|
reasoning_output_tokens: usage.reasoningOutputTokens,
|
|
45
59
|
metered: dispatch.execution.authKind === "api_key",
|
|
46
60
|
usage_source: usage.source,
|
|
47
|
-
provider_duration_ms: usage.providerDurationMs
|
|
61
|
+
provider_duration_ms: usage.providerDurationMs,
|
|
62
|
+
normalized_total_tokens: totalTokens(usage),
|
|
63
|
+
normalized_total_formula: "total_input + output; cached_input is a subset of total_input",
|
|
64
|
+
raw_usage: usage.rawUsage
|
|
48
65
|
};
|
|
49
66
|
const total = totalTokens(usage);
|
|
50
67
|
return {
|
|
51
68
|
role: "runner",
|
|
52
69
|
kind: "checkpoint",
|
|
53
|
-
content: `Provider usage: ${total} tokens (${usage.inputTokens} in / ${usage.outputTokens} out) for ${dispatch.execution.model}.`,
|
|
70
|
+
content: `Provider usage: ${total} tokens (${usage.inputTokens} total in, including ${usage.cacheReadTokens} cached / ${usage.outputTokens} out) for ${dispatch.execution.model}.`,
|
|
54
71
|
eventType: "provider_usage",
|
|
55
72
|
data: JSON.stringify(data)
|
|
56
73
|
};
|
|
@@ -62,3 +79,9 @@ export function readUsageNumber(source, key) {
|
|
|
62
79
|
function nonNegative(value) {
|
|
63
80
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
64
81
|
}
|
|
82
|
+
function mergeSemantics(base, next) {
|
|
83
|
+
if (!next || next === "unknown") {
|
|
84
|
+
return base;
|
|
85
|
+
}
|
|
86
|
+
return base === "unknown" || base === next ? next : "unknown";
|
|
87
|
+
}
|