mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 19:28:11 +00:00
feat: add sessions_yield tool for cooperative turn-ending (#36537)
Merged via squash.
Prepared head SHA: 75d9204c86
Co-authored-by: jriff <50276+jriff@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
45
src/agents/tools/sessions-yield-tool.test.ts
Normal file
45
src/agents/tools/sessions-yield-tool.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createSessionsYieldTool } from "./sessions-yield-tool.js";
|
||||
|
||||
describe("sessions_yield tool", () => {
|
||||
it("returns error when no sessionId is provided", async () => {
|
||||
const onYield = vi.fn();
|
||||
const tool = createSessionsYieldTool({ onYield });
|
||||
const result = await tool.execute("call-1", {});
|
||||
expect(result.details).toMatchObject({
|
||||
status: "error",
|
||||
error: "No session context",
|
||||
});
|
||||
expect(onYield).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes onYield callback with default message", async () => {
|
||||
const onYield = vi.fn();
|
||||
const tool = createSessionsYieldTool({ sessionId: "test-session", onYield });
|
||||
const result = await tool.execute("call-1", {});
|
||||
expect(result.details).toMatchObject({ status: "yielded", message: "Turn yielded." });
|
||||
expect(onYield).toHaveBeenCalledOnce();
|
||||
expect(onYield).toHaveBeenCalledWith("Turn yielded.");
|
||||
});
|
||||
|
||||
it("passes the custom message through the yield callback", async () => {
|
||||
const onYield = vi.fn();
|
||||
const tool = createSessionsYieldTool({ sessionId: "test-session", onYield });
|
||||
const result = await tool.execute("call-1", { message: "Waiting for fact-checker" });
|
||||
expect(result.details).toMatchObject({
|
||||
status: "yielded",
|
||||
message: "Waiting for fact-checker",
|
||||
});
|
||||
expect(onYield).toHaveBeenCalledOnce();
|
||||
expect(onYield).toHaveBeenCalledWith("Waiting for fact-checker");
|
||||
});
|
||||
|
||||
it("returns error without onYield callback", async () => {
|
||||
const tool = createSessionsYieldTool({ sessionId: "test-session" });
|
||||
const result = await tool.execute("call-1", {});
|
||||
expect(result.details).toMatchObject({
|
||||
status: "error",
|
||||
error: "Yield not supported in this context",
|
||||
});
|
||||
});
|
||||
});
|
||||
32
src/agents/tools/sessions-yield-tool.ts
Normal file
32
src/agents/tools/sessions-yield-tool.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readStringParam } from "./common.js";
|
||||
|
||||
const SessionsYieldToolSchema = Type.Object({
|
||||
message: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export function createSessionsYieldTool(opts?: {
|
||||
sessionId?: string;
|
||||
onYield?: (message: string) => Promise<void> | void;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Yield",
|
||||
name: "sessions_yield",
|
||||
description:
|
||||
"End your current turn. Use after spawning subagents to receive their results as the next message.",
|
||||
parameters: SessionsYieldToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
const message = readStringParam(params, "message") || "Turn yielded.";
|
||||
if (!opts?.sessionId) {
|
||||
return jsonResult({ status: "error", error: "No session context" });
|
||||
}
|
||||
if (!opts?.onYield) {
|
||||
return jsonResult({ status: "error", error: "Yield not supported in this context" });
|
||||
}
|
||||
await opts.onYield(message);
|
||||
return jsonResult({ status: "yielded", message });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user