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.
280 lines
9.1 KiB
TypeScript
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,
|
|
});
|
|
},
|
|
};
|
|
}
|