UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

265 lines (264 loc) • 9.46 kB
import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; const SIDECAR_RE = /^agent-(.+)\.jsonl$/; export function listCcSidecarAgents(subagentsDir) { if (!existsSync(subagentsDir)) return []; const out = []; const walk = (dir, segments) => { let entries; try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const ent of entries) { if (ent.isDirectory()) { walk(join(dir, ent.name), [...segments, ent.name]); continue; } if (!ent.isFile()) continue; const m = SIDECAR_RE.exec(ent.name); if (!m) continue; const agentId = m[1]; const jsonlPath = join(dir, ent.name); const metaPath = join(dir, `agent-${agentId}.meta.json`); let meta; if (existsSync(metaPath)) { try { const parsed = JSON.parse(readFileSync(metaPath, "utf8")); if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) { meta = parsed; } } catch { meta = undefined; } } out.push({ agentId, jsonlPath, segments, meta }); } }; walk(subagentsDir, []); out.sort((a, b) => { const sa = a.segments.join("/"); const sb = b.segments.join("/"); return sa < sb ? -1 : sa > sb ? 1 : a.agentId < b.agentId ? -1 : a.agentId > b.agentId ? 1 : 0; }); return out; } function splitLines(bytes) { const out = []; let start = 0; for (let i = 0; i < bytes.length; i++) { if (bytes[i] === 0x0a) { out.push(bytes.subarray(start, i)); start = i + 1; } } out.push(bytes.subarray(start)); return out; } export function readCcSidecarTranscript(jsonlPath) { let bytes; try { bytes = readFileSync(jsonlPath); } catch (err) { const code = err.code; if (code === "ENOENT" || code === "ENOTDIR") return { lines: [], malformedLines: 0 }; throw err; } const lines = []; let malformedLines = 0; const decoder = new TextDecoder("utf-8", { fatal: true }); for (const lineBytes of splitLines(bytes)) { let rawLine; try { rawLine = decoder.decode(lineBytes); } catch { if (lineBytes.length > 0) malformedLines += 1; continue; } if (rawLine.trim() === "") continue; try { const parsed = JSON.parse(rawLine); if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) { lines.push({ raw: parsed }); } else { malformedLines += 1; } } catch { malformedLines += 1; } } return { lines, malformedLines }; } function asRecord(v) { return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined; } function toMs(iso) { if (typeof iso !== "string") return 0; const ms = Date.parse(iso); return Number.isFinite(ms) ? ms : 0; } function num(v) { return typeof v === "number" && Number.isFinite(v) ? v : 0; } function mapStopReason(v) { switch (v) { case "tool_use": return "toolUse"; case "max_tokens": return "length"; default: return "stop"; } } function mapUsage(v) { const u = asRecord(v); const input = num(u?.input_tokens); const output = num(u?.output_tokens); const cacheRead = num(u?.cache_read_input_tokens); const cacheWrite = num(u?.cache_creation_input_tokens); return { usage: { input, output, cacheRead, cacheWrite, totalTokens: input + output + cacheRead + cacheWrite, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, usageMissing: u === undefined, }; } export function ccSidecarToMessages(lines) { const messages = []; let skippedLines = 0; const toolNames = new Map(); for (const { raw } of lines) { const message = asRecord(raw.message); const timestamp = toMs(raw.timestamp); if (raw.type === "user" && message !== undefined) { const content = message.content; if (typeof content === "string") { const m = { role: "user", content, timestamp }; messages.push(m); continue; } if (Array.isArray(content)) { let userBlocks = []; let projectedAny = false; const flushUser = () => { if (userBlocks.length > 0) { messages.push({ role: "user", content: userBlocks, timestamp }); userBlocks = []; projectedAny = true; } }; for (const block of content) { const b = asRecord(block); if (b?.type === "text" && typeof b.text === "string") { userBlocks.push({ type: "text", text: b.text }); } else if (b?.type === "image") { const source = asRecord(b.source); if (source?.type === "base64" && typeof source.data === "string" && typeof source.media_type === "string") { userBlocks.push({ type: "image", data: source.data, mimeType: source.media_type }); } } else if (b?.type === "tool_result" && typeof b.tool_use_id === "string") { flushUser(); const m = { role: "toolResult", toolCallId: b.tool_use_id, toolName: toolNames.get(b.tool_use_id) ?? "unknown", content: projectToolResultContent(b.content), isError: b.is_error === true, timestamp, }; messages.push(m); projectedAny = true; } } flushUser(); if (!projectedAny) skippedLines += 1; continue; } skippedLines += 1; continue; } if (raw.type === "assistant" && message !== undefined && Array.isArray(message.content)) { const blocks = []; for (const block of message.content) { const b = asRecord(block); if (b?.type === "text" && typeof b.text === "string") { blocks.push({ type: "text", text: b.text }); } else if (b?.type === "thinking" && typeof b.thinking === "string") { const t = { type: "thinking", thinking: b.thinking }; if (typeof b.signature === "string") t.thinkingSignature = b.signature; blocks.push(t); } else if (b?.type === "redacted_thinking" && typeof b.data === "string") { blocks.push({ type: "thinking", thinking: "", thinkingSignature: b.data, redacted: true }); } else if (b?.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") { toolNames.set(b.id, b.name); blocks.push({ type: "toolCall", id: b.id, name: b.name, arguments: asRecord(b.input) ?? {} }); } } if (blocks.length === 0) { skippedLines += 1; continue; } const { usage, usageMissing } = mapUsage(message.usage); const m = { role: "assistant", content: blocks, api: "anthropic-messages", provider: "claude-code", model: typeof message.model === "string" ? message.model : "unknown", usage, stopReason: mapStopReason(message.stop_reason), timestamp, }; if (usageMissing) m.usageMissing = true; messages.push(m); continue; } skippedLines += 1; } return { messages, skippedLines }; } function projectToolResultContent(content) { if (typeof content === "string") return [{ type: "text", text: content }]; if (!Array.isArray(content)) return []; const out = []; for (const block of content) { const b = asRecord(block); if (b?.type === "text" && typeof b.text === "string") out.push({ type: "text", text: b.text }); } return out; } export function readCcSidecarMessages(handle) { const { lines, malformedLines } = readCcSidecarTranscript(handle.jsonlPath); const projected = ccSidecarToMessages(lines); return { ...projected, malformedLines }; }