feat: expose mailbox session discovery in sessions_list

This commit is contained in:
dangoZhang
2026-04-22 05:11:28 +08:00
committed by Peter Steinberger
parent dcc243c889
commit 1a4c32e366
6 changed files with 82 additions and 10 deletions

View File

@@ -16,7 +16,7 @@ orchestrate sub-agents.
| Tool | What it does |
| ------------------ | --------------------------------------------------------------------------- |
| `sessions_list` | List sessions with optional filters (kind, recency) |
| `sessions_list` | List sessions with optional filters (kind, label, agent, recency, preview) |
| `sessions_history` | Read the transcript of a specific session |
| `sessions_send` | Send a message to another session and optionally wait |
| `sessions_spawn` | Spawn an isolated sub-agent session for background work |
@@ -26,9 +26,11 @@ orchestrate sub-agents.
## Listing and reading sessions
`sessions_list` returns sessions with their key, kind, channel, model, token
counts, and timestamps. Filter by kind (`main`, `group`, `cron`, `hook`,
`node`) or recency (`activeMinutes`).
`sessions_list` returns sessions with their key, agentId, kind, channel, model,
token counts, and timestamps. Filter by kind (`main`, `group`, `cron`, `hook`,
`node`), exact `label`, exact `agentId`, search text, or recency
(`activeMinutes`). When you need mailbox-style triage, it can also ask for
derived titles, last-message previews, or bounded recent messages.
`sessions_history` fetches the conversation transcript for a specific session.
By default, tool results are excluded -- pass `includeTools: true` to see them.

View File

