Merge db88d3b463a7fa2a45943f6dfee03baae90fe214 into 598f1826d8b2bc969aace2c6459824737667218c
This commit is contained in:
commit
c4a0bfc5ef
@ -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<string, unknown> }> = [];
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as {
|
||||
method?: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -20,6 +20,7 @@ export const DEFAULT_TOOL_ALLOW = [
|
||||
"image",
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_manage",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"sessions_yield",
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
155
src/agents/tools/sessions-manage-tool.ts
Normal file
155
src/agents/tools/sessions-manage-tool.ts
Normal file
@ -0,0 +1,155 @@
|
||||
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),
|
||||
});
|
||||
|
||||
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<string, unknown>;
|
||||
const sessionKeyParam = readStringParam(params, "sessionKey", { required: true });
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
|
||||
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,
|
||||
});
|
||||
// TODO: add "manage" to SessionAccessAction for accurate error messages
|
||||
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 },
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user