From 1a4c32e366629adb508a8593bb404401243a9fa5 Mon Sep 17 00:00:00 2001 From: dangoZhang <253027408+dangoZhang@users.noreply.github.com> Date: Wed, 22 Apr 2026 05:11:28 +0800 Subject: [PATCH] feat: expose mailbox session discovery in sessions_list --- docs/concepts/session-tool.md | 10 +++-- src/agents/openclaw-tools.sessions.test.ts | 43 +++++++++++++++++++++- src/agents/tool-description-presets.ts | 4 +- src/agents/tool-display-config.ts | 12 +++++- src/agents/tools/sessions-helpers.ts | 3 ++ src/agents/tools/sessions-list-tool.ts | 20 +++++++++- 6 files changed, 82 insertions(+), 10 deletions(-) diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index 2c019c7f0ee..4e2f8166fe1 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -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. diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 90ab5e9e968..9a49d558390 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -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"); diff --git a/src/agents/tool-description-presets.ts b/src/agents/tool-description-presets.ts index 96dec61ad4e..60f4a27c0e5 100644 --- a/src/agents/tool-description-presets.ts +++ b/src/agents/tool-description-presets.ts @@ -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(" "); } diff --git a/src/agents/tool-display-config.ts b/src/agents/tool-display-config.ts index 16f49d16257..844faf45e78 100644 --- a/src/agents/tool-display-config.ts +++ b/src/agents/tool-display-config.ts @@ -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: "📨", diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index 372dcad20c7..09116686a09 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -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; diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index cf1aa2285cf..36d0d285890 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -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; 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({