UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

158 lines (157 loc) • 10.9 kB
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 } : {}); }