refactor(plugin-sdk): share tool payload extraction

This commit is contained in:
Vincent Koc
2026-04-08 08:45:08 +01:00
parent a04b9a27fb
commit be530f085d
10 changed files with 104 additions and 92 deletions

View File

@@ -262,6 +262,7 @@ Current bundled provider examples:
| `plugin-sdk/request-url` | Request URL helpers | Extract string URLs from request-like inputs |
| `plugin-sdk/run-command` | Timed command helpers | Timed command runner with normalized stdout/stderr |
| `plugin-sdk/param-readers` | Param readers | Common tool/CLI param readers |
| `plugin-sdk/tool-payload` | Tool payload extraction | Extract normalized payloads from tool result objects |
| `plugin-sdk/tool-send` | Tool send extraction | Extract canonical send target fields from tool args |
| `plugin-sdk/temp-path` | Temp path helpers | Shared temp-download path helpers |
| `plugin-sdk/logging-core` | Logging helpers | Subsystem logger and redaction helpers |

View File

@@ -201,6 +201,7 @@ explicitly promotes one as public.
| `plugin-sdk/request-url` | Extract string URLs from fetch/request-like inputs |
| `plugin-sdk/run-command` | Timed command runner with normalized stdout/stderr results |
| `plugin-sdk/param-readers` | Common tool/CLI param readers |
| `plugin-sdk/tool-payload` | Extract normalized payloads from tool result objects |
| `plugin-sdk/tool-send` | Extract canonical send target fields from tool args |
| `plugin-sdk/temp-path` | Shared temp-download path helpers |
| `plugin-sdk/logging-core` | Subsystem logger and redaction helpers |

View File

@@ -1,31 +1 @@
type ResultWithDetails = {
details?: unknown;
content?: unknown;
};
export function extractQaToolPayload(result: ResultWithDetails | null | undefined): unknown {
if (!result) {
return undefined;
}
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) {
return result.content ?? result;
}
try {
return JSON.parse(text);
} catch {
return text;
}
}
export { extractToolPayload as extractQaToolPayload } from "openclaw/plugin-sdk/tool-payload";

View File

@@ -1005,6 +1005,10 @@
"types": "./dist/plugin-sdk/tlon.d.ts",
"default": "./dist/plugin-sdk/tlon.js"
},
"./plugin-sdk/tool-payload": {
"types": "./dist/plugin-sdk/tool-payload.d.ts",
"default": "./dist/plugin-sdk/tool-payload.js"
},
"./plugin-sdk/tool-send": {
"types": "./dist/plugin-sdk/tool-send.d.ts",
"default": "./dist/plugin-sdk/tool-send.js"

View File

@@ -240,6 +240,7 @@
"telegram-command-config",
"thread-ownership",
"tlon",
"tool-payload",
"tool-send",
"twitch",
"webhook-ingress",

View File

@@ -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);
});
});

View File

@@ -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";

View 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);
});
});

View 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;
}
}

View File

@@ -394,6 +394,10 @@ describe("plugin-sdk subpath exports", () => {
"wrapExternalContent",
],
});
expectSourceContract("tool-payload", {
mentions: ["extractToolPayload", "ToolPayloadCarrier"],
omits: ["createAnthropicToolPayloadCompatibilityWrapper", "extractToolSend"],
});
expectSourceMentions("compat", [
"createPluginRuntimeStore",
"createScopedChannelConfigAdapter",