refactor: align package folder names with npm package names
CI / check (pull_request) Failing after 8m30s
CI / check (pull_request) Failing after 8m30s
Rename packages/ subdirectories to match their @united-workforce/* scope: cli-workflow → cli workflow-agent-builtin → agent-builtin workflow-agent-claude-code → agent-claude-code workflow-agent-hermes → agent-hermes workflow-dashboard → dashboard workflow-protocol → protocol workflow-util-agent → util-agent workflow-util → util Updated all tsconfig references, scripts, and active docs. Historical docs (docs/plans/, docs/superpowers/) left as-is. Closes #21
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import type { Store } from "@ocas/core";
|
||||
import { createLogger } from "@united-workforce/util";
|
||||
import {
|
||||
type AgentContext,
|
||||
type AgentRunResult,
|
||||
buildContinuationPrompt,
|
||||
buildRolePrompt,
|
||||
createAgent,
|
||||
getCachedSessionId,
|
||||
setCachedSessionId,
|
||||
} from "@united-workforce/util-agent";
|
||||
|
||||
import { parseClaudeCodeStreamOutput, storeClaudeCodeDetail } from "./session-detail.js";
|
||||
|
||||
const log = createLogger({ sink: { kind: "stderr" } });
|
||||
|
||||
const CLAUDE_COMMAND = "claude";
|
||||
const CLAUDE_MAX_TURNS = 90;
|
||||
const CLAUDE_MODEL = process.env.CLAUDE_MODEL ?? null;
|
||||
|
||||
/** Assemble system prompt, task, and prior step outputs for Claude Code. */
|
||||
export function buildClaudeCodePrompt(ctx: AgentContext): string {
|
||||
const roleDef = ctx.workflow.roles[ctx.role];
|
||||
const rolePrompt = roleDef !== undefined ? buildRolePrompt(roleDef) : "";
|
||||
const parts: string[] = [];
|
||||
if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") {
|
||||
parts.push(ctx.outputFormatInstruction, "");
|
||||
}
|
||||
parts.push(rolePrompt, "", "## Task", ctx.start.prompt);
|
||||
|
||||
if (!ctx.isFirstVisit) {
|
||||
// Re-entry (session will be resumed): show only steps since last visit, meta only
|
||||
parts.push("", buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt));
|
||||
} else if (ctx.steps.length > 0) {
|
||||
// First visit: show all steps with content for recent ones
|
||||
parts.push(
|
||||
"",
|
||||
buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt, {
|
||||
includeContent: true,
|
||||
quota: 32000,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
parts.push("", "## Current Instruction", "", ctx.edgePrompt);
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function spawnClaude(
|
||||
args: string[],
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(CLAUDE_COMMAND, args, {
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
child.on("error", (cause) => {
|
||||
const message = cause instanceof Error ? cause.message : String(cause);
|
||||
reject(new Error(`claude spawn failed: ${message}`));
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr, exitCode: code });
|
||||
return;
|
||||
}
|
||||
const detail = stderr.trim() !== "" ? ` stderr=${stderr.trim()}` : "";
|
||||
reject(new Error(`claude exited with code ${code ?? "null"}${detail}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function spawnClaudeRun(
|
||||
prompt: string,
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
|
||||
const args = [
|
||||
"-p",
|
||||
prompt,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--dangerously-skip-permissions",
|
||||
"--max-turns",
|
||||
String(CLAUDE_MAX_TURNS),
|
||||
];
|
||||
if (CLAUDE_MODEL !== null) {
|
||||
args.push("--model", CLAUDE_MODEL);
|
||||
}
|
||||
return spawnClaude(args);
|
||||
}
|
||||
|
||||
function spawnClaudeResume(
|
||||
sessionId: string,
|
||||
message: string,
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
|
||||
const args = [
|
||||
"-p",
|
||||
message,
|
||||
"--resume",
|
||||
sessionId,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--dangerously-skip-permissions",
|
||||
"--max-turns",
|
||||
String(CLAUDE_MAX_TURNS),
|
||||
];
|
||||
if (CLAUDE_MODEL !== null) {
|
||||
args.push("--model", CLAUDE_MODEL);
|
||||
}
|
||||
return spawnClaude(args);
|
||||
}
|
||||
|
||||
async function processClaudeOutput(
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
exitCode: number | null,
|
||||
store: Store,
|
||||
assembledPrompt: string,
|
||||
): Promise<AgentRunResult> {
|
||||
const parsed = parseClaudeCodeStreamOutput(stdout);
|
||||
|
||||
if (parsed !== null) {
|
||||
const { detailHash, output, sessionId } = await storeClaudeCodeDetail(store, parsed);
|
||||
|
||||
// Log incomplete results for visibility
|
||||
if (parsed.subtype === "incomplete") {
|
||||
log(
|
||||
"7NQW8R4P",
|
||||
`Claude Code exited with incomplete output (no result line). Exit code: ${exitCode ?? "null"}, stderr: ${stderr.slice(0, 200)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { output, detailHash, sessionId, assembledPrompt };
|
||||
}
|
||||
|
||||
// Truly unparseable output - provide enhanced error message
|
||||
const exitInfo = exitCode !== null && exitCode !== 0 ? `Exit code: ${exitCode}\n` : "";
|
||||
const stderrInfo = stderr.trim() !== "" ? `Stderr: ${stderr.slice(0, 200)}\n` : "";
|
||||
const stdoutSnippet = stdout.slice(0, 200);
|
||||
|
||||
throw new Error(
|
||||
`Claude Code exited without producing parseable output.\n${exitInfo}${stderrInfo}Stdout (first 200 chars): ${stdoutSnippet}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> {
|
||||
const fullPrompt = buildClaudeCodePrompt(ctx);
|
||||
|
||||
log("K7R2M4N8", `prompt for role=${ctx.role} (length=${fullPrompt.length}):\n${fullPrompt}`);
|
||||
|
||||
// Try resuming a cached session for re-entry scenarios (e.g. reviewer reject → developer re-entry).
|
||||
if (!ctx.isFirstVisit) {
|
||||
const cachedSessionId = await getCachedSessionId("claude-code", ctx.threadId, ctx.role);
|
||||
if (cachedSessionId !== null) {
|
||||
try {
|
||||
const { stdout, stderr, exitCode } = await spawnClaudeResume(cachedSessionId, fullPrompt);
|
||||
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt);
|
||||
if (result.sessionId !== undefined && result.sessionId !== "") {
|
||||
await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(
|
||||
"5VKR8N3Q",
|
||||
`resume failed for session ${cachedSessionId}, falling back to fresh run: ${err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { stdout, stderr, exitCode } = await spawnClaudeRun(fullPrompt);
|
||||
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt);
|
||||
if (result.sessionId !== undefined && result.sessionId !== "") {
|
||||
await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function continueClaudeCode(
|
||||
sessionId: string,
|
||||
message: string,
|
||||
store: Store,
|
||||
): Promise<AgentRunResult> {
|
||||
const { stdout, stderr, exitCode } = await spawnClaudeResume(sessionId, message);
|
||||
return processClaudeOutput(stdout, stderr, exitCode, store, "");
|
||||
}
|
||||
|
||||
/** Agent CLI factory: parses argv, runs Claude Code, extracts output, writes StepNode. */
|
||||
export function createClaudeCodeAgent(): () => Promise<void> {
|
||||
return createAgent({
|
||||
name: "claude-code",
|
||||
run: runClaudeCode,
|
||||
continue: continueClaudeCode,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { createClaudeCodeAgent } from "./claude-code.js";
|
||||
|
||||
const main = createClaudeCodeAgent();
|
||||
void main();
|
||||
@@ -0,0 +1,7 @@
|
||||
export { buildClaudeCodePrompt, createClaudeCodeAgent } from "./claude-code.js";
|
||||
export {
|
||||
parseClaudeCodeJsonOutput,
|
||||
parseClaudeCodeStreamOutput,
|
||||
storeClaudeCodeDetail,
|
||||
storeClaudeCodeRawOutput,
|
||||
} from "./session-detail.js";
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { JSONSchema } from "@ocas/core";
|
||||
|
||||
export const CLAUDE_CODE_DETAIL_SCHEMA: JSONSchema = {
|
||||
title: "claude-code-detail",
|
||||
type: "object",
|
||||
required: [
|
||||
"sessionId",
|
||||
"model",
|
||||
"subtype",
|
||||
"durationMs",
|
||||
"numTurns",
|
||||
"totalCostUsd",
|
||||
"stopReason",
|
||||
"usage",
|
||||
"turns",
|
||||
],
|
||||
properties: {
|
||||
sessionId: { type: "string" },
|
||||
model: { type: "string" },
|
||||
subtype: { type: "string" },
|
||||
durationMs: { type: "integer" },
|
||||
numTurns: { type: "integer" },
|
||||
totalCostUsd: { type: "number" },
|
||||
stopReason: { type: "string" },
|
||||
usage: {
|
||||
type: "object",
|
||||
properties: {
|
||||
inputTokens: { type: "integer" },
|
||||
outputTokens: { type: "integer" },
|
||||
cacheReadInputTokens: { type: "integer" },
|
||||
cacheCreationInputTokens: { type: "integer" },
|
||||
},
|
||||
required: ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"],
|
||||
},
|
||||
turns: {
|
||||
type: "array",
|
||||
items: { type: "string", format: "ocas_ref" },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const CLAUDE_CODE_TURN_SCHEMA: JSONSchema = {
|
||||
title: "claude-code-turn",
|
||||
type: "object",
|
||||
required: ["index", "role", "content", "toolCalls"],
|
||||
properties: {
|
||||
index: { type: "integer" },
|
||||
role: { type: "string" },
|
||||
content: { type: "string" },
|
||||
toolCalls: {},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
export const CLAUDE_CODE_RAW_OUTPUT_SCHEMA: JSONSchema = {
|
||||
title: "claude-code-raw-output",
|
||||
type: "object",
|
||||
required: ["text"],
|
||||
properties: {
|
||||
text: { type: "string" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
@@ -0,0 +1,317 @@
|
||||
import { bootstrap, putSchema, type Store } from "@ocas/core";
|
||||
|
||||
import {
|
||||
CLAUDE_CODE_DETAIL_SCHEMA,
|
||||
CLAUDE_CODE_RAW_OUTPUT_SCHEMA,
|
||||
CLAUDE_CODE_TURN_SCHEMA,
|
||||
} from "./schemas.js";
|
||||
import type {
|
||||
ClaudeCodeDetailPayload,
|
||||
ClaudeCodeParsedResult,
|
||||
ClaudeCodeToolCall,
|
||||
ClaudeCodeTurnPayload,
|
||||
} from "./types.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function safeNumber(v: unknown, fallback = 0): number {
|
||||
return typeof v === "number" ? v : fallback;
|
||||
}
|
||||
|
||||
function safeString(v: unknown, fallback = ""): string {
|
||||
return typeof v === "string" ? v : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool calls from an assistant message content array.
|
||||
*/
|
||||
function extractToolCalls(content: unknown[]): ClaudeCodeToolCall[] {
|
||||
const calls: ClaudeCodeToolCall[] = [];
|
||||
for (const item of content) {
|
||||
if (isRecord(item) && item.type === "tool_use" && typeof item.name === "string") {
|
||||
calls.push({
|
||||
name: item.name,
|
||||
input: typeof item.input === "string" ? item.input : JSON.stringify(item.input ?? {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from a message content array.
|
||||
*/
|
||||
function extractTextContent(content: unknown[]): string {
|
||||
const texts: string[] = [];
|
||||
for (const item of content) {
|
||||
if (isRecord(item) && item.type === "text" && typeof item.text === "string") {
|
||||
texts.push(item.text);
|
||||
}
|
||||
}
|
||||
return texts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool result content from a user message content array.
|
||||
*/
|
||||
function extractToolResultContent(content: unknown[]): string {
|
||||
const results: string[] = [];
|
||||
for (const item of content) {
|
||||
if (isRecord(item) && item.type === "tool_result") {
|
||||
const text = typeof item.content === "string" ? item.content : "";
|
||||
results.push(text);
|
||||
}
|
||||
}
|
||||
return results.join("\n");
|
||||
}
|
||||
|
||||
type ParseState = {
|
||||
turns: ClaudeCodeTurnPayload[];
|
||||
resultLine: Record<string, unknown> | null;
|
||||
model: string;
|
||||
sessionId: string;
|
||||
turnIndex: number;
|
||||
};
|
||||
|
||||
function processSystemLine(parsed: Record<string, unknown>, state: ParseState): void {
|
||||
if (typeof parsed.model === "string") {
|
||||
state.model = parsed.model;
|
||||
}
|
||||
if (typeof parsed.session_id === "string") {
|
||||
state.sessionId = parsed.session_id;
|
||||
}
|
||||
}
|
||||
|
||||
function processAssistantLine(parsed: Record<string, unknown>, state: ParseState): void {
|
||||
if (!isRecord(parsed.message)) return;
|
||||
const content = Array.isArray(parsed.message.content) ? parsed.message.content : [];
|
||||
const textContent = extractTextContent(content as unknown[]);
|
||||
const toolCalls = extractToolCalls(content as unknown[]);
|
||||
if (textContent !== "" || toolCalls.length > 0) {
|
||||
state.turns.push({
|
||||
index: state.turnIndex++,
|
||||
role: "assistant",
|
||||
content: textContent,
|
||||
toolCalls: toolCalls.length > 0 ? toolCalls : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function processUserLine(parsed: Record<string, unknown>, state: ParseState): void {
|
||||
if (!isRecord(parsed.message)) return;
|
||||
const content = Array.isArray(parsed.message.content) ? parsed.message.content : [];
|
||||
const resultContent = extractToolResultContent(content as unknown[]);
|
||||
if (resultContent !== "") {
|
||||
state.turns.push({
|
||||
index: state.turnIndex++,
|
||||
role: "tool_result",
|
||||
content: resultContent,
|
||||
toolCalls: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function processLine(line: string, state: ParseState): void {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!isRecord(parsed)) return;
|
||||
const type = parsed.type;
|
||||
if (type === "system") processSystemLine(parsed, state);
|
||||
else if (type === "assistant") processAssistantLine(parsed, state);
|
||||
else if (type === "user") processUserLine(parsed, state);
|
||||
else if (type === "result") state.resultLine = parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract output text from the last assistant turn.
|
||||
* Used for best-effort extraction when no result line is present.
|
||||
*/
|
||||
function extractLastAssistantContent(turns: ClaudeCodeTurnPayload[]): string {
|
||||
for (let i = turns.length - 1; i >= 0; i--) {
|
||||
const turn = turns[i];
|
||||
if (turn !== undefined && turn.role === "assistant" && turn.content !== "") {
|
||||
return turn.content;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function assembleResult(state: ParseState): ClaudeCodeParsedResult | null {
|
||||
// Handle incomplete result (no result line)
|
||||
if (state.resultLine === null) {
|
||||
// Need at least a session_id from system line to be parseable
|
||||
if (state.sessionId === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Best-effort extraction: get output from last assistant turn
|
||||
const result = extractLastAssistantContent(state.turns);
|
||||
|
||||
return {
|
||||
type: "result",
|
||||
subtype: "incomplete",
|
||||
result,
|
||||
sessionId: state.sessionId,
|
||||
numTurns: state.turns.length,
|
||||
totalCostUsd: 0,
|
||||
durationMs: 0,
|
||||
model: state.model,
|
||||
stopReason: "incomplete_no_result_line",
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
},
|
||||
turns: state.turns,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle complete result (has result line)
|
||||
const sessionId = state.resultLine.session_id;
|
||||
const result = state.resultLine.result;
|
||||
const subtype = state.resultLine.subtype;
|
||||
if (typeof sessionId !== "string" || typeof result !== "string" || typeof subtype !== "string") {
|
||||
return null;
|
||||
}
|
||||
const usage = isRecord(state.resultLine.usage) ? state.resultLine.usage : {};
|
||||
return {
|
||||
type: safeString(state.resultLine.type, "result"),
|
||||
subtype: subtype as ClaudeCodeParsedResult["subtype"],
|
||||
result,
|
||||
sessionId,
|
||||
numTurns: safeNumber(state.resultLine.num_turns),
|
||||
totalCostUsd: safeNumber(state.resultLine.total_cost_usd),
|
||||
durationMs: safeNumber(state.resultLine.duration_ms),
|
||||
model: state.model,
|
||||
stopReason: safeString(state.resultLine.stop_reason),
|
||||
usage: {
|
||||
inputTokens: safeNumber(usage.input_tokens),
|
||||
outputTokens: safeNumber(usage.output_tokens),
|
||||
cacheReadInputTokens: safeNumber(usage.cache_read_input_tokens),
|
||||
cacheCreationInputTokens: safeNumber(usage.cache_creation_input_tokens),
|
||||
},
|
||||
turns: state.turns,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Claude Code stream-json (NDJSON) output.
|
||||
* Each line is a JSON object with type: "system" | "assistant" | "user" | "result".
|
||||
*/
|
||||
export function parseClaudeCodeStreamOutput(stdout: string): ClaudeCodeParsedResult | null {
|
||||
const lines = stdout.trim().split("\n");
|
||||
const state: ParseState = {
|
||||
turns: [],
|
||||
resultLine: null,
|
||||
model: "",
|
||||
sessionId: "",
|
||||
turnIndex: 0,
|
||||
};
|
||||
for (const line of lines) {
|
||||
processLine(line, state);
|
||||
}
|
||||
return assembleResult(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy: parse Claude Code plain JSON output (non-streaming).
|
||||
* Falls back when stream-json is not available.
|
||||
*/
|
||||
export function parseClaudeCodeJsonOutput(stdout: string): ClaudeCodeParsedResult | null {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(stdout.trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) return null;
|
||||
|
||||
const sessionId = parsed.session_id;
|
||||
const result = parsed.result;
|
||||
const subtype = parsed.subtype;
|
||||
|
||||
if (typeof sessionId !== "string" || typeof result !== "string" || typeof subtype !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usage = isRecord(parsed.usage) ? parsed.usage : {};
|
||||
|
||||
return {
|
||||
type: safeString(parsed.type, "result"),
|
||||
subtype: subtype as ClaudeCodeParsedResult["subtype"],
|
||||
result,
|
||||
sessionId,
|
||||
numTurns: safeNumber(parsed.num_turns),
|
||||
totalCostUsd: safeNumber(parsed.total_cost_usd),
|
||||
durationMs: safeNumber(parsed.duration_ms),
|
||||
model: "",
|
||||
stopReason: safeString(parsed.stop_reason),
|
||||
usage: {
|
||||
inputTokens: safeNumber(usage.input_tokens),
|
||||
outputTokens: safeNumber(usage.output_tokens),
|
||||
cacheReadInputTokens: safeNumber(usage.cache_read_input_tokens),
|
||||
cacheCreationInputTokens: safeNumber(usage.cache_creation_input_tokens),
|
||||
},
|
||||
turns: [],
|
||||
};
|
||||
}
|
||||
|
||||
type ClaudeCodeSchemaHashes = {
|
||||
detail: string;
|
||||
turn: string;
|
||||
rawOutput: string;
|
||||
};
|
||||
|
||||
async function registerSchemas(store: Store): Promise<ClaudeCodeSchemaHashes> {
|
||||
await bootstrap(store);
|
||||
const [detail, turn, rawOutput] = await Promise.all([
|
||||
putSchema(store, CLAUDE_CODE_DETAIL_SCHEMA),
|
||||
putSchema(store, CLAUDE_CODE_TURN_SCHEMA),
|
||||
putSchema(store, CLAUDE_CODE_RAW_OUTPUT_SCHEMA),
|
||||
]);
|
||||
return { detail, turn, rawOutput };
|
||||
}
|
||||
|
||||
/** Store parsed Claude Code result with per-turn breakdown as CAS detail nodes. */
|
||||
export async function storeClaudeCodeDetail(
|
||||
store: Store,
|
||||
parsed: ClaudeCodeParsedResult,
|
||||
): Promise<{ detailHash: string; output: string; sessionId: string }> {
|
||||
const schemas = await registerSchemas(store);
|
||||
|
||||
// Store each turn as an individual CAS node
|
||||
const turnHashes: string[] = [];
|
||||
for (const turn of parsed.turns) {
|
||||
const hash = await store.put(schemas.turn, turn);
|
||||
turnHashes.push(hash);
|
||||
}
|
||||
|
||||
const detail: ClaudeCodeDetailPayload = {
|
||||
sessionId: parsed.sessionId,
|
||||
model: parsed.model,
|
||||
subtype: parsed.subtype,
|
||||
durationMs: parsed.durationMs,
|
||||
numTurns: parsed.numTurns,
|
||||
totalCostUsd: parsed.totalCostUsd,
|
||||
stopReason: parsed.stopReason,
|
||||
usage: parsed.usage,
|
||||
turns: turnHashes,
|
||||
};
|
||||
|
||||
const detailHash = await store.put(schemas.detail, detail);
|
||||
return { detailHash, output: parsed.result, sessionId: parsed.sessionId };
|
||||
}
|
||||
|
||||
/** Fallback: store raw text output when JSON parsing fails. */
|
||||
export async function storeClaudeCodeRawOutput(store: Store, rawOutput: string): Promise<string> {
|
||||
const schemas = await registerSchemas(store);
|
||||
return store.put(schemas.rawOutput, { text: rawOutput });
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export type ClaudeCodeResultSubtype = "success" | "error_max_turns" | "error_budget" | "incomplete";
|
||||
|
||||
/** A single tool call within an assistant turn. */
|
||||
export type ClaudeCodeToolCall = {
|
||||
name: string;
|
||||
input: string;
|
||||
};
|
||||
|
||||
/** A single turn (assistant text, tool use, or tool result). */
|
||||
export type ClaudeCodeTurnPayload = {
|
||||
index: number;
|
||||
role: "assistant" | "tool_result";
|
||||
content: string;
|
||||
toolCalls: ClaudeCodeToolCall[] | null;
|
||||
};
|
||||
|
||||
/** Top-level detail stored as CAS node. */
|
||||
export type ClaudeCodeDetailPayload = {
|
||||
sessionId: string;
|
||||
model: string;
|
||||
subtype: string;
|
||||
durationMs: number;
|
||||
numTurns: number;
|
||||
totalCostUsd: number;
|
||||
stopReason: string;
|
||||
usage: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheReadInputTokens: number;
|
||||
cacheCreationInputTokens: number;
|
||||
};
|
||||
turns: string[]; // CAS hashes of ClaudeCodeTurnPayload
|
||||
};
|
||||
|
||||
/** Intermediate parsed result from stream-json output. */
|
||||
export type ClaudeCodeParsedResult = {
|
||||
type: string;
|
||||
subtype: ClaudeCodeResultSubtype;
|
||||
result: string;
|
||||
sessionId: string;
|
||||
numTurns: number;
|
||||
totalCostUsd: number;
|
||||
durationMs: number;
|
||||
model: string;
|
||||
stopReason: string;
|
||||
usage: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheReadInputTokens: number;
|
||||
cacheCreationInputTokens: number;
|
||||
};
|
||||
turns: ClaudeCodeTurnPayload[];
|
||||
};
|
||||
Reference in New Issue
Block a user