UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

189 lines (188 loc) • 11.7 kB
import { Type } from "typebox"; import { defineTool } from "../core/tools.js"; import { inlineUntrusted } from "../core/untrusted-text.js"; import { formatPeerNameRef } from "./cross-session-ref.js"; import { entryAccessible } from "./roster-store.js"; import { mintPeerSessionCandidates, PEER_PRESENCE_MAX_CHARS, peerSessionBoxHandle } from "./peer-directory.js"; import { PEER_LANE_DEFAULTS } from "./peer-admission.js"; import { toolFace } from "../core/tool-catalog-entries.js"; import { thrownText } from "../core/thrown-value.js"; import { assertNoRetiredKeys } from "../core/retired-keys.js"; import { agentActivityClause, agentApprovalClause, formatAgo, readAgentState } from "../core/agent-state.js"; export const LIST_AGENTS_TOOL_NAME = "ListAgents"; export const LIST_AGENTS_TOOL_ALIAS = "ListPeers"; export const LIST_AGENTS_MAX_RESULT_CHARS = 10_000; export const PEER_LISTING_FRESH_WINDOW_MS = PEER_LANE_DEFAULTS.listingFreshWindowMs; export function isFreshPeerRow(r, now, windowMs = PEER_LISTING_FRESH_WINDOW_MS) { if (r.liveness !== "live") return false; if (r.tempo === "active" || r.tempo === "blocked") return true; return now - r.updatedAt <= windowMs; } export { formatAgo }; export function cwdTail(cwd) { const parts = cwd.split(/[\\/]+/).filter((p) => p !== ""); return parts.slice(-2).join("/"); } const AGENT_ROW_STATE = { pending: "running", running: "running", parked: "parked", completed: "finished", failed: "failed", killed: "killed", cancelled: "cancelled", }; function agentRowState(status) { return Object.hasOwn(AGENT_ROW_STATE, status) ? AGENT_ROW_STATE[status] : "unknown"; } const PEER_ROW_STATE = { active: "busy", blocked: "busy", idle: "idle", }; function peerRowState(r) { if (r.liveness === "dead") return "offline"; const tempo = r.tempo; return tempo !== undefined && Object.hasOwn(PEER_ROW_STATE, tempo) ? PEER_ROW_STATE[tempo] : "unknown"; } function renderPresence(p) { const parts = [...(p.agent !== undefined ? [`agent=${inlineUntrusted(p.agent, PEER_PRESENCE_MAX_CHARS)}`] : []), ...(p.intent !== undefined ? [`intent=${inlineUntrusted(p.intent, PEER_PRESENCE_MAX_CHARS)}`] : [])]; return `self: ${parts.join(", ")}`; } const RETIRED_LIST_AGENTS_OPTION_KEYS = { includeOffline: "the model asks for stale and offline rows per call with the `includeAll` tool parameter (a superset: stale + offline)", }; export function createListAgentsTool(opts) { assertNoRetiredKeys(opts, RETIRED_LIST_AGENTS_OPTION_KEYS, "ListAgentsToolOptions"); const peers = opts.peerDirectory !== undefined; const windowMs = opts.freshWindowMs ?? PEER_LISTING_FRESH_WINDOW_MS; return defineTool({ name: LIST_AGENTS_TOOL_NAME, ...toolFace("list-agents"), isConcurrencySafe: () => true, description: `List the agents${peers ? " and peer sessions" : ""} you can message with SendMessage. ` + (peers ? `Peer sessions are other sessions of this engine running for the same user, shown as \`name [ref] · state · active <N> ago · <directory> · self: agent=…, intent=…\`; send to one with its name (or \`name [ref]\` ` + `when a name is ambiguous, or its \`session.<id>\` address). By default only FRESH peers are listed (working now, or active within the last ${formatAgo(windowMs)}); ` + `pass includeAll: true to also list stale and offline ones. A peer that is offline or unlisted still accepts messages — they are parked ` + `durably in its session box and delivered when it next runs. ` : `Peer sessions of this engine are not addressable on this mount (no session directory). `) + `Background agents you spawned are listed with their task_id, their status, when they last did anything, and — for one parked on a durable approval — that approval's card id, state and deadline. ` + `A ref is only valid against the listing (or disambiguation error) you read it from — do not invent one.`, parameters: Type.Object({ includeAll: Type.Optional(Type.Boolean({ description: "Also list stale (no activity within the freshness window) and offline peer sessions. Default false." })), }), execute: async (args) => { const includeAll = args.includeAll === true; const now = opts.now?.() ?? Date.now(); const rows = []; let peerRows = []; let directoryFault; if (opts.peerDirectory !== undefined) { try { peerRows = await opts.peerDirectory.listPeerSessions({ scope: opts.self.scope }); } catch (e) { directoryFault = thrownText(e); } } for (const c of mintPeerSessionCandidates(peerRows, { sessionId: opts.self.sessionId })) { const r = c.record; if (r.sessionId.toLowerCase() === opts.self.sessionId.toLowerCase()) continue; if (r.liveness === "deleted") continue; if (!includeAll && !isFreshPeerRow(r, now, windowMs)) continue; const presence = { ...(r.agent !== undefined ? { agent: r.agent } : {}), ...(r.intent !== undefined ? { intent: r.intent } : {}) }; rows.push({ kind: "peer_session", name: r.name, ref: c.ref, address: peerSessionBoxHandle(r.sessionId), state: peerRowState(r), ...(r.liveness === "live" ? { lastActiveAt: r.updatedAt } : r.diedAt !== undefined ? { offlineAt: r.diedAt } : {}), ...(r.cwd !== undefined ? { cwd: cwdTail(r.cwd) } : {}), ...(presence.agent !== undefined || presence.intent !== undefined ? { presence } : {}), }); } let registryCapped = false; const agentStateOf = (id, view) => { const src = opts.registry?.agentStateSource?.(id, view); return src === undefined ? undefined : readAgentState(src, opts.checkpointSeat, opts.now?.() ?? Date.now()); }; const views = [opts.access, opts.parentAccess].filter((a) => a !== undefined); const seenIds = new Set(); if (opts.registry !== undefined) { for (const view of views) { const listed = opts.registry.list(view, { limit: 500 }); registryCapped ||= listed.length >= 500; for (const t of listed) { if (t.type !== "background_agent" || seenIds.has(t.task_id)) continue; seenIds.add(t.task_id); const full = opts.registry.getAccessibleTask(t.task_id, view); const stateRead = agentStateOf(t.task_id, view); const agentState = stateRead instanceof Promise ? await stateRead : stateRead; rows.push({ kind: "background_agent", name: full?.name ?? t.description ?? t.task_id, address: t.task_id, state: agentRowState(agentState?.status ?? t.status), ...(agentState !== undefined ? { agentState } : {}), }); } } } if (opts.roster !== undefined && views.length > 0) { try { for (const e of await opts.roster.list()) { const view = views.find((v) => entryAccessible(e, v)); if (view === undefined || seenIds.has(e.agentId)) continue; seenIds.add(e.agentId); const live = opts.registry?.getAccessibleTask(e.agentId, view); const stateRead = live === undefined ? undefined : agentStateOf(e.agentId, view); const agentState = stateRead instanceof Promise ? await stateRead : stateRead; rows.push({ kind: "roster", name: e.name, address: e.agentId, state: live === undefined ? "unknown" : agentRowState(agentState?.status ?? live.status), ...(agentState !== undefined ? { agentState } : {}), }); } } catch { } } const tier = opts.continuationTier ?? "none"; const rosterLabel = tier === "full" ? "agent (durable record)" : tier === "rows" ? "agent (record kept; conversation not kept across restarts)" : "agent (recorded name)"; const finishedReach = tier === "full" ? "while its session is retained or a durable record exists" : tier === "rows" ? "while its session is retained or this engine process still holds its conversation" : "while its session is retained"; const lines = rows.map((r) => { const label = r.kind === "peer_session" ? formatPeerNameRef(inlineUntrusted(r.name, 64), r.ref) : inlineUntrusted(r.name, 64); const age = r.kind !== "peer_session" ? undefined : r.lastActiveAt !== undefined ? `active ${formatAgo(now - r.lastActiveAt)} ago` : r.offlineAt !== undefined ? `offline ${formatAgo(now - r.offlineAt)} ago` : "offline"; const activity = r.agentState !== undefined ? agentActivityClause(r.agentState) : undefined; const approval = r.agentState?.approval !== undefined ? agentApprovalClause(r.agentState.approval, r.agentState.readAt) : undefined; const cols = [label, r.state, age, activity, approval, r.cwd !== undefined ? inlineUntrusted(r.cwd, 64) : undefined, r.presence !== undefined ? renderPresence(r.presence) : undefined, r.kind === "peer_session" ? "peer session" : r.kind === "roster" ? rosterLabel : "agent", r.address]; return cols.filter((c) => c !== undefined).join(" · "); }); const finishedCount = rows.filter((r) => r.state === "finished").length; const header = rows.length === 0 ? "No agents or peer sessions are listed right now." : `${rows.length} listed:`; const finishedNote = finishedCount > 0 ? `\n(${finishedCount} finished agent(s): continuable by SendMessage only ${finishedReach} — otherwise SendMessage answers with an honest refusal)` : ""; const fault = (directoryFault !== undefined ? `\n(peer-session directory unreadable: ${inlineUntrusted(directoryFault, 200)} — peer rows may be missing)` : "") + (registryCapped ? "\n(the live task list is at its 500-row window: older background agents may be missing from this listing — address them by task_id)" : ""); let content = `${header}${lines.length > 0 ? `\n${lines.join("\n")}` : ""}${finishedNote}${fault}`; if (content.length > LIST_AGENTS_MAX_RESULT_CHARS) content = `${content.slice(0, LIST_AGENTS_MAX_RESULT_CHARS)}\n[listing truncated: ${content.length} chars total]`; return { content, details: { type: "list-agents", rows, ...(directoryFault !== undefined ? { directoryFault: true } : {}) } }; }, }); }