From ef7ce1822db71a50923fc7a13b25624566683c45 Mon Sep 17 00:00:00 2001 From: Kaspre Date: Fri, 20 Mar 2026 23:58:50 -0400 Subject: [PATCH 1/2] feat(tools): add sessions_manage tool for programmatic compact/reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents cannot programmatically compact or reset sessions — /compact and /new are user-only slash commands. This creates an operational gap for autonomous workflows (task-runners, orchestrators, heartbeat agents) that need to manage session context without human intervention. Add sessions_manage tool that exposes sessions.compact and sessions.reset gateway RPC methods as an agent tool, using the same session resolution, visibility enforcement, and agent-to-agent policy checks as the existing sessions_send tool. Addresses #10981. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/agents/openclaw-tools.sessions.test.ts | 58 +++++++ src/agents/openclaw-tools.ts | 6 + ...e-aliases-schemas-without-dropping.test.ts | 2 + src/agents/pi-tools.policy.ts | 2 + src/agents/sandbox/constants.ts | 1 + src/agents/system-prompt.ts | 3 + src/agents/tool-catalog.ts | 8 + src/agents/tool-display-overrides.json | 5 + src/agents/tools/sessions-manage-tool.ts | 159 ++++++++++++++++++ 9 files changed, 244 insertions(+) create mode 100644 src/agents/tools/sessions-manage-tool.ts diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 90f991b4484..0868cd1713a 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -87,6 +87,7 @@ 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_manage", "action").type).toBe("string"); expect(schemaProp("sessions_send", "timeoutSeconds").type).toBe("number"); expect(schemaProp("sessions_spawn", "thinking").type).toBe("string"); expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe("number"); @@ -669,6 +670,63 @@ describe("sessions tools", () => { expect(sendCallCount).toBe(0); }); + it("sessions_manage compacts and resets via gateway methods", async () => { + callGatewayMock.mockReset(); + const calls: Array<{ method?: string; params?: Record }> = []; + callGatewayMock.mockImplementation(async (opts: unknown) => { + const request = opts as { + method?: string; + params?: Record; + }; + calls.push(request); + if (request.method === "sessions.resolve") { + return { key: "main" }; + } + if (request.method === "sessions.compact") { + return { ok: true, compacted: true }; + } + if (request.method === "sessions.reset") { + return { ok: true, key: "main", entry: { key: "main", sessionId: "new-sess" } }; + } + return {}; + }); + + const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_manage"); + expect(tool).toBeDefined(); + if (!tool) { + throw new Error("missing sessions_manage tool"); + } + + const compactResult = await tool.execute("call-manage-1", { + sessionKey: "main", + action: "compact", + }); + expect(compactResult.details).toMatchObject({ + status: "ok", + action: "compact", + sessionKey: "main", + compacted: true, + }); + + const resetResult = await tool.execute("call-manage-2", { + sessionKey: "main", + action: "reset", + }); + expect(resetResult.details).toMatchObject({ + status: "ok", + action: "reset", + sessionKey: "main", + resetOk: true, + }); + + expect( + calls.some((call) => call.method === "sessions.compact" && call.params?.key === "main"), + ).toBe(true); + expect( + calls.some((call) => call.method === "sessions.reset" && call.params?.key === "main"), + ).toBe(true); + }); + it("sessions_send resolves sessionId inputs", async () => { const sessionId = "sess-send"; const targetKey = "agent:main:discord:channel:123"; diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index de5e91fdf0c..6b2d3b7148d 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -20,6 +20,7 @@ import { createPdfTool } from "./tools/pdf-tool.js"; import { createSessionStatusTool } from "./tools/session-status-tool.js"; import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js"; import { createSessionsListTool } from "./tools/sessions-list-tool.js"; +import { createSessionsManageTool } from "./tools/sessions-manage-tool.js"; import { createSessionsSendTool } from "./tools/sessions-send-tool.js"; import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js"; import { createSessionsYieldTool } from "./tools/sessions-yield-tool.js"; @@ -197,6 +198,11 @@ export function createOpenClawTools( sandboxed: options?.sandboxed, config: options?.config, }), + createSessionsManageTool({ + agentSessionKey: options?.agentSessionKey, + sandboxed: options?.sandboxed, + config: options?.config, + }), createSessionsYieldTool({ sessionId: options?.sessionId, onYield: options?.onYield, diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts index c704515ac6e..eec4977543a 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts +++ b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts @@ -305,6 +305,7 @@ describe("createOpenClawCodingTools", () => { "sessions_list", "sessions_history", "sessions_send", + "sessions_manage", "sessions_spawn", "subagents", "session_status", @@ -328,6 +329,7 @@ describe("createOpenClawCodingTools", () => { expect(names.has("sessions_list")).toBe(false); expect(names.has("sessions_history")).toBe(false); expect(names.has("sessions_send")).toBe(false); + expect(names.has("sessions_manage")).toBe(false); expect(names.has("sessions_spawn")).toBe(false); // Explicit subagent orchestration tool remains available (list/steer/kill with safeguards). expect(names.has("subagents")).toBe(true); diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 4e7cea7c94e..30d3f58aa32 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -35,6 +35,8 @@ const SUBAGENT_TOOL_DENY_ALWAYS = [ "memory_get", // Direct session sends - subagents communicate through announce chain "sessions_send", + // Session management - subagents should not compact/reset sessions + "sessions_manage", ]; /** diff --git a/src/agents/sandbox/constants.ts b/src/agents/sandbox/constants.ts index 80915b3bfce..dd65a1fdcb1 100644 --- a/src/agents/sandbox/constants.ts +++ b/src/agents/sandbox/constants.ts @@ -20,6 +20,7 @@ export const DEFAULT_TOOL_ALLOW = [ "image", "sessions_list", "sessions_history", + "sessions_manage", "sessions_send", "sessions_spawn", "sessions_yield", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 3ee438db2d4..f884678a0fa 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -261,6 +261,7 @@ export function buildAgentSystemPrompt(params: { sessions_list: "List other sessions (incl. sub-agents) with filters/last", sessions_history: "Fetch history for another session/sub-agent", sessions_send: "Send a message to another session/sub-agent", + sessions_manage: "Compact or reset a session (programmatic /compact and /new)", sessions_spawn: acpSpawnRuntimeEnabled ? 'Spawn an isolated sub-agent or ACP coding session (runtime="acp" requires `agentId` unless `acp.defaultAgent` is configured; ACP harness ids follow acp.allowedAgents, not agents_list)' : "Spawn an isolated sub-agent session", @@ -293,6 +294,7 @@ export function buildAgentSystemPrompt(params: { "sessions_list", "sessions_history", "sessions_send", + "sessions_manage", "subagents", "session_status", "image", @@ -443,6 +445,7 @@ export function buildAgentSystemPrompt(params: { "- sessions_list: list sessions", "- sessions_history: fetch session history", "- sessions_send: send to another session", + "- sessions_manage: compact or reset a session", "- subagents: list/steer/kill sub-agent runs", '- session_status: show usage/time/model state and answer "what model are we using?"', ].join("\n"), diff --git a/src/agents/tool-catalog.ts b/src/agents/tool-catalog.ts index 0d58c066928..e01f0d8f5c6 100644 --- a/src/agents/tool-catalog.ts +++ b/src/agents/tool-catalog.ts @@ -137,6 +137,14 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [ profiles: ["coding", "messaging"], includeInOpenClawGroup: true, }, + { + id: "sessions_manage", + label: "sessions_manage", + description: "Compact/reset session", + sectionId: "sessions", + profiles: ["coding", "messaging"], + includeInOpenClawGroup: true, + }, { id: "sessions_spawn", label: "sessions_spawn", diff --git a/src/agents/tool-display-overrides.json b/src/agents/tool-display-overrides.json index 590485404ff..34712a9a655 100644 --- a/src/agents/tool-display-overrides.json +++ b/src/agents/tool-display-overrides.json @@ -31,6 +31,11 @@ "title": "Session Send", "detailKeys": ["label", "sessionKey", "agentId", "timeoutSeconds"] }, + "sessions_manage": { + "emoji": "🧰", + "title": "Session Manage", + "detailKeys": ["sessionKey", "action"] + }, "sessions_history": { "emoji": "🧾", "title": "Session History", diff --git a/src/agents/tools/sessions-manage-tool.ts b/src/agents/tools/sessions-manage-tool.ts new file mode 100644 index 00000000000..7a849222e78 --- /dev/null +++ b/src/agents/tools/sessions-manage-tool.ts @@ -0,0 +1,159 @@ +import { Type } from "@sinclair/typebox"; +import type { OpenClawConfig } from "../../config/config.js"; +import { callGateway } from "../../gateway/call.js"; +import { stringEnum } from "../schema/typebox.js"; +import type { AnyAgentTool } from "./common.js"; +import { jsonResult, readStringParam } from "./common.js"; +import { + createAgentToAgentPolicy, + createSessionVisibilityGuard, + resolveEffectiveSessionToolsVisibility, + resolveSessionReference, + resolveSessionToolContext, + resolveVisibleSessionReference, +} from "./sessions-helpers.js"; + +const SESSIONS_MANAGE_ACTIONS = ["compact", "reset"] as const; + +const SessionsManageToolSchema = Type.Object({ + sessionKey: Type.String(), + action: stringEnum(SESSIONS_MANAGE_ACTIONS), + instructions: Type.Optional(Type.String()), +}); + +export function createSessionsManageTool(opts?: { + agentSessionKey?: string; + sandboxed?: boolean; + config?: OpenClawConfig; +}): AnyAgentTool { + return { + label: "Session Manage", + name: "sessions_manage", + description: + "Compact or reset a session by key. Use action 'compact' to compress context or 'reset' to start fresh.", + parameters: SessionsManageToolSchema, + execute: async (_toolCallId, args) => { + const params = args as Record; + const sessionKeyParam = readStringParam(params, "sessionKey", { required: true }); + const action = readStringParam(params, "action", { required: true }); + const instructions = readStringParam(params, "instructions"); + + if (!SESSIONS_MANAGE_ACTIONS.includes(action as (typeof SESSIONS_MANAGE_ACTIONS)[number])) { + return jsonResult({ status: "error", error: "action must be 'compact' or 'reset'" }); + } + + const { cfg, mainKey, alias, effectiveRequesterKey, restrictToSpawned } = + resolveSessionToolContext(opts); + + // Resolve and validate the target session, same pattern as sessions_send. + const resolvedSession = await resolveSessionReference({ + sessionKey: sessionKeyParam, + alias, + mainKey, + requesterInternalKey: effectiveRequesterKey, + restrictToSpawned, + }); + if (!resolvedSession.ok) { + return jsonResult({ status: resolvedSession.status, error: resolvedSession.error }); + } + + const visibleSession = await resolveVisibleSessionReference({ + resolvedSession, + requesterSessionKey: effectiveRequesterKey, + restrictToSpawned, + visibilitySessionKey: sessionKeyParam, + }); + if (!visibleSession.ok) { + return jsonResult({ + status: visibleSession.status, + error: visibleSession.error, + sessionKey: visibleSession.displayKey, + }); + } + + const resolvedKey = visibleSession.key; + const displayKey = visibleSession.displayKey; + + // Enforce agent-to-agent and visibility policy. + const a2aPolicy = createAgentToAgentPolicy(cfg); + const sessionVisibility = resolveEffectiveSessionToolsVisibility({ + cfg, + sandboxed: opts?.sandboxed === true, + }); + const visibilityGuard = await createSessionVisibilityGuard({ + action: "send", + requesterSessionKey: effectiveRequesterKey, + visibility: sessionVisibility, + a2aPolicy, + }); + const access = visibilityGuard.check(resolvedKey); + if (!access.allowed) { + return jsonResult({ + status: access.status, + error: access.error, + sessionKey: displayKey, + }); + } + + // Execute the requested action. + if (action === "compact") { + try { + const result = await callGateway<{ + compacted?: boolean; + reason?: string; + kept?: number; + }>({ + method: "sessions.compact", + params: { + key: resolvedKey, + ...(instructions ? { instructions } : {}), + }, + }); + return jsonResult({ + status: "ok", + action, + sessionKey: displayKey, + compacted: result?.compacted === true, + ...(typeof result?.reason === "string" ? { reason: result.reason } : {}), + ...(typeof result?.kept === "number" ? { kept: result.kept } : {}), + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return jsonResult({ + status: "error", + action, + sessionKey: displayKey, + error: msg, + }); + } + } + + // action === "reset" + try { + const result = await callGateway<{ + ok?: boolean; + key?: string; + entry?: unknown; + }>({ + method: "sessions.reset", + params: { key: resolvedKey }, + }); + return jsonResult({ + status: "ok", + action, + sessionKey: displayKey, + resetOk: result?.ok === true, + ...(typeof result?.key === "string" ? { newKey: result.key } : {}), + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return jsonResult({ + status: "error", + action, + sessionKey: displayKey, + error: msg, + }); + } + }, + }; +} From db88d3b463a7fa2a45943f6dfee03baae90fe214 Mon Sep 17 00:00:00 2001 From: Kaspre Date: Sat, 21 Mar 2026 00:28:04 -0400 Subject: [PATCH 2/2] fix: address review feedback on sessions_manage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove `instructions` parameter — sessions.compact gateway schema only accepts `key` and `maxLines` (additionalProperties: false), so passing instructions would be rejected at runtime - Add TODO for SessionAccessAction "manage" variant (currently uses "send" for visibility guard error messages) - Classify sessions_manage as mutating tool in tool-mutation.ts to prevent silent error suppression Co-Authored-By: Claude Opus 4.6 (1M context) --- src/agents/tool-mutation.ts | 2 ++ src/agents/tools/sessions-manage-tool.ts | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/agents/tool-mutation.ts b/src/agents/tool-mutation.ts index a88bbadfd21..6b67314032c 100644 --- a/src/agents/tool-mutation.ts +++ b/src/agents/tool-mutation.ts @@ -7,6 +7,7 @@ const MUTATING_TOOL_NAMES = new Set([ "process", "message", "sessions_send", + "sessions_manage", "cron", "gateway", "canvas", @@ -107,6 +108,7 @@ export function isMutatingToolCall(toolName: string, args: unknown): boolean { case "exec": case "bash": case "sessions_send": + case "sessions_manage": return true; case "process": return action != null && PROCESS_MUTATING_ACTIONS.has(action); diff --git a/src/agents/tools/sessions-manage-tool.ts b/src/agents/tools/sessions-manage-tool.ts index 7a849222e78..bff37a2e02d 100644 --- a/src/agents/tools/sessions-manage-tool.ts +++ b/src/agents/tools/sessions-manage-tool.ts @@ -18,7 +18,6 @@ const SESSIONS_MANAGE_ACTIONS = ["compact", "reset"] as const; const SessionsManageToolSchema = Type.Object({ sessionKey: Type.String(), action: stringEnum(SESSIONS_MANAGE_ACTIONS), - instructions: Type.Optional(Type.String()), }); export function createSessionsManageTool(opts?: { @@ -36,7 +35,6 @@ export function createSessionsManageTool(opts?: { const params = args as Record; const sessionKeyParam = readStringParam(params, "sessionKey", { required: true }); const action = readStringParam(params, "action", { required: true }); - const instructions = readStringParam(params, "instructions"); if (!SESSIONS_MANAGE_ACTIONS.includes(action as (typeof SESSIONS_MANAGE_ACTIONS)[number])) { return jsonResult({ status: "error", error: "action must be 'compact' or 'reset'" }); @@ -80,6 +78,7 @@ export function createSessionsManageTool(opts?: { cfg, sandboxed: opts?.sandboxed === true, }); + // TODO: add "manage" to SessionAccessAction for accurate error messages const visibilityGuard = await createSessionVisibilityGuard({ action: "send", requesterSessionKey: effectiveRequesterKey, @@ -104,10 +103,7 @@ export function createSessionsManageTool(opts?: { kept?: number; }>({ method: "sessions.compact", - params: { - key: resolvedKey, - ...(instructions ? { instructions } : {}), - }, + params: { key: resolvedKey }, }); return jsonResult({ status: "ok",