mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 20:21:13 +00:00
refactor(plugin-sdk): share tool payload extraction
This commit is contained in:
@@ -1,42 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractToolPayload as extractSharedToolPayload } from "../../plugin-sdk/tool-payload.js";
|
||||
import { extractToolPayload } from "./tool-payload.js";
|
||||
|
||||
describe("extractToolPayload", () => {
|
||||
it("prefers explicit details payloads", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
details: { ok: true },
|
||||
content: [{ type: "text", text: '{"ignored":true}' }],
|
||||
} as never),
|
||||
).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("parses JSON text blocks from tool content", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content: [
|
||||
{ type: "image", url: "https://example.com/a.png" },
|
||||
{ type: "text", text: '{"ok":true,"count":2}' },
|
||||
],
|
||||
} as never),
|
||||
).toEqual({ ok: true, count: 2 });
|
||||
});
|
||||
|
||||
it("falls back to raw text, then content, then the whole result", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content: [{ type: "text", text: "not json" }],
|
||||
} as never),
|
||||
).toBe("not json");
|
||||
|
||||
const content = [{ type: "image", url: "https://example.com/a.png" }];
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content,
|
||||
} as never),
|
||||
).toBe(content);
|
||||
|
||||
const result = { status: "ok" };
|
||||
expect(extractToolPayload(result as never)).toBe(result);
|
||||
it("re-exports the shared plugin-sdk helper", () => {
|
||||
expect(extractToolPayload).toBe(extractSharedToolPayload);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
|
||||
export function extractToolPayload(result: AgentToolResult<unknown>): unknown {
|
||||
if (result.details !== undefined) {
|
||||
return result.details;
|
||||
}
|
||||
const textBlock = Array.isArray(result.content)
|
||||
? result.content.find(
|
||||
(block) =>
|
||||
block &&
|
||||
typeof block === "object" &&
|
||||
(block as { type?: unknown }).type === "text" &&
|
||||
typeof (block as { text?: unknown }).text === "string",
|
||||
)
|
||||
: undefined;
|
||||
const text = (textBlock as { text?: string } | undefined)?.text;
|
||||
if (text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return result.content ?? result;
|
||||
}
|
||||
export { extractToolPayload } from "../../plugin-sdk/tool-payload.js";
|
||||
|
||||
45
src/plugin-sdk/tool-payload.test.ts
Normal file
45
src/plugin-sdk/tool-payload.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractToolPayload } from "./tool-payload.js";
|
||||
|
||||
describe("extractToolPayload", () => {
|
||||
it("returns undefined for missing results", () => {
|
||||
expect(extractToolPayload(undefined)).toBeUndefined();
|
||||
expect(extractToolPayload(null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prefers explicit details payloads", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
details: { ok: true },
|
||||
content: [{ type: "text", text: '{"ignored":true}' }],
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("parses JSON text blocks and falls back to raw text, content, or the whole result", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content: [
|
||||
{ type: "image", url: "https://example.com/a.png" },
|
||||
{ type: "text", text: '{"ok":true,"count":2}' },
|
||||
],
|
||||
}),
|
||||
).toEqual({ ok: true, count: 2 });
|
||||
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content: [{ type: "text", text: "not json" }],
|
||||
}),
|
||||
).toBe("not json");
|
||||
|
||||
const content = [{ type: "image", url: "https://example.com/a.png" }];
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content,
|
||||
}),
|
||||
).toBe(content);
|
||||
|
||||
const result = { status: "ok" };
|
||||
expect(extractToolPayload(result)).toBe(result);
|
||||
});
|
||||
});
|
||||
43
src/plugin-sdk/tool-payload.ts
Normal file
43
src/plugin-sdk/tool-payload.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
type ToolPayloadTextBlock = {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type ToolPayloadCarrier = {
|
||||
details?: unknown;
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
function isToolPayloadTextBlock(block: unknown): block is ToolPayloadTextBlock {
|
||||
return (
|
||||
!!block &&
|
||||
typeof block === "object" &&
|
||||
(block as { type?: unknown }).type === "text" &&
|
||||
typeof (block as { text?: unknown }).text === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the most useful payload from tool result-like objects shared across
|
||||
* outbound core flows and bundled plugin helpers.
|
||||
*/
|
||||
export function extractToolPayload(result: ToolPayloadCarrier | null | undefined): unknown {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
if (result.details !== undefined) {
|
||||
return result.details;
|
||||
}
|
||||
const textBlock = Array.isArray(result.content)
|
||||
? result.content.find(isToolPayloadTextBlock)
|
||||
: undefined;
|
||||
const text = textBlock?.text;
|
||||
if (!text) {
|
||||
return result.content ?? result;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -394,6 +394,10 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"wrapExternalContent",
|
||||
],
|
||||
});
|
||||
expectSourceContract("tool-payload", {
|
||||
mentions: ["extractToolPayload", "ToolPayloadCarrier"],
|
||||
omits: ["createAnthropicToolPayloadCompatibilityWrapper", "extractToolSend"],
|
||||
});
|
||||
expectSourceMentions("compat", [
|
||||
"createPluginRuntimeStore",
|
||||
"createScopedChannelConfigAdapter",
|
||||
|
||||
Reference in New Issue
Block a user