@sema-agent/core
Version:
Stateless, task-oriented AI agent core
158 lines (157 loc) • 10.9 kB
JavaScript
import { Type } from "typebox";
import { defineTool } from "../core/tools.js";
import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord } from "../core/background-agent-store.js";
import { DURABLE_AGENT_HANDLE_RE } from "../core/task-registry.js";
import { inlineUntrusted } from "../core/untrusted-text.js";
import { stepsFromMessages } from "./subagent-steps.js";
import { agentStateSourceOfRecord, agentStateSummary, formatAgo, readAgentState } from "../core/agent-state.js";
import { REPORT_FIELD_MAX } from "./subagent.js";
import { getSessionRetainLedger } from "./retain-ledger.js";
import { toolFace } from "../core/tool-catalog-entries.js";
import { thrownText } from "../core/thrown-value.js";
export const AGENT_TRANSCRIPT_TOOL_NAME = "AgentTranscript";
const AGENT_TRANSCRIPT_DEFAULT_N = 20;
const AGENT_TRANSCRIPT_MAX_N = 50;
function stepTimePrefix(s, readAt) {
if (s.inFlight === true)
return s.at !== undefined ? `[started ${formatAgo(readAt - s.at)} ago, no result yet] ` : "[no result yet] ";
return s.at !== undefined ? `[${formatAgo(readAt - s.at)} ago] ` : "";
}
function renderSteps(steps, readAt) {
return steps.map((s) => `- ${stepTimePrefix(s, readAt)}${inlineUntrusted(`${s.tool}${s.target ? ` ${s.target}` : ""}${s.outcome ? ` → ${s.outcome}` : ""}`, REPORT_FIELD_MAX)}`).join("\n");
}
const clipStateHeader = (state) => `State: ${agentStateSummary(state).slice(0, REPORT_FIELD_MAX)}\n`;
export function createAgentTranscriptTool(opts) {
return defineTool({
name: AGENT_TRANSCRIPT_TOOL_NAME,
...toolFace("agent-transcript"),
description: `Read the recent tool steps of a background agent you launched (task_id 'a…' from the Agent tool with ` +
`run_in_background), so you can see precisely where it got to — useful before SendMessage-ing a follow-up, ` +
`or to inspect a stopped agent's tail. Returns the last N steps (tool, target, outcome); it does not re-run ` +
`the agent. Works for agents this run retained (retainSubagentSessions) and for agents whose durable ` +
`record preserved their transcript session; when neither holds you get an honest error. ` +
`The first line states the agent's current state: its status (running / parked / completed / failed / killed …), ` +
`when it last did anything, and — only for an agent parked on a durable approval — that approval's card id, ` +
`state and deadline. Each step shows how long ago it happened; a call still waiting for its result shows ` +
`"started <age> ago, no result yet" (an approval answered in-process while the agent runs is not visible ` +
`here — such a wait shows only as that unfinished call). Report what these lines say rather than estimating progress.`,
parameters: Type.Object({
id: Type.String({ description: "The background agent's task_id (a…) returned by the Agent tool." }),
lastN: Type.Optional(Type.Integer({ minimum: 1, maximum: AGENT_TRANSCRIPT_MAX_N, description: `How many recent steps to return (default ${AGENT_TRANSCRIPT_DEFAULT_N}).` })),
}),
execute: async (args, ctx) => {
const a = args;
const id = String(a.id ?? "").trim();
if (!id)
return { content: "No transcript read: 'id' was empty. Pass the agent's task_id (a…).", details: { error: "empty id" }, isError: true };
const lastN = typeof a.lastN === "number" && Number.isFinite(a.lastN) ? Math.max(1, Math.min(AGENT_TRANSCRIPT_MAX_N, Math.floor(a.lastN))) : AGENT_TRANSCRIPT_DEFAULT_N;
const access = {
owner: ctx.taskId ?? opts.owner,
scope: ctx.principal ?? opts.scope,
...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}),
};
const seat = opts.checkpointSeat ?? ctx.checkpointStoreForChildren;
const stateOf = (src) => readAgentState(src, seat, Date.now());
const readDurableSteps = async (live) => {
if (!opts.agentStore || access.scope === undefined)
return undefined;
let durableRow;
try {
durableRow = await opts.agentStore.get(id, access.scope);
}
catch {
return undefined;
}
if (!durableRow || !canAccessAgentRecord(durableRow, access))
return undefined;
const recordSource = agentStateSourceOfRecord(durableRow);
if (durableRow.sessionId === undefined)
return live === undefined ? await transcriptGone(recordSource) : undefined;
let steps;
try {
const acquired = await opts.runner.sessions.acquire(durableRow.sessionId, { requireExisting: true });
const branch = await acquired.session.getBranch();
const messages = branch.filter((e) => e.type === "message").map((e) => e.message);
steps = stepsFromMessages(messages, lastN);
}
catch (e) {
if (e?.code === "not_found") {
announceTranscriptIntegrityGapOnce(id, access.scope, opts.onTranscriptIntegrityGap);
}
return live === undefined ? await transcriptGone(recordSource) : undefined;
}
const state = await stateOf(live?.() ?? recordSource);
if (steps.length === 0) {
return { content: `${clipStateHeader(state)}Agent ${id} recorded no tool steps.`, details: { type: "agent-transcript", id, steps: [], agentState: state } };
}
return {
content: `${clipStateHeader(state)}Agent ${id} — last ${steps.length} tool step(s) (from its preserved transcript):\n${renderSteps(steps, state.readAt)}`,
details: { type: "agent-transcript", id, steps, agentState: state },
};
};
const transcriptGone = async (source) => {
const state = await stateOf(source);
return { content: `${clipStateHeader(state)}No transcript found for agent ${id}.`, details: { error: "not_found", id, agentState: state }, isError: true };
};
const row = opts.registry.getAccessibleTask(id, access);
if (!row || row.type !== "background_agent") {
if (/^w[0-9a-f]{16}$/.test(id) || (row !== undefined && row.type === "workflow")) {
return {
content: `No transcript: "${id}" is a WORKFLOW run id — AgentTranscript reads sub-agent transcripts only. For the run's per-agent rows (which agent failed, why, elapsed) call TaskOutput({ task_id: "${id}" }); to continue a fixed script, re-invoke the workflow with resumeFromRunId: "${id}".`,
details: { error: "workflow_id", id },
isError: true,
};
}
const durable = await readDurableSteps();
if (durable)
return durable;
if (DURABLE_AGENT_HANDLE_RE.test(id)) {
return { content: `No transcript found for agent ${id}.`, details: { error: "not_found", id }, isError: true };
}
return { content: `No transcript: no background agent task "${id}" (unknown id, or not yours).`, details: { error: "not_found", id }, isError: true };
}
const firstLive = opts.registry.agentStateSource?.(id, access);
const live = firstLive !== undefined ? () => opts.registry.agentStateSource?.(id, access) ?? firstLive : undefined;
const runLedger = ctx.subagentRetain ?? undefined;
const atLedgerSessionId = ctx.sessionId ?? opts.sessionId;
const sessionLedger = atLedgerSessionId !== undefined ? getSessionRetainLedger(atLedgerSessionId) : undefined;
const entry = row.toolUseId !== undefined ? (sessionLedger?.get(row.toolUseId) ?? runLedger?.get(row.toolUseId)) : undefined;
if (!entry) {
const durable = await readDurableSteps(live);
if (durable)
return durable;
const state = live !== undefined ? await stateOf(live()) : undefined;
return {
content: `${state !== undefined ? clipStateHeader(state) : ""}No transcript: agent ${id}'s session was not retained (this run did not enable retainSubagentSessions), so its history is unavailable. Its completion notification carried the recent steps.`,
details: { error: "not_retained", id, ...(state !== undefined ? { agentState: state } : {}) },
isError: true,
};
}
let steps;
try {
const acquired = await (entry.runner ?? opts.runner).sessions.acquire(entry.childSessionId, { requireExisting: true });
const branch = await acquired.session.getBranch();
const messages = branch.filter((e) => e.type === "message").map((e) => e.message);
steps = stepsFromMessages(messages, lastN);
}
catch (e) {
const code = e?.code;
const state = live !== undefined ? await stateOf(live()) : undefined;
return {
content: `${state !== undefined ? clipStateHeader(state) : ""}No transcript: agent ${id}'s session could not be read (${(code ?? thrownText(e)).slice(0, REPORT_FIELD_MAX)}).`,
details: { error: code ?? "read_failed", id, ...(state !== undefined ? { agentState: state } : {}) },
isError: true,
};
}
const state = live !== undefined ? await stateOf(live()) : undefined;
const readAt = state?.readAt ?? Date.now();
if (steps.length === 0) {
return { content: `${state !== undefined ? clipStateHeader(state) : ""}Agent ${id} recorded no tool steps yet.`, details: { type: "agent-transcript", id, steps: [], ...(state !== undefined ? { agentState: state } : {}) } };
}
return {
content: `${state !== undefined ? clipStateHeader(state) : ""}Agent ${id} — last ${steps.length} tool step(s):\n${renderSteps(steps, readAt)}`,
details: { type: "agent-transcript", id, steps, ...(state !== undefined ? { agentState: state } : {}) },
};
},
}, opts.enrichCtx !== undefined ? { enrichCtx: opts.enrichCtx } : {});
}