ai-foreman 1.7.11 → 1.7.13
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 +15 -0
- package/dist/activity.d.ts +5 -1
- package/dist/activity.js +1 -1
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +133 -7
- package/dist/adapters/codex.d.ts +5 -0
- package/dist/adapters/codex.js +121 -11
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +11 -1
- package/dist/adapters/types.d.ts +47 -1
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +8 -2
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +12 -3
- package/dist/branch/runner.js +131 -67
- package/dist/buildRuns.d.ts +6 -2
- package/dist/buildRuns.js +76 -32
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +84 -1
- package/dist/cli/manager.d.ts +17 -0
- package/dist/cli/manager.js +135 -0
- package/dist/cli/recovery.d.ts +4 -0
- package/dist/cli/recovery.js +80 -0
- package/dist/cli/start.js +446 -142
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +27 -9
- package/dist/config.d.ts +4 -0
- package/dist/config.js +28 -1
- package/dist/continuity.js +14 -9
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +26 -6
- package/dist/foreman.js +147 -104
- package/dist/handoffs.d.ts +35 -3
- package/dist/handoffs.js +155 -17
- package/dist/index.js +6 -0
- package/dist/log.d.ts +3 -3
- package/dist/log.js +4 -1
- package/dist/managerPacket.d.ts +25 -0
- package/dist/managerPacket.js +83 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +4 -1
- package/dist/notify.d.ts +4 -0
- package/dist/notify.js +15 -0
- package/dist/observability.d.ts +248 -0
- package/dist/observability.js +1018 -0
- package/dist/planChanges.d.ts +4 -0
- package/dist/planChanges.js +16 -0
- package/dist/projectDiagnostics.d.ts +27 -0
- package/dist/projectDiagnostics.js +407 -0
- package/dist/qaRecovery.d.ts +140 -0
- package/dist/qaRecovery.js +595 -0
- package/dist/qaReview.d.ts +133 -14
- package/dist/qaReview.js +984 -59
- package/dist/qaSnapshot.d.ts +31 -5
- package/dist/qaSnapshot.js +209 -134
- package/dist/recoveryPolicy.d.ts +37 -0
- package/dist/recoveryPolicy.js +251 -0
- package/dist/roles.js +1 -0
- package/dist/supervisor.d.ts +40 -0
- package/dist/supervisor.js +102 -0
- package/dist/ticketHistoryReader.d.ts +19 -0
- package/dist/ticketHistoryReader.js +58 -0
- package/dist/tickets/details.d.ts +8 -0
- package/dist/tickets/details.js +59 -11
- package/dist/tickets/output.d.ts +5 -0
- package/dist/tickets/output.js +73 -0
- package/dist/tickets/stateDb.d.ts +1 -0
- package/dist/tickets/stateDb.js +5 -0
- package/dist/workflowDb.d.ts +31 -2
- package/dist/workflowDb.js +192 -23
- package/dist/workflowReader.d.ts +42 -0
- package/dist/workflowReader.js +171 -0
- package/foreman.yaml +13 -0
- package/package.json +51 -3
package/README.md
CHANGED
|
@@ -148,6 +148,21 @@ With the saved `current` strategy, Foreman works in the active branch while the
|
|
|
148
148
|
|
|
149
149
|
The current terminal status includes the role/provider, activity, truthful context state, successful compaction count, and handoff generation. At the Builder threshold, Foreman settles the in-flight action, verifies native compaction plus a fresh provider usage sample, and resumes the frozen action. The configured maximum defaults to ten successful compactions per location-scoped provider session; the next crossing uses a validated fresh handoff instead of exceeding it. Disposable QA snapshots never reuse a prior QA conversation in a new `/tmp/rafi-qa-*` worktree; they transfer cumulative state through an accepted handoff.
|
|
150
150
|
|
|
151
|
+
Failed QA reviews use the versioned `RAFI_QA_FAILURE_REPORT_START` / `RAFI_QA_FAILURE_REPORT_END` JSON contract. Foreman validates the report before Builder remediation and preserves reports by digest. A malformed failing report receives two correction-only turns, a compacted pair, then five turns in a validated fresh successor. Recovery packets are owner-only, locally Git-excluded, and stored under `.foreman/qa-report-recovery/`; the successor must acknowledge the copied packet and reviewed-state digests before reporting.
|
|
152
|
+
|
|
153
|
+
### `ai-foreman manager`
|
|
154
|
+
|
|
155
|
+
Manager is a project-wide, read-only diagnostic conversation over every retained build run. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
|
|
156
|
+
|
|
157
|
+
Permanent sanitized run summaries keep timing, counts, provider/model configuration, usage coverage, failure category, and terminal Git facts useful after detailed spans expire. Detailed spans retain the configured 30-day policy. Missing historical facts are reported as partial rather than reconstructed or treated as zero.
|
|
158
|
+
|
|
159
|
+
When the initial bounded packet omits evidence needed for a question, Manager can request catalog rows, aggregates, comparisons, or details through a host-validated read-only lookup. Each question is limited to two rounds, six operations per round, five detailed runs per operation, and fifty catalog rows per list operation. Questions, answers, and lookup envelopes are not stored as project evidence.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
ai-foreman manager ./my-project
|
|
163
|
+
ai-foreman manager ./my-project --run <run-id> --ask "Compare this run with the last five successful runs"
|
|
164
|
+
```
|
|
165
|
+
|
|
151
166
|
### GitHub PR Failure Recovery
|
|
152
167
|
|
|
153
168
|
When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
|
package/dist/activity.d.ts
CHANGED
|
@@ -63,7 +63,11 @@ export declare class ActivityReporter {
|
|
|
63
63
|
export declare function currentActivity(): ActivityReporter | undefined;
|
|
64
64
|
export declare function withActivityContext<T>(command: string, operation: () => Promise<T>, options?: ActivityReporterOptions): Promise<T>;
|
|
65
65
|
export declare function withActivityPhase<T>(label: string, operation: () => Promise<T>): Promise<T>;
|
|
66
|
-
export
|
|
66
|
+
export interface ActivityInputPauseMetadata {
|
|
67
|
+
label?: string;
|
|
68
|
+
context?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
export declare function pauseActivityForInput<T>(operation: () => Promise<T>, _metadata?: ActivityInputPauseMetadata): Promise<T>;
|
|
67
71
|
export declare function reportBuilderEvent(event: BuilderEvent): void;
|
|
68
72
|
/** Async event queue that also feeds the active CLI reporter without consuming the stream. */
|
|
69
73
|
export declare class BuilderEventQueue extends AsyncQueue<BuilderEvent> {
|
package/dist/activity.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function withActivityPhase(label, operation) {
|
|
|
225
225
|
end?.();
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
export async function pauseActivityForInput(operation) {
|
|
228
|
+
export async function pauseActivityForInput(operation, _metadata = {}) {
|
|
229
229
|
const resume = currentActivity()?.pause();
|
|
230
230
|
try {
|
|
231
231
|
return await operation();
|
|
@@ -62,6 +62,10 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
62
62
|
private nativeCompactions;
|
|
63
63
|
private nativeCompactionSequence;
|
|
64
64
|
private cumulativeUsage;
|
|
65
|
+
private priorTurnUsage;
|
|
66
|
+
private activeProviderTurnId?;
|
|
67
|
+
private activeProviderTurnSpanId?;
|
|
68
|
+
private readonly toolSpans;
|
|
65
69
|
private pending?;
|
|
66
70
|
private terminalResult?;
|
|
67
71
|
private streamEnded;
|
|
@@ -74,11 +78,14 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
74
78
|
sendTurn(text: string): Promise<TurnResult>;
|
|
75
79
|
private sendTurnInternal;
|
|
76
80
|
private captureStderr;
|
|
81
|
+
private observationContext;
|
|
82
|
+
private observeToolResults;
|
|
77
83
|
sessionId(): string | undefined;
|
|
78
84
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
79
85
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
80
86
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
81
87
|
compact(): Promise<CompactResult>;
|
|
88
|
+
private compactInternal;
|
|
82
89
|
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
|
|
83
90
|
autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
|
|
84
91
|
drainNativeCompactions(): NativeCompaction[];
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1
2
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
2
3
|
export async function requireClaudeSDK() {
|
|
3
4
|
try {
|
|
@@ -159,6 +160,10 @@ export class ClaudeAdapter {
|
|
|
159
160
|
nativeCompactions = [];
|
|
160
161
|
nativeCompactionSequence = 0;
|
|
161
162
|
cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
|
|
163
|
+
priorTurnUsage = {};
|
|
164
|
+
activeProviderTurnId;
|
|
165
|
+
activeProviderTurnSpanId;
|
|
166
|
+
toolSpans = new Map();
|
|
162
167
|
pending;
|
|
163
168
|
terminalResult;
|
|
164
169
|
streamEnded = false;
|
|
@@ -275,14 +280,28 @@ export class ClaudeAdapter {
|
|
|
275
280
|
}
|
|
276
281
|
for (const block of msg.message.content) {
|
|
277
282
|
if (block.type === "text" && block.text) {
|
|
278
|
-
this.eventQueue.push({ kind: "text", text: block.text });
|
|
283
|
+
this.eventQueue.push({ kind: "text", text: block.text, byteCount: Buffer.byteLength(block.text), digest: createHash("sha256").update(block.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
284
|
+
this.opts.observer?.signal(true);
|
|
279
285
|
}
|
|
280
286
|
else if (block.type === "tool_use") {
|
|
287
|
+
const callId = block.id;
|
|
281
288
|
this.eventQueue.push({
|
|
282
289
|
kind: "tool",
|
|
283
290
|
name: block.name,
|
|
284
291
|
input: block.input,
|
|
292
|
+
inputCompleteness: "complete",
|
|
293
|
+
lifecycle: "started",
|
|
294
|
+
callId,
|
|
295
|
+
completionKnown: false,
|
|
296
|
+
providerTurnId: this.activeProviderTurnId,
|
|
297
|
+
eventId: randomUUID(),
|
|
298
|
+
observedAt: new Date().toISOString(),
|
|
285
299
|
});
|
|
300
|
+
if (this.opts.observer) {
|
|
301
|
+
const spanId = this.opts.observer.store.startSpan(this.observationContext(), { kind: "tool", name: block.name, providerTurnId: this.activeProviderTurnId, attributes: { callId, input: block.input } });
|
|
302
|
+
this.toolSpans.set(callId, spanId);
|
|
303
|
+
this.opts.observer.signal(true);
|
|
304
|
+
}
|
|
286
305
|
}
|
|
287
306
|
}
|
|
288
307
|
}
|
|
@@ -309,10 +328,24 @@ export class ClaudeAdapter {
|
|
|
309
328
|
this.structuredError = msg.error;
|
|
310
329
|
this.apiErrorStatus = msg.error_status;
|
|
311
330
|
this.turnSignals.push(`API retry ${msg.attempt}/${msg.max_retries}: ${msg.error}${msg.error_status === null ? "" : ` (HTTP ${msg.error_status})`}`);
|
|
312
|
-
this.
|
|
331
|
+
const retryEvent = { ...claudeApiRetryEvent(msg), retryId: randomUUID(), providerTurnId: this.activeProviderTurnId, observedAt: new Date().toISOString() };
|
|
332
|
+
this.eventQueue.push(retryEvent);
|
|
333
|
+
if (this.opts.observer) {
|
|
334
|
+
const attributes = { provider: "claude", attempt: msg.attempt, maximum: msg.max_retries, reportedDelayMs: msg.retry_delay_ms, reasonDigest: createHash("sha256").update(msg.error).digest("hex") };
|
|
335
|
+
const retrySpanId = this.opts.observer.store.startSpan(this.observationContext(), { spanId: retryEvent.retryId, kind: "retry", name: "Claude provider retry", providerTurnId: this.activeProviderTurnId, attributes });
|
|
336
|
+
this.opts.observer.store.recordEvent(this.observationContext(), "retry", { eventId: retryEvent.retryId, spanId: retrySpanId, severity: "warning", attributes });
|
|
337
|
+
this.opts.observer.store.finishSpan(retrySpanId, { outcome: "reported" });
|
|
338
|
+
}
|
|
313
339
|
}
|
|
314
340
|
else if (msg.type === "tool_progress") {
|
|
315
341
|
this.eventQueue.push({ kind: "activity", provider: "claude", state: "running tool", detail: `${msg.tool_name} (${Math.floor(msg.elapsed_time_seconds)}s)`, transient: true });
|
|
342
|
+
const progress = msg;
|
|
343
|
+
const callId = typeof progress.tool_use_id === "string" ? progress.tool_use_id : typeof progress.toolUseID === "string" ? progress.toolUseID : undefined;
|
|
344
|
+
this.eventQueue.push({ kind: "tool", name: msg.tool_name, input: {}, lifecycle: "progress", callId, durationMs: Math.max(0, msg.elapsed_time_seconds * 1000), completionKnown: false, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
345
|
+
this.opts.observer?.signal(false);
|
|
346
|
+
}
|
|
347
|
+
else if (msg.type === "user") {
|
|
348
|
+
this.observeToolResults(msg);
|
|
316
349
|
}
|
|
317
350
|
else if (msg.type === "system") {
|
|
318
351
|
const system = msg;
|
|
@@ -364,13 +397,20 @@ export class ClaudeAdapter {
|
|
|
364
397
|
costUsd: msg.total_cost_usd,
|
|
365
398
|
costAuthoritative: Number.isFinite(msg.total_cost_usd),
|
|
366
399
|
failure,
|
|
400
|
+
turnId: this.pending?.turnId ?? this.activeProviderTurnId,
|
|
401
|
+
hostInstruction: this.pending?.instruction,
|
|
402
|
+
providerInstruction: this.pending?.instruction,
|
|
403
|
+
rawResponse: text,
|
|
404
|
+
cleanedResponse: failure ? formatClaudeFailure(failure) : text,
|
|
405
|
+
providerMetadata: { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef },
|
|
367
406
|
};
|
|
368
407
|
const rawResult = msg;
|
|
369
408
|
const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
|
|
370
409
|
result.inputTokens = mergedUsage.inputTokens;
|
|
371
410
|
result.outputTokens = mergedUsage.outputTokens;
|
|
411
|
+
result.usage = { scope: "session-cumulative", inputTokens: mergedUsage.sample.inputTokens, outputTokens: mergedUsage.sample.outputTokens, totalTokens: mergedUsage.sample.totalTokens, costUsd: mergedUsage.sample.authoritativeCostUsd };
|
|
372
412
|
this.cumulativeUsage = mergedUsage.sample;
|
|
373
|
-
this.eventQueue.push({ kind: "turn-complete", result });
|
|
413
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
374
414
|
this.settlePending(result, false);
|
|
375
415
|
this.turnSignals = [];
|
|
376
416
|
this.structuredError = undefined;
|
|
@@ -378,8 +418,43 @@ export class ClaudeAdapter {
|
|
|
378
418
|
this.stderrChunks.length = 0;
|
|
379
419
|
}
|
|
380
420
|
}
|
|
381
|
-
sendTurn(text) {
|
|
382
|
-
|
|
421
|
+
async sendTurn(text) {
|
|
422
|
+
const turnId = randomUUID();
|
|
423
|
+
this.activeProviderTurnId = turnId;
|
|
424
|
+
const observer = this.opts.observer;
|
|
425
|
+
if (!observer) {
|
|
426
|
+
try {
|
|
427
|
+
return await withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
this.activeProviderTurnId = undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const context = this.observationContext();
|
|
434
|
+
const spanId = observer.store.startSpan(context, { spanId: turnId, kind: "provider_turn", name: `Claude ${activityPhase(this.opts.runtimePhase)}`, providerTurnId: turnId, attributes: { provider: "claude" } });
|
|
435
|
+
this.activeProviderTurnSpanId = spanId;
|
|
436
|
+
observer.store.updateCurrentState({ runId: observer.runId, role: context.role ?? "host", stream: context.stream ?? "claude", executionId: observer.executionId, ticketId: context.ticketId, deliveryUnitId: context.deliveryUnitId, providerSessionId: context.providerSessionId, phase: "provider turn", activeSpanId: spanId, activeSpanKind: "provider_turn", lastSemanticProgressAt: new Date().toISOString() });
|
|
437
|
+
try {
|
|
438
|
+
const result = await withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
|
|
439
|
+
const deltaInput = nonNegativeDelta(result.usage?.inputTokens, this.priorTurnUsage.inputTokens);
|
|
440
|
+
const deltaOutput = nonNegativeDelta(result.usage?.outputTokens, this.priorTurnUsage.outputTokens);
|
|
441
|
+
const deltaCost = nonNegativeDelta(result.usage?.costUsd, this.priorTurnUsage.costUsd);
|
|
442
|
+
this.priorTurnUsage = { inputTokens: result.usage?.inputTokens, outputTokens: result.usage?.outputTokens, costUsd: result.usage?.costUsd };
|
|
443
|
+
observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { cumulativeUsage: result.usage, turnDelta: { inputTokens: deltaInput, outputTokens: deltaOutput, costUsd: deltaCost } } });
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
for (const [callId, toolSpanId] of this.toolSpans) {
|
|
452
|
+
observer.store.finishSpan(toolSpanId, { outcome: "unknown", completionKnown: false, attributes: { callId } });
|
|
453
|
+
this.toolSpans.delete(callId);
|
|
454
|
+
}
|
|
455
|
+
this.activeProviderTurnId = undefined;
|
|
456
|
+
this.activeProviderTurnSpanId = undefined;
|
|
457
|
+
}
|
|
383
458
|
}
|
|
384
459
|
sendTurnInternal(text) {
|
|
385
460
|
if (this.closed)
|
|
@@ -394,7 +469,7 @@ export class ClaudeAdapter {
|
|
|
394
469
|
this.turnSignals = [];
|
|
395
470
|
this.structuredError = undefined;
|
|
396
471
|
this.apiErrorStatus = undefined;
|
|
397
|
-
this.pending = { resolve, reject };
|
|
472
|
+
this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
|
|
398
473
|
this.inbox.push({
|
|
399
474
|
type: "user",
|
|
400
475
|
message: { role: "user", content: text },
|
|
@@ -407,6 +482,37 @@ export class ClaudeAdapter {
|
|
|
407
482
|
while (this.stderrChunks.join("").length > 8 * 1024)
|
|
408
483
|
this.stderrChunks.shift();
|
|
409
484
|
}
|
|
485
|
+
observationContext() {
|
|
486
|
+
const observer = this.opts.observer;
|
|
487
|
+
const inherited = observer.context();
|
|
488
|
+
return { ...inherited, runId: observer.runId, executionId: observer.executionId, role: this.opts.sessionRole ?? inherited.role ?? "builder",
|
|
489
|
+
stream: this.opts.sessionStream ?? inherited.stream ?? this.opts.sessionRole ?? "builder", providerSessionId: this._sessionId,
|
|
490
|
+
ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
|
|
491
|
+
parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
|
|
492
|
+
}
|
|
493
|
+
observeToolResults(message) {
|
|
494
|
+
const body = message.message;
|
|
495
|
+
if (!Array.isArray(body?.content))
|
|
496
|
+
return;
|
|
497
|
+
for (const block of body.content) {
|
|
498
|
+
if (block.type !== "tool_result")
|
|
499
|
+
continue;
|
|
500
|
+
const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
|
|
501
|
+
if (!callId)
|
|
502
|
+
continue;
|
|
503
|
+
const spanId = this.toolSpans.get(callId);
|
|
504
|
+
const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
505
|
+
const sanitized = text.replace(/\b(?:token|password|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[REDACTED]");
|
|
506
|
+
const failed = block.is_error === true;
|
|
507
|
+
const outputDigest = createHash("sha256").update(text).digest("hex");
|
|
508
|
+
this.eventQueue.push({ kind: "tool", name: "tool_result", input: {}, inputCompleteness: "unavailable", lifecycle: "completed", callId, status: failed ? "failed" : "completed", outputSummary: sanitized.slice(0, 1000), outputDigest, outputCompleteness: block.is_truncated === true ? "truncated" : "complete", rawOutputBytes: Buffer.byteLength(text), completionKnown: true, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
509
|
+
if (spanId && this.opts.observer) {
|
|
510
|
+
this.opts.observer.store.finishSpan(spanId, { outcome: failed ? "failed" : "completed", completionKnown: true, attributes: { callId, outputSummary: sanitized.slice(0, 1000), outputDigest } });
|
|
511
|
+
this.toolSpans.delete(callId);
|
|
512
|
+
}
|
|
513
|
+
this.opts.observer?.signal(true);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
410
516
|
sessionId() {
|
|
411
517
|
return this._sessionId;
|
|
412
518
|
}
|
|
@@ -430,6 +536,11 @@ export class ClaudeAdapter {
|
|
|
430
536
|
});
|
|
431
537
|
}
|
|
432
538
|
async compact() {
|
|
539
|
+
return this.opts.observer
|
|
540
|
+
? this.opts.observer.span("compaction", "Claude context compaction", () => this.compactInternal())
|
|
541
|
+
: this.compactInternal();
|
|
542
|
+
}
|
|
543
|
+
async compactInternal() {
|
|
433
544
|
this.compactResult = undefined;
|
|
434
545
|
this.manualCompactionInFlight = true;
|
|
435
546
|
let result;
|
|
@@ -553,8 +664,16 @@ export class ClaudeAdapter {
|
|
|
553
664
|
if (!pending)
|
|
554
665
|
return;
|
|
555
666
|
this.pending = undefined;
|
|
667
|
+
if (!result.turnId) {
|
|
668
|
+
result.turnId = pending.turnId;
|
|
669
|
+
result.hostInstruction = pending.instruction;
|
|
670
|
+
result.providerInstruction = pending.instruction;
|
|
671
|
+
result.rawResponse = result.text;
|
|
672
|
+
result.cleanedResponse = result.text;
|
|
673
|
+
result.providerMetadata = { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef };
|
|
674
|
+
}
|
|
556
675
|
if (emit)
|
|
557
|
-
this.eventQueue.push({ kind: "turn-complete", result });
|
|
676
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
558
677
|
pending.resolve(result);
|
|
559
678
|
}
|
|
560
679
|
streamFailureResult(message, sessionUnavailable) {
|
|
@@ -575,6 +694,11 @@ export class ClaudeAdapter {
|
|
|
575
694
|
}
|
|
576
695
|
}
|
|
577
696
|
function finiteNumber(value) { const parsed = Number(value); return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; }
|
|
697
|
+
function nonNegativeDelta(current, prior) {
|
|
698
|
+
if (current === undefined)
|
|
699
|
+
return undefined;
|
|
700
|
+
return prior === undefined ? current : Math.max(0, current - prior);
|
|
701
|
+
}
|
|
578
702
|
function validThreshold(value) {
|
|
579
703
|
if (!Number.isInteger(value) || value < 1 || value > 99)
|
|
580
704
|
throw new Error("automatic compaction threshold must be an integer from 1 to 99");
|
|
@@ -595,6 +719,8 @@ function activityPhase(phase) {
|
|
|
595
719
|
return "reviewing with QA";
|
|
596
720
|
if (phase === "uninstaller")
|
|
597
721
|
return "planning uninstall";
|
|
722
|
+
if (phase === "manager")
|
|
723
|
+
return "analyzing diagnostics";
|
|
598
724
|
return "building";
|
|
599
725
|
}
|
|
600
726
|
function formatClaudeFailure(failure) {
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -35,6 +35,9 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
35
35
|
private nativeCompactionSequence;
|
|
36
36
|
private manualCompactionInFlight;
|
|
37
37
|
private observedToolCalls;
|
|
38
|
+
private activeProviderTurnId?;
|
|
39
|
+
private activeProviderTurnSpanId?;
|
|
40
|
+
private readonly toolSpans;
|
|
38
41
|
constructor(opts: BuilderAdapterOptions);
|
|
39
42
|
buildInstruction(instruction: string): string;
|
|
40
43
|
buildAppServerArgs(): string[];
|
|
@@ -43,6 +46,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
43
46
|
sendTurn(instruction: string): Promise<TurnResult>;
|
|
44
47
|
private sendTurnInternal;
|
|
45
48
|
compact(): Promise<CompactResult>;
|
|
49
|
+
private compactInternal;
|
|
46
50
|
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
|
|
47
51
|
autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
|
|
48
52
|
drainNativeCompactions(): NativeCompaction[];
|
|
@@ -63,6 +67,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
63
67
|
private request;
|
|
64
68
|
private write;
|
|
65
69
|
private handle;
|
|
70
|
+
private observationContext;
|
|
66
71
|
private waitFor;
|
|
67
72
|
private disconnect;
|
|
68
73
|
private restartForAutoCompaction;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
@@ -51,6 +52,9 @@ export class CodexAdapter {
|
|
|
51
52
|
nativeCompactionSequence = 0;
|
|
52
53
|
manualCompactionInFlight = false;
|
|
53
54
|
observedToolCalls = 0;
|
|
55
|
+
activeProviderTurnId;
|
|
56
|
+
activeProviderTurnSpanId;
|
|
57
|
+
toolSpans = new Map();
|
|
54
58
|
constructor(opts) {
|
|
55
59
|
this.opts = opts;
|
|
56
60
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
@@ -82,13 +86,45 @@ export class CodexAdapter {
|
|
|
82
86
|
return args;
|
|
83
87
|
}
|
|
84
88
|
async sendTurn(instruction) {
|
|
85
|
-
|
|
89
|
+
const turnId = randomUUID();
|
|
90
|
+
this.activeProviderTurnId = turnId;
|
|
91
|
+
const observer = this.opts.observer;
|
|
92
|
+
if (!observer) {
|
|
93
|
+
try {
|
|
94
|
+
return await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.activeProviderTurnId = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const context = this.observationContext();
|
|
101
|
+
const spanId = observer.store.startSpan(context, { spanId: turnId, kind: "provider_turn", name: `Codex ${activityPhase(this.opts.runtimePhase)}`, providerTurnId: turnId, attributes: { provider: "codex" } });
|
|
102
|
+
this.activeProviderTurnSpanId = spanId;
|
|
103
|
+
observer.store.updateCurrentState({ runId: observer.runId, role: context.role ?? "host", stream: context.stream ?? "codex", executionId: observer.executionId, ticketId: context.ticketId, deliveryUnitId: context.deliveryUnitId, providerSessionId: context.providerSessionId, phase: "provider turn", activeSpanId: spanId, activeSpanKind: "provider_turn", lastSemanticProgressAt: new Date().toISOString() });
|
|
104
|
+
try {
|
|
105
|
+
const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
|
|
106
|
+
observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { usage: result.usage } });
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
for (const [callId, toolSpanId] of this.toolSpans) {
|
|
115
|
+
observer.store.finishSpan(toolSpanId, { outcome: "unknown", completionKnown: false });
|
|
116
|
+
this.toolSpans.delete(callId);
|
|
117
|
+
}
|
|
118
|
+
this.activeProviderTurnId = undefined;
|
|
119
|
+
this.activeProviderTurnSpanId = undefined;
|
|
120
|
+
}
|
|
86
121
|
}
|
|
87
122
|
async sendTurnInternal(instruction) {
|
|
88
123
|
if (this.closed)
|
|
89
124
|
throw new Error("builder is closed");
|
|
90
125
|
this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
|
|
91
126
|
let turnStartDispatched = false;
|
|
127
|
+
const providerInstruction = this.buildInstruction(instruction);
|
|
92
128
|
try {
|
|
93
129
|
await this.ensureThread();
|
|
94
130
|
this.activeText = [];
|
|
@@ -97,7 +133,7 @@ export class CodexAdapter {
|
|
|
97
133
|
turnStartDispatched = true;
|
|
98
134
|
await this.request("turn/start", {
|
|
99
135
|
threadId: this._sessionId,
|
|
100
|
-
input: [{ type: "text", text:
|
|
136
|
+
input: [{ type: "text", text: providerInstruction, text_elements: [] }],
|
|
101
137
|
cwd: this.opts.cwd,
|
|
102
138
|
model: this.opts.model ?? null,
|
|
103
139
|
effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
|
|
@@ -110,16 +146,27 @@ export class CodexAdapter {
|
|
|
110
146
|
const result = {
|
|
111
147
|
text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
112
148
|
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
149
|
+
turnId: this.activeProviderTurnId, hostInstruction: instruction, providerInstruction,
|
|
150
|
+
rawResponse: text, cleanedResponse: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
151
|
+
providerMetadata: { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef },
|
|
113
152
|
...(this.lastTurnTokens ?? {}),
|
|
114
153
|
};
|
|
115
|
-
|
|
154
|
+
result.usage = { scope: "turn-delta", inputTokens: result.inputTokens, outputTokens: result.outputTokens,
|
|
155
|
+
...(result.inputTokens !== undefined || result.outputTokens !== undefined ? { totalTokens: (result.inputTokens ?? 0) + (result.outputTokens ?? 0) } : {}) };
|
|
156
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
116
157
|
return result;
|
|
117
158
|
}
|
|
118
159
|
catch (error) {
|
|
119
160
|
if (error instanceof SessionUnavailableError) {
|
|
120
161
|
const result = sessionUnavailableResult(error);
|
|
121
162
|
this.eventQueue.push({ kind: "error", message: result.text });
|
|
122
|
-
|
|
163
|
+
result.turnId = this.activeProviderTurnId;
|
|
164
|
+
result.hostInstruction = instruction;
|
|
165
|
+
result.providerInstruction = providerInstruction;
|
|
166
|
+
result.rawResponse = result.text;
|
|
167
|
+
result.cleanedResponse = result.text;
|
|
168
|
+
result.providerMetadata = { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef };
|
|
169
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
123
170
|
return result;
|
|
124
171
|
}
|
|
125
172
|
const detail = [error instanceof Error ? error.message : String(error), this.stderr].filter(Boolean).join("\n");
|
|
@@ -134,11 +181,22 @@ export class CodexAdapter {
|
|
|
134
181
|
}))
|
|
135
182
|
: { text, isError: true, numTurns: 1, costUsd: 0, costAuthoritative: false };
|
|
136
183
|
this.eventQueue.push({ kind: "error", message: text });
|
|
137
|
-
|
|
184
|
+
result.turnId = this.activeProviderTurnId;
|
|
185
|
+
result.hostInstruction = instruction;
|
|
186
|
+
result.providerInstruction = providerInstruction;
|
|
187
|
+
result.rawResponse = result.text;
|
|
188
|
+
result.cleanedResponse = result.text;
|
|
189
|
+
result.providerMetadata = { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef };
|
|
190
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
138
191
|
return result;
|
|
139
192
|
}
|
|
140
193
|
}
|
|
141
194
|
async compact() {
|
|
195
|
+
return this.opts.observer
|
|
196
|
+
? this.opts.observer.span("compaction", "Codex context compaction", () => this.compactInternal())
|
|
197
|
+
: this.compactInternal();
|
|
198
|
+
}
|
|
199
|
+
async compactInternal() {
|
|
142
200
|
try {
|
|
143
201
|
await this.ensureThread();
|
|
144
202
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
@@ -418,20 +476,43 @@ export class CodexAdapter {
|
|
|
418
476
|
const params = message.params ?? {};
|
|
419
477
|
if (message.method === "item/started") {
|
|
420
478
|
const item = params.item;
|
|
421
|
-
if (isCodexToolItem(item))
|
|
479
|
+
if (isCodexToolItem(item)) {
|
|
422
480
|
this.observedToolCalls += 1;
|
|
481
|
+
const callId = codexItemId(item) ?? randomUUID();
|
|
482
|
+
const name = codexToolName(item);
|
|
483
|
+
this.eventQueue.push({ kind: "tool", name, input: exactToolInput(item), inputCompleteness: "complete", lifecycle: "started", callId, completionKnown: false, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
484
|
+
if (this.opts.observer) {
|
|
485
|
+
const spanId = this.opts.observer.store.startSpan(this.observationContext(), { kind: "tool", name, providerTurnId: this.activeProviderTurnId, attributes: { callId, input: boundedToolInput(item) } });
|
|
486
|
+
this.toolSpans.set(callId, spanId);
|
|
487
|
+
this.opts.observer.signal(true);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
423
490
|
const activity = codexItemActivity(item);
|
|
424
491
|
if (activity)
|
|
425
492
|
this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
|
|
426
493
|
}
|
|
427
494
|
else if (message.method === "item/completed") {
|
|
428
495
|
const item = params.item;
|
|
496
|
+
if (isCodexToolItem(item)) {
|
|
497
|
+
const callId = codexItemId(item);
|
|
498
|
+
const spanId = callId ? this.toolSpans.get(callId) : undefined;
|
|
499
|
+
const status = typeof item?.status === "string" ? item.status : undefined;
|
|
500
|
+
const exitCode = finiteNumber(item?.exitCode ?? item?.exit_code);
|
|
501
|
+
const durationMs = finiteNumber(item?.durationMs ?? item?.duration_ms);
|
|
502
|
+
const output = codexOutputSummary(item);
|
|
503
|
+
this.eventQueue.push({ kind: "tool", name: codexToolName(item), input: exactToolInput(item), inputCompleteness: "complete", lifecycle: "completed", callId, status, durationMs, exitCode, outputSummary: output?.summary, outputDigest: output?.digest, outputCompleteness: output?.completeness ?? "unavailable", rawOutputBytes: output?.bytes, completionKnown: true, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
504
|
+
if (spanId && this.opts.observer) {
|
|
505
|
+
this.opts.observer.store.finishSpan(spanId, { outcome: status ?? (exitCode === undefined || exitCode === 0 ? "completed" : "failed"), completionKnown: true, attributes: { callId, providerDurationMs: durationMs, exitCode, outputSummary: output?.summary, outputDigest: output?.digest } });
|
|
506
|
+
this.toolSpans.delete(callId);
|
|
507
|
+
}
|
|
508
|
+
this.opts.observer?.signal(true);
|
|
509
|
+
}
|
|
429
510
|
if (item?.type === "agentMessage" && typeof item.text === "string") {
|
|
430
511
|
this.activeText.push(item.text);
|
|
431
|
-
this.eventQueue.push({ kind: "text", text: item.text });
|
|
512
|
+
this.eventQueue.push({ kind: "text", text: item.text, byteCount: Buffer.byteLength(item.text), digest: createHash("sha256").update(item.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
513
|
+
this.opts.observer?.signal(true);
|
|
432
514
|
}
|
|
433
|
-
else if (item?.type === "commandExecution") {
|
|
434
|
-
this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
|
|
515
|
+
else if (item?.type === "commandExecution") { /* tool completion was emitted above */
|
|
435
516
|
}
|
|
436
517
|
else if (item?.type === "contextCompaction" && !this.manualCompactionInFlight) {
|
|
437
518
|
// Completion is the provider's authoritative confirmation. Token-usage
|
|
@@ -485,8 +566,18 @@ export class CodexAdapter {
|
|
|
485
566
|
else if (message.method === "error") {
|
|
486
567
|
const error = params.error;
|
|
487
568
|
const reason = String(error?.message ?? params.message ?? "Codex app-server error");
|
|
488
|
-
if (params.willRetry === true)
|
|
489
|
-
|
|
569
|
+
if (params.willRetry === true) {
|
|
570
|
+
const retryId = randomUUID();
|
|
571
|
+
this.eventQueue.push(this.opts.observer
|
|
572
|
+
? { kind: "retry", provider: "codex", reason, managedBy: "provider", retryId, providerTurnId: this.activeProviderTurnId, eventId: retryId, observedAt: new Date().toISOString() }
|
|
573
|
+
: { kind: "retry", provider: "codex", reason, managedBy: "provider" });
|
|
574
|
+
if (this.opts.observer) {
|
|
575
|
+
const retrySpanId = this.opts.observer.store.startSpan(this.observationContext(), { spanId: retryId, kind: "retry", name: "Codex provider retry", providerTurnId: this.activeProviderTurnId,
|
|
576
|
+
attributes: { provider: "codex", reasonDigest: createHash("sha256").update(reason).digest("hex") } });
|
|
577
|
+
this.opts.observer.store.recordEvent(this.observationContext(), "retry", { eventId: retryId, spanId: retrySpanId, severity: "warning", attributes: { provider: "codex", reasonDigest: createHash("sha256").update(reason).digest("hex") } });
|
|
578
|
+
this.opts.observer.store.finishSpan(retrySpanId, { outcome: "reported" });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
490
581
|
else
|
|
491
582
|
this.eventQueue.push({ kind: "error", message: reason });
|
|
492
583
|
}
|
|
@@ -496,6 +587,14 @@ export class CodexAdapter {
|
|
|
496
587
|
waiter.predicate(params) ? waiter.resolve(params) : remaining.push(waiter);
|
|
497
588
|
this.notificationWaiters.set(message.method, remaining);
|
|
498
589
|
}
|
|
590
|
+
observationContext() {
|
|
591
|
+
const observer = this.opts.observer;
|
|
592
|
+
const inherited = observer.context();
|
|
593
|
+
return { ...inherited, runId: observer.runId, executionId: observer.executionId, role: this.opts.sessionRole ?? inherited.role ?? "builder",
|
|
594
|
+
stream: this.opts.sessionStream ?? inherited.stream ?? this.opts.sessionRole ?? "builder", providerSessionId: this._sessionId,
|
|
595
|
+
ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
|
|
596
|
+
parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
|
|
597
|
+
}
|
|
499
598
|
waitFor(method, predicate, timeoutMs) {
|
|
500
599
|
return new Promise((resolve, reject) => {
|
|
501
600
|
let timeout;
|
|
@@ -571,6 +670,8 @@ function activityPhase(phase) {
|
|
|
571
670
|
return "reviewing with QA";
|
|
572
671
|
if (phase === "uninstaller")
|
|
573
672
|
return "planning uninstall";
|
|
673
|
+
if (phase === "manager")
|
|
674
|
+
return "analyzing diagnostics";
|
|
574
675
|
return "building";
|
|
575
676
|
}
|
|
576
677
|
function codexItemActivity(item, completed = false) {
|
|
@@ -596,6 +697,15 @@ function codexItemActivity(item, completed = false) {
|
|
|
596
697
|
function isCodexToolItem(item) {
|
|
597
698
|
return item?.type === "commandExecution" || item?.type === "mcpToolCall" || item?.type === "dynamicToolCall" || item?.type === "webSearch" || item?.type === "fileChange";
|
|
598
699
|
}
|
|
700
|
+
function codexItemId(item) { const value = item?.id ?? item?.itemId ?? item?.callId; return typeof value === "string" && value ? value : undefined; }
|
|
701
|
+
function codexToolName(item) { return String(item?.tool ?? item?.name ?? item?.type ?? "tool"); }
|
|
702
|
+
function exactToolInput(item) { if (!item)
|
|
703
|
+
return {}; return Object.fromEntries(Object.entries(item).filter(([key]) => ["command", "tool", "name", "type", "path", "query", "arguments", "input"].includes(key))); }
|
|
704
|
+
function boundedToolInput(item) { if (!item)
|
|
705
|
+
return {}; return Object.fromEntries(Object.entries(exactToolInput(item)).map(([key, value]) => [key, typeof value === "string" ? value.slice(0, 1000) : value])); }
|
|
706
|
+
function codexOutputSummary(item) { const value = item?.aggregatedOutput ?? item?.output ?? item?.stdout; if (typeof value !== "string" || !value)
|
|
707
|
+
return undefined; const sanitized = value.replace(/\b(?:token|password|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[REDACTED]"); const providerTruncated = item?.outputTruncated === true || item?.truncated === true; return { summary: sanitized.slice(0, 1000), digest: createHash("sha256").update(value).digest("hex"), completeness: providerTruncated ? "truncated" : "complete", bytes: Buffer.byteLength(value) }; }
|
|
708
|
+
function finiteNumber(value) { const result = Number(value); return Number.isFinite(result) ? result : undefined; }
|
|
599
709
|
function loadSkillMarkdown(cwd, skill) {
|
|
600
710
|
const projectPath = join(cwd, ".agents", "skills", skill, "SKILL.md");
|
|
601
711
|
if (existsSync(projectPath))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentRuntime } from "../runtimeAuth.js";
|
|
2
2
|
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
3
3
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
4
|
+
import type { RunObserver } from "../observability.js";
|
|
4
5
|
export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
|
|
5
6
|
export interface RecoveringAdapterOptions {
|
|
6
7
|
initial: BuilderAdapter;
|
|
@@ -12,6 +13,7 @@ export interface RecoveringAdapterOptions {
|
|
|
12
13
|
choose?: (result: TurnResult, runtime: AgentRuntime, otherRuntime: AgentRuntime) => Promise<TurnRecoveryChoice>;
|
|
13
14
|
onRuntimeChange?: (runtime: AgentRuntime) => void;
|
|
14
15
|
onSessionRef?: (ref: ProviderSessionRefV1) => void;
|
|
16
|
+
observer?: RunObserver;
|
|
15
17
|
}
|
|
16
18
|
/** Adds interactive recovery around structured runtime turn failures. */
|
|
17
19
|
export declare class RecoveringAdapter implements BuilderAdapter {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isCancel, log, select } from "@clack/prompts";
|
|
2
2
|
import { AsyncQueue } from "../util/asyncQueue.js";
|
|
3
3
|
import { pauseActivityForInput, reportBuilderEvent } from "../activity.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
/** Adds interactive recovery around structured runtime turn failures. */
|
|
5
6
|
export class RecoveringAdapter {
|
|
6
7
|
opts;
|
|
@@ -39,7 +40,9 @@ export class RecoveringAdapter {
|
|
|
39
40
|
const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
|
|
40
41
|
const choice = this.opts.choose
|
|
41
42
|
? await this.opts.choose(result, this.runtime, otherRuntime)
|
|
42
|
-
: await
|
|
43
|
+
: await (this.opts.observer
|
|
44
|
+
? this.opts.observer.span("user_wait", "runtime recovery choice", () => pauseActivityForInput(() => promptTurnRecovery(result, this.opts.label, this.runtime, otherRuntime, this.opts.allowSwitch)))
|
|
45
|
+
: pauseActivityForInput(() => promptTurnRecovery(result, this.opts.label, this.runtime, otherRuntime, this.opts.allowSwitch)));
|
|
43
46
|
if (choice === "cancel")
|
|
44
47
|
return result;
|
|
45
48
|
if (choice === "switch" && !this.opts.allowSwitch)
|
|
@@ -49,6 +52,13 @@ export class RecoveringAdapter {
|
|
|
49
52
|
: { kind: "activity", provider: otherRuntime, state: `switching to ${otherRuntime}`, detail: "starting a fresh provider session" };
|
|
50
53
|
this.eventQueue.push(recoveryEvent);
|
|
51
54
|
reportBuilderEvent(recoveryEvent);
|
|
55
|
+
if (choice === "retry" && this.opts.observer) {
|
|
56
|
+
const retryId = randomUUID();
|
|
57
|
+
const context = this.opts.observer.context();
|
|
58
|
+
const spanId = this.opts.observer.store.startSpan(context, { spanId: retryId, kind: "retry", name: "Rafi recovery retry", attributes: { provider: this.runtime, managedBy: "rafi" } });
|
|
59
|
+
this.opts.observer.store.recordEvent(context, "retry", { eventId: retryId, spanId, severity: "warning", attributes: { provider: this.runtime, managedBy: "rafi" } });
|
|
60
|
+
this.opts.observer.store.finishSpan(spanId, { outcome: "scheduled" });
|
|
61
|
+
}
|
|
52
62
|
const nextRuntime = choice === "switch" ? otherRuntime : this.runtime;
|
|
53
63
|
const resumeSessionId = choice === "retry" ? this.adapter.sessionId() : undefined;
|
|
54
64
|
const resumeSessionRef = choice === "retry" ? this.adapter.sessionRef?.() : undefined;
|