import path from "node:path"; import { Type } from "@sinclair/typebox"; import { loadConfig } from "../../config/config.js"; import { callGateway } from "../../gateway/call.js"; import { isSubagentSessionKey, normalizeAgentId, parseAgentSessionKey, } from "../../routing/session-key.js"; import type { AnyAgentTool } from "./common.js"; import { jsonResult, readStringArrayParam } from "./common.js"; import { classifySessionKind, deriveProvider, resolveDisplaySessionKey, resolveInternalSessionKey, resolveMainSessionAlias, type SessionKind, stripToolMessages, } from "./sessions-helpers.js"; type SessionListRow = { key: string; kind: SessionKind; provider: string; label?: string; displayName?: string; updatedAt?: number | null; sessionId?: string; model?: string; contextTokens?: number | null; totalTokens?: number | null; thinkingLevel?: string; verboseLevel?: string; systemSent?: boolean; abortedLastRun?: boolean; sendPolicy?: string; lastProvider?: string; lastTo?: string; lastAccountId?: string; transcriptPath?: string; messages?: unknown[]; }; const SessionsListToolSchema = Type.Object({ kinds: Type.Optional(Type.Array(Type.String())), limit: Type.Optional(Type.Number({ minimum: 1 })), activeMinutes: Type.Optional(Type.Number({ minimum: 1 })), messageLimit: Type.Optional(Type.Number({ minimum: 0 })), }); function resolveSandboxSessionToolsVisibility( cfg: ReturnType, ) { return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; } export function createSessionsListTool(opts?: { agentSessionKey?: string; sandboxed?: boolean; }): AnyAgentTool { return { label: "Sessions", name: "sessions_list", description: "List sessions with optional filters and last messages.", parameters: SessionsListToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; const cfg = loadConfig(); const { mainKey, alias } = resolveMainSessionAlias(cfg); const visibility = resolveSandboxSessionToolsVisibility(cfg); const requesterInternalKey = typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim() ? resolveInternalSessionKey({ key: opts.agentSessionKey, alias, mainKey, }) : undefined; const restrictToSpawned = opts?.sandboxed === true && visibility === "spawned" && requesterInternalKey && !isSubagentSessionKey(requesterInternalKey); const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) => value.trim().toLowerCase(), ); const allowedKindsList = (kindsRaw ?? []).filter((value) => ["main", "group", "cron", "hook", "node", "other"].includes(value), ); const allowedKinds = allowedKindsList.length ? new Set(allowedKindsList) : undefined; const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.floor(params.limit)) : undefined; const activeMinutes = typeof params.activeMinutes === "number" && Number.isFinite(params.activeMinutes) ? Math.max(1, Math.floor(params.activeMinutes)) : undefined; const messageLimitRaw = typeof params.messageLimit === "number" && Number.isFinite(params.messageLimit) ? Math.max(0, Math.floor(params.messageLimit)) : 0; const messageLimit = Math.min(messageLimitRaw, 20); const list = (await callGateway({ method: "sessions.list", params: { limit, activeMinutes, includeGlobal: !restrictToSpawned, includeUnknown: !restrictToSpawned, spawnedBy: restrictToSpawned ? requesterInternalKey : undefined, }, })) as { path?: string; sessions?: Array>; }; const sessions = Array.isArray(list?.sessions) ? list.sessions : []; const storePath = typeof list?.path === "string" ? list.path : undefined; const routingA2A = cfg.tools?.agentToAgent; const a2aEnabled = routingA2A?.enabled === true; const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : []; const matchesAllow = (agentId: string) => { if (allowPatterns.length === 0) return true; return allowPatterns.some((pattern) => { const raw = String(pattern ?? "").trim(); if (!raw) return false; if (raw === "*") return true; if (!raw.includes("*")) return raw === agentId; const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i"); return re.test(agentId); }); }; const requesterAgentId = normalizeAgentId( parseAgentSessionKey(requesterInternalKey)?.agentId, ); const rows: SessionListRow[] = []; for (const entry of sessions) { if (!entry || typeof entry !== "object") continue; const key = typeof entry.key === "string" ? entry.key : ""; if (!key) continue; const entryAgentId = normalizeAgentId( parseAgentSessionKey(key)?.agentId, ); const crossAgent = entryAgentId !== requesterAgentId; if (crossAgent) { if (!a2aEnabled) continue; if (!matchesAllow(requesterAgentId) || !matchesAllow(entryAgentId)) continue; } if (key === "unknown") continue; if (key === "global" && alias !== "global") continue; const gatewayKind = typeof entry.kind === "string" ? entry.kind : undefined; const kind = classifySessionKind({ key, gatewayKind, alias, mainKey }); if (allowedKinds && !allowedKinds.has(kind)) continue; const displayKey = resolveDisplaySessionKey({ key, alias, mainKey, }); const entryProvider = typeof entry.provider === "string" ? entry.provider : undefined; const lastProvider = typeof entry.lastProvider === "string" ? entry.lastProvider : undefined; const lastAccountId = typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined; const derivedProvider = deriveProvider({ key, kind, provider: entryProvider, lastProvider, }); const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : undefined; const transcriptPath = sessionId && storePath ? path.join(path.dirname(storePath), `${sessionId}.jsonl`) : undefined; const row: SessionListRow = { key: displayKey, kind, provider: derivedProvider, label: typeof entry.label === "string" ? entry.label : undefined, displayName: typeof entry.displayName === "string" ? entry.displayName : undefined, updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : undefined, sessionId, model: typeof entry.model === "string" ? entry.model : undefined, contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : undefined, totalTokens: typeof entry.totalTokens === "number" ? entry.totalTokens : undefined, thinkingLevel: typeof entry.thinkingLevel === "string" ? entry.thinkingLevel : undefined, verboseLevel: typeof entry.verboseLevel === "string" ? entry.verboseLevel : undefined, systemSent: typeof entry.systemSent === "boolean" ? entry.systemSent : undefined, abortedLastRun: typeof entry.abortedLastRun === "boolean" ? entry.abortedLastRun : undefined, sendPolicy: typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined, lastProvider, lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined, lastAccountId, transcriptPath, }; if (messageLimit > 0) { const resolvedKey = resolveInternalSessionKey({ key: displayKey, alias, mainKey, }); const history = (await callGateway({ method: "chat.history", params: { sessionKey: resolvedKey, limit: messageLimit }, })) as { messages?: unknown[] }; const rawMessages = Array.isArray(history?.messages) ? history.messages : []; const filtered = stripToolMessages(rawMessages); row.messages = filtered.length > messageLimit ? filtered.slice(-messageLimit) : filtered; } rows.push(row); } return jsonResult({ count: rows.length, sessions: rows, }); }, }; }