@@ -200,10 +200,15 @@ describe("sessions tools", () => {
expect(schemaProp("sessions_list", "limit").type).toBe("number");
expect(schemaProp("sessions_list", "activeMinutes").type).toBe("number");
expect(schemaProp("sessions_list", "messageLimit").type).toBe("number");
expect(schemaProp("sessions_list", "label").type).toBe("string");
expect(schemaProp("sessions_list", "agentId").type).toBe("string");
expect(schemaProp("sessions_list", "search").type).toBe("string");
expect(schemaProp("sessions_list", "includeDerivedTitles").type).toBe("boolean");
expect(schemaProp("sessions_list", "includeLastMessage").type).toBe("boolean");
expect(schemaProp("sessions_send", "timeoutSeconds").type).toBe("number");
});
it("sessions_list filters kinds and includes messages", async () => {
it("sessions_list forwards mailbox filters and includes messages", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "sessions.list") {
@@ -216,6 +221,8 @@ describe("sessions tools", () => {
sessionId: "s-main",
updatedAt: 10,
lastChannel: "whatsapp",
derivedTitle: "Main mailbox",
lastMessagePreview: "Latest assistant update",
},
{
key: "discord:group:dev",
@@ -229,6 +236,8 @@ describe("sessions tools", () => {
runtimeMs: 42,
estimatedCostUsd: 0.0042,
childSessions: ["agent:main:subagent:worker"],
derivedTitle: "Dev room",
lastMessagePreview: "Need review on the patch",
},
{
key: "agent:main:dashboard:child",
@@ -275,11 +284,36 @@ describe("sessions tools", () => {
throw new Error("missing sessions_list tool");
}
const result = await tool.execute("call1", { messageLimit: 1 });
const result = await tool.execute("call1", {
agentId: "main",
label: "mailbox",
search: "review",
includeDerivedTitles: true,
includeLastMessage: true,
messageLimit: 1,
});
expect(callGatewayMock).toHaveBeenNthCalledWith(1, {
method: "sessions.list",
params: {
activeMinutes: undefined,
agentId: "main",
includeDerivedTitles: true,
includeGlobal: true,
includeLastMessage: true,
includeUnknown: true,
label: "mailbox",
limit: undefined,
search: "review",
spawnedBy: undefined,
},
});
const details = result.details as {
sessions?: Array<{
key?: string;
agentId?: string;
channel?: string;
derivedTitle?: string;
lastMessagePreview?: string;
spawnedBy?: string;
status?: string;
startedAt?: number;
@@ -292,7 +326,10 @@ describe("sessions tools", () => {
};
expect(details.sessions).toHaveLength(5);
const main = details.sessions?.find((s) => s.key === "main");
expect(main?.agentId).toBe("main");
expect(main?.channel).toBe("whatsapp");
expect(main?.derivedTitle).toBe("Main mailbox");
expect(main?.lastMessagePreview).toBe("Latest assistant update");
expect(main?.messages?.length).toBe(1);
expect(main?.messages?.[0]?.role).toBe("assistant");
@@ -302,6 +339,8 @@ describe("sessions tools", () => {
expect(group?.runtimeMs).toBe(42);
expect(group?.estimatedCostUsd).toBe(0.0042);
expect(group?.childSessions).toEqual(["agent:main:subagent:worker"]);
expect(group?.derivedTitle).toBe("Dev room");
expect(group?.lastMessagePreview).toBe("Need review on the patch");
const dashboardChild = details.sessions?.find((s) => s.key === "agent:main:dashboard:child");
expect(dashboardChild?.parentSessionKey).toBe("agent:main:main");

View File

@@ -2,7 +2,7 @@ export const EXEC_TOOL_DISPLAY_SUMMARY = "Run shell commands that start now.";
export const PROCESS_TOOL_DISPLAY_SUMMARY = "Inspect and control running exec sessions.";
export const CRON_TOOL_DISPLAY_SUMMARY = "Schedule cron jobs, reminders, and wake events.";
export const SESSIONS_LIST_TOOL_DISPLAY_SUMMARY =
"List visible sessions and optional recent messages.";
"List visible sessions with mailbox filters and optional previews.";
export const SESSIONS_HISTORY_TOOL_DISPLAY_SUMMARY =
"Read sanitized message history for a visible session.";
export const SESSIONS_SEND_TOOL_DISPLAY_SUMMARY = "Send a message to another visible session.";
@@ -12,7 +12,7 @@ export const UPDATE_PLAN_TOOL_DISPLAY_SUMMARY = "Track a short structured work p
export function describeSessionsListTool(): string {
return [
"List visible sessions with optional filters for kind, recent activity, and last messages.",
"List visible sessions with optional filters for kind, label, agentId, search, recent activity, derived titles, and last-message previews.",
"Use this to discover a target session before calling sessions_history or sessions_send.",
].join(" ");
}

View File

@@ -287,7 +287,17 @@ export const TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
sessions_list: {
emoji: "🗂️",
title: "Sessions",
detailKeys: ["kinds", "limit", "activeMinutes", "messageLimit"],
detailKeys: [
"kinds",
"label",
"agentId",
"search",
"limit",
"activeMinutes",
"includeDerivedTitles",
"includeLastMessage",
"messageLimit",
],
},
sessions_send: {
emoji: "📨",

View File

@@ -50,6 +50,7 @@ export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeo
export type SessionListRow = {
key: string;
agentId?: string;
kind: SessionKind;
channel: string;
origin?: {
@@ -59,6 +60,8 @@ export type SessionListRow = {
spawnedBy?: string;
label?: string;
displayName?: string;
derivedTitle?: string;
lastMessagePreview?: string;
parentSessionKey?: string;
deliveryContext?: SessionListDeliveryContext;
updatedAt?: number | null;

View File

@@ -15,7 +15,7 @@ import {
SESSIONS_LIST_TOOL_DISPLAY_SUMMARY,
} from "../tool-description-presets.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringArrayParam } from "./common.js";
import { jsonResult, readStringArrayParam, readStringParam } from "./common.js";
import {
createSessionVisibilityGuard,
createAgentToAgentPolicy,
@@ -35,6 +35,11 @@ const SessionsListToolSchema = Type.Object({
limit: Type.Optional(Type.Number({ minimum: 1 })),
activeMinutes: Type.Optional(Type.Number({ minimum: 1 })),
messageLimit: Type.Optional(Type.Number({ minimum: 0 })),
label: Type.Optional(Type.String({ minLength: 1 })),
agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
search: Type.Optional(Type.String({ minLength: 1 })),
includeDerivedTitles: Type.Optional(Type.Boolean()),
includeLastMessage: Type.Optional(Type.Boolean()),
});
type GatewayCaller = typeof callGateway;
@@ -97,6 +102,11 @@ export function createSessionsListTool(opts?: {
? Math.max(0, Math.floor(params.messageLimit))
: 0;
const messageLimit = Math.min(messageLimitRaw, 20);
const label = readStringParam(params, "label");
const agentId = readStringParam(params, "agentId");
const search = readStringParam(params, "search");
const includeDerivedTitles = params.includeDerivedTitles === true;
const includeLastMessage = params.includeLastMessage === true;
const gatewayCall = opts?.callGateway ?? callGateway;
const list = await gatewayCall<{ sessions: Array<SessionListRow>; path: string }>({
@@ -104,6 +114,11 @@ export function createSessionsListTool(opts?: {
params: {
limit,
activeMinutes,
label,
agentId,
search,
includeDerivedTitles,
includeLastMessage,
includeGlobal: !restrictToSpawned,
includeUnknown: !restrictToSpawned,
spawnedBy: restrictToSpawned ? effectiveRequesterKey : undefined,
@@ -215,6 +230,7 @@ export function createSessionsListTool(opts?: {
const row: SessionListRow = {
key: displayKey,
agentId: resolveAgentIdFromSessionKey(key),
kind,
channel: derivedChannel,
origin:
@@ -235,6 +251,8 @@ export function createSessionsListTool(opts?: {
: undefined,
label: readStringValue(entry.label),
displayName: readStringValue(entry.displayName),
derivedTitle: readStringValue(entry.derivedTitle),
lastMessagePreview: readStringValue(entry.lastMessagePreview),
parentSessionKey:
typeof entry.parentSessionKey === "string"
? resolveDisplaySessionKey({