Files
clawdbot/src/agents/tools/sessions-list-tool.ts
Keith the Silly Goose 423eef4624 fix: simplify tool schemas for Gemini compatibility
Replaces Type.Integer with Type.Number and simplifies the sessions_send tool schema to avoid anyOf/oneOf unions that cause 400 errors with Google Cloud Code Assist API.
2026-01-09 20:15:26 +01:00

280 lines
9.1 KiB
TypeScript

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<typeof loadConfig>,
) {
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<string, unknown>;
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<Record<string, unknown>>;
};
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,
});
},
};
}