feat: write transcript for CLI provider runs [AI-assisted]

CLI backends (claude-cli, codex-cli, etc.) don't normally save session
transcripts because they run as external processes. This causes issues
with subagent announce flow - when a CLI-backed subagent completes, the
main agent can't read the reply because no transcript exists.

This change writes a JSONL transcript file after CLI provider runs
complete, using the same format as embedded Pi sessions:
- Session header with version, id, timestamp, cwd
- Message entry with assistant role, content, timestamp, usage

The transcript path is resolved from the session key/id and agent id,
matching the embedded runner's behavior.

Transcript writes are best-effort and won't fail the run.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ryan McMillan 2026-01-28 13:56:17 -06:00
parent 109ac1c549
commit b61aa94c28

View File

@ -1,5 +1,6 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
import { runCliAgent } from "../../agents/cli-runner.js";
import { getCliSessionId } from "../../agents/cli-session.js";
@ -192,6 +193,50 @@ export async function runAgentTurnWithFallback(params: {
data: { text: cliText },
});
}
// Write CLI result to transcript for subagent announce flow
if (cliText && params.sessionKey && params.followupRun.run.sessionId) {
try {
const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
const transcriptPath = resolveSessionTranscriptPath(
params.followupRun.run.sessionId,
agentId,
);
// Ensure directory exists
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
// Create session header if file doesn't exist
if (!fs.existsSync(transcriptPath)) {
const header = {
type: "session",
version: 1,
id: params.followupRun.run.sessionId,
timestamp: new Date().toISOString(),
cwd: process.cwd(),
};
fs.writeFileSync(transcriptPath, JSON.stringify(header) + "\n", "utf-8");
}
// Append assistant message
const messageEntry = {
type: "message",
id: crypto.randomUUID().slice(0, 8),
timestamp: new Date().toISOString(),
message: {
role: "assistant",
content: [{ type: "text", text: cliText }],
timestamp: Date.now(),
stopReason: "cli",
usage: result.meta?.agentMeta?.usage ?? {
input: 0,
output: 0,
totalTokens: 0,
},
},
};
fs.appendFileSync(transcriptPath, JSON.stringify(messageEntry) + "\n", "utf-8");
} catch (err) {
// Best effort - don't fail the run if transcript write fails
logVerbose(`CLI transcript write failed: ${String(err)}`);
}
}
emitAgentEvent({
runId,
stream: "lifecycle",