@sema-agent/core
Version:
Stateless, task-oriented AI agent core
265 lines (264 loc) • 9.46 kB
JavaScript
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 };
}