@downcity/agent 1.1.153 → 1.1.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
- package/bin/agent/local/services/AgentAssemblyService.js +1 -0
- package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts.map +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.js +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts +5 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.js +26 -0
- package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolDefinition.d.ts +11 -0
- package/bin/executor/tools/plugin/PluginToolDefinition.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolDefinition.js +12 -3
- package/bin/executor/tools/plugin/PluginToolDefinition.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolSchemas.d.ts +4 -0
- package/bin/executor/tools/plugin/PluginToolSchemas.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolSchemas.js +11 -1
- package/bin/executor/tools/plugin/PluginToolSchemas.js.map +1 -1
- package/bin/executor/tools/plugin/types/PluginTool.d.ts +20 -0
- package/bin/executor/tools/plugin/types/PluginTool.d.ts.map +1 -1
- package/bin/index.d.ts +3 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -0
- package/bin/index.js.map +1 -1
- package/bin/plugin/core/PluginActionFactory.d.ts +99 -0
- package/bin/plugin/core/PluginActionFactory.d.ts.map +1 -0
- package/bin/plugin/core/PluginActionFactory.js +59 -0
- package/bin/plugin/core/PluginActionFactory.js.map +1 -0
- package/bin/plugin/core/PluginActionRunner.d.ts.map +1 -1
- package/bin/plugin/core/PluginActionRunner.js +14 -1
- package/bin/plugin/core/PluginActionRunner.js.map +1 -1
- package/bin/plugin/core/PluginLocalExecution.d.ts.map +1 -1
- package/bin/plugin/core/PluginLocalExecution.js +15 -1
- package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
- package/bin/plugin/core/PluginRegistry.d.ts +18 -1
- package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
- package/bin/plugin/core/PluginRegistry.js +74 -1
- package/bin/plugin/core/PluginRegistry.js.map +1 -1
- package/bin/plugin/types/Plugin.d.ts +2 -2
- package/bin/plugin/types/Plugin.d.ts.map +1 -1
- package/bin/types/plugin/PluginAction.d.ts +40 -2
- package/bin/types/plugin/PluginAction.d.ts.map +1 -1
- package/bin/types/plugin/PluginRuntime.d.ts +44 -0
- package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/image-plugin-job.test.mjs +209 -68
- package/scripts/plugin-tool-bridge.test.mjs +113 -2
- package/src/agent/local/services/AgentAssemblyService.ts +1 -0
- package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
- package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +3 -2
- package/src/executor/tools/plugin/PluginToolBridge.ts +30 -0
- package/src/executor/tools/plugin/PluginToolDefinition.ts +25 -4
- package/src/executor/tools/plugin/PluginToolSchemas.ts +12 -1
- package/src/executor/tools/plugin/types/PluginTool.ts +22 -0
- package/src/index.ts +11 -0
- package/src/plugin/core/PluginActionFactory.ts +185 -0
- package/src/plugin/core/PluginActionRunner.ts +14 -1
- package/src/plugin/core/PluginLocalExecution.ts +15 -1
- package/src/plugin/core/PluginRegistry.ts +89 -1
- package/src/plugin/types/Plugin.ts +5 -0
- package/src/types/plugin/PluginAction.ts +43 -2
- package/src/types/plugin/PluginRuntime.ts +44 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 ImagePlugin
|
|
2
|
+
* @file 验证 ImagePlugin 的两步式图片任务协议。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* -
|
|
6
|
-
* - image_result
|
|
7
|
-
* -
|
|
5
|
+
* - 插件只暴露 image_create / image_result 两个任务 action。
|
|
6
|
+
* - image_result 只读取一次当前状态,不在 plugin 层等待终态。
|
|
7
|
+
* - 成功图片仍以 UIMessage 返回,后续由 plugin bridge 落盘 file parts。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
12
15
|
|
|
13
16
|
import { ImagePlugin } from "../../plugins/bin/index.js";
|
|
17
|
+
import { createAgentPluginRegistry } from "../bin/agent/local/AgentPluginFactory.js";
|
|
14
18
|
|
|
15
19
|
function create_image_message() {
|
|
16
20
|
return {
|
|
@@ -27,38 +31,27 @@ function create_image_message() {
|
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
image_result: (input) => {
|
|
40
|
-
calls.push(["result", input.job_id]);
|
|
41
|
-
return calls.length === 2
|
|
42
|
-
? { job_id: input.job_id, status: "running", poll_after_ms: 1 }
|
|
43
|
-
: { job_id: input.job_id, status: "succeeded", result: message, poll_after_ms: 1 };
|
|
44
|
-
},
|
|
34
|
+
function create_context(rootPath = process.cwd()) {
|
|
35
|
+
return { rootPath };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function create_registry(plugin, rootPath = process.cwd()) {
|
|
39
|
+
return createAgentPluginRegistry({
|
|
40
|
+
plugins: [plugin],
|
|
41
|
+
get_context: () => create_context(rootPath),
|
|
45
42
|
});
|
|
43
|
+
}
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
actionName: "generate",
|
|
45
|
+
test("ImagePlugin exposes only job-style image actions", async () => {
|
|
46
|
+
const plugin = new ImagePlugin({
|
|
47
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
48
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
52
49
|
});
|
|
53
50
|
|
|
54
|
-
assert.equal(
|
|
55
|
-
assert.equal(
|
|
56
|
-
assert.equal(
|
|
57
|
-
assert.
|
|
58
|
-
["create", "draw"],
|
|
59
|
-
["result", "img_1"],
|
|
60
|
-
["result", "img_1"],
|
|
61
|
-
]);
|
|
51
|
+
assert.equal("image_create" in plugin.actions, true);
|
|
52
|
+
assert.equal("image_result" in plugin.actions, true);
|
|
53
|
+
assert.equal("models" in plugin.actions, true);
|
|
54
|
+
assert.equal("generate" in plugin.actions, false);
|
|
62
55
|
});
|
|
63
56
|
|
|
64
57
|
test("ImagePlugin image_create returns image job", async () => {
|
|
@@ -73,7 +66,7 @@ test("ImagePlugin image_create returns image job", async () => {
|
|
|
73
66
|
});
|
|
74
67
|
|
|
75
68
|
const result = await plugin.actions.image_create.execute({
|
|
76
|
-
context:
|
|
69
|
+
context: create_context(),
|
|
77
70
|
payload: { prompt: "draw" },
|
|
78
71
|
pluginName: "image",
|
|
79
72
|
actionName: "image_create",
|
|
@@ -110,7 +103,7 @@ test("ImagePlugin models lists image-capable models", async () => {
|
|
|
110
103
|
});
|
|
111
104
|
|
|
112
105
|
const result = await plugin.actions.models.execute({
|
|
113
|
-
context:
|
|
106
|
+
context: create_context(),
|
|
114
107
|
payload: {},
|
|
115
108
|
pluginName: "image",
|
|
116
109
|
actionName: "models",
|
|
@@ -122,60 +115,81 @@ test("ImagePlugin models lists image-capable models", async () => {
|
|
|
122
115
|
assert.equal(result.data.items[0].meta.provider, "test");
|
|
123
116
|
});
|
|
124
117
|
|
|
125
|
-
test("ImagePlugin
|
|
118
|
+
test("ImagePlugin exposes action metadata through plugin registry", async () => {
|
|
119
|
+
const plugin = new ImagePlugin({
|
|
120
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
121
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
122
|
+
});
|
|
123
|
+
const registry = create_registry(plugin);
|
|
124
|
+
|
|
125
|
+
const metadata = registry.read({
|
|
126
|
+
plugin: "image",
|
|
127
|
+
action: "image_create",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
assert.equal(metadata.name, "image");
|
|
131
|
+
assert.equal(metadata.actions.length, 1);
|
|
132
|
+
assert.equal(metadata.actions[0].name, "image_create");
|
|
133
|
+
assert.equal(metadata.actions[0].has_input_schema, true);
|
|
134
|
+
assert.match(metadata.actions[0].description, /Create an async image job/);
|
|
135
|
+
assert.equal(metadata.actions[0].examples[0].payload.prompt.includes("rainy city"), true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("ImagePlugin image_result reads pending state once", async () => {
|
|
126
139
|
const calls = [];
|
|
127
|
-
const message = create_image_message();
|
|
128
140
|
const plugin = new ImagePlugin({
|
|
129
|
-
min_poll_interval_ms: 1,
|
|
130
141
|
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
131
142
|
image_result: (input) => {
|
|
132
143
|
calls.push(input.job_id);
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
return {
|
|
145
|
+
job_id: input.job_id,
|
|
146
|
+
status: "running",
|
|
147
|
+
message: "upstream pending",
|
|
148
|
+
poll_after_ms: 1,
|
|
149
|
+
};
|
|
136
150
|
},
|
|
137
151
|
});
|
|
138
152
|
|
|
139
153
|
const result = await plugin.actions.image_result.execute({
|
|
140
|
-
context:
|
|
154
|
+
context: create_context(),
|
|
141
155
|
payload: { job_id: "img_1" },
|
|
142
156
|
pluginName: "image",
|
|
143
157
|
actionName: "image_result",
|
|
144
158
|
});
|
|
145
159
|
|
|
146
160
|
assert.equal(result.success, true);
|
|
147
|
-
assert.equal(result.data.
|
|
148
|
-
assert.equal(result.data.
|
|
149
|
-
assert.
|
|
161
|
+
assert.equal(result.data.job_id, "img_1");
|
|
162
|
+
assert.equal(result.data.status, "running");
|
|
163
|
+
assert.equal(result.data.message, "upstream pending");
|
|
164
|
+
assert.deepEqual(calls, ["img_1"]);
|
|
150
165
|
});
|
|
151
166
|
|
|
152
|
-
test("ImagePlugin image_result
|
|
153
|
-
const
|
|
167
|
+
test("ImagePlugin image_result returns final message when succeeded", async () => {
|
|
168
|
+
const message = create_image_message();
|
|
154
169
|
const plugin = new ImagePlugin({
|
|
155
|
-
min_poll_interval_ms: 1,
|
|
156
170
|
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
157
|
-
image_result: (input) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
image_result: (input) => ({
|
|
172
|
+
job_id: input.job_id,
|
|
173
|
+
status: "succeeded",
|
|
174
|
+
result: message,
|
|
175
|
+
poll_after_ms: 1,
|
|
176
|
+
}),
|
|
161
177
|
});
|
|
162
178
|
|
|
163
179
|
const result = await plugin.actions.image_result.execute({
|
|
164
|
-
context:
|
|
165
|
-
payload: { job_id: "img_1"
|
|
180
|
+
context: create_context(),
|
|
181
|
+
payload: { job_id: "img_1" },
|
|
166
182
|
pluginName: "image",
|
|
167
183
|
actionName: "image_result",
|
|
168
184
|
});
|
|
169
185
|
|
|
170
186
|
assert.equal(result.success, true);
|
|
171
|
-
assert.equal(result.data.
|
|
172
|
-
assert.equal(result.data.
|
|
173
|
-
assert.deepEqual(calls, ["img_1"]);
|
|
187
|
+
assert.equal(result.data.role, "assistant");
|
|
188
|
+
assert.equal(result.data.parts[0].type, "file");
|
|
174
189
|
});
|
|
175
190
|
|
|
176
|
-
test("ImagePlugin image_result reports failed terminal job
|
|
191
|
+
test("ImagePlugin image_result reports failed terminal job", async () => {
|
|
177
192
|
const plugin = new ImagePlugin({
|
|
178
|
-
min_poll_interval_ms: 1,
|
|
179
193
|
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
180
194
|
image_result: (input) => ({
|
|
181
195
|
job_id: input.job_id,
|
|
@@ -186,7 +200,7 @@ test("ImagePlugin image_result reports failed terminal job by default", async ()
|
|
|
186
200
|
});
|
|
187
201
|
|
|
188
202
|
const result = await plugin.actions.image_result.execute({
|
|
189
|
-
context:
|
|
203
|
+
context: create_context(),
|
|
190
204
|
payload: { job_id: "img_1" },
|
|
191
205
|
pluginName: "image",
|
|
192
206
|
actionName: "image_result",
|
|
@@ -194,22 +208,149 @@ test("ImagePlugin image_result reports failed terminal job by default", async ()
|
|
|
194
208
|
|
|
195
209
|
assert.equal(result.success, false);
|
|
196
210
|
assert.match(result.error, /provider failed/);
|
|
211
|
+
assert.equal(result.data.job_id, "img_1");
|
|
197
212
|
});
|
|
198
213
|
|
|
199
|
-
test("ImagePlugin
|
|
214
|
+
test("ImagePlugin image_result payload is schema validated by registry", async () => {
|
|
200
215
|
const plugin = new ImagePlugin({
|
|
201
|
-
min_poll_interval_ms: 1,
|
|
202
216
|
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
203
|
-
image_result: () => ({ job_id: "img_1", status: "
|
|
217
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
204
218
|
});
|
|
219
|
+
const registry = create_registry(plugin);
|
|
205
220
|
|
|
206
|
-
const result = await
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
actionName: "generate",
|
|
221
|
+
const result = await registry.runAction({
|
|
222
|
+
plugin: "image",
|
|
223
|
+
action: "image_result",
|
|
224
|
+
payload: {},
|
|
211
225
|
});
|
|
212
226
|
|
|
213
227
|
assert.equal(result.success, false);
|
|
214
|
-
assert.match(result.error, /
|
|
228
|
+
assert.match(result.error, /Invalid payload/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("ImagePlugin image_create converts local content image paths", async () => {
|
|
232
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-image-plugin-content-"));
|
|
233
|
+
try {
|
|
234
|
+
await fs.writeFile(path.join(tempDir, "input.png"), Buffer.from("png"));
|
|
235
|
+
let captured_input;
|
|
236
|
+
const plugin = new ImagePlugin({
|
|
237
|
+
image_create: (input) => {
|
|
238
|
+
captured_input = input;
|
|
239
|
+
return { job_id: "img_1", status: "queued", poll_after_ms: 1 };
|
|
240
|
+
},
|
|
241
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const result = await plugin.actions.image_create.execute({
|
|
245
|
+
context: create_context(tempDir),
|
|
246
|
+
payload: {
|
|
247
|
+
content: [
|
|
248
|
+
{ type: "text", text: "change background" },
|
|
249
|
+
{ type: "image", url: "./input.png" },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
pluginName: "image",
|
|
253
|
+
actionName: "image_create",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
assert.equal(result.success, true);
|
|
257
|
+
assert.equal(captured_input.messages[0].content[0].text, "change background");
|
|
258
|
+
assert.match(captured_input.messages[0].content[1].data_url, /^data:image\/png;base64,/);
|
|
259
|
+
assert.equal(captured_input.messages[0].content[1].media_type, "image/png");
|
|
260
|
+
assert.equal("content" in captured_input, false);
|
|
261
|
+
assert.equal("prompt" in captured_input, false);
|
|
262
|
+
} finally {
|
|
263
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("ImagePlugin image_create uses content instead of prompt when both exist", async () => {
|
|
268
|
+
let captured_input;
|
|
269
|
+
const plugin = new ImagePlugin({
|
|
270
|
+
image_create: (input) => {
|
|
271
|
+
captured_input = input;
|
|
272
|
+
return { job_id: "img_1", status: "queued", poll_after_ms: 1 };
|
|
273
|
+
},
|
|
274
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const result = await plugin.actions.image_create.execute({
|
|
278
|
+
context: create_context(),
|
|
279
|
+
payload: {
|
|
280
|
+
prompt: "ignore this prompt",
|
|
281
|
+
content: [{ type: "text", text: "use this content" }],
|
|
282
|
+
},
|
|
283
|
+
pluginName: "image",
|
|
284
|
+
actionName: "image_create",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
assert.equal(result.success, true);
|
|
288
|
+
assert.equal(captured_input.messages[0].content[0].text, "use this content");
|
|
289
|
+
assert.equal("prompt" in captured_input, false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("ImagePlugin image_create keeps remote content image URLs", async () => {
|
|
293
|
+
let captured_input;
|
|
294
|
+
const plugin = new ImagePlugin({
|
|
295
|
+
image_create: (input) => {
|
|
296
|
+
captured_input = input;
|
|
297
|
+
return { job_id: "img_1", status: "queued", poll_after_ms: 1 };
|
|
298
|
+
},
|
|
299
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const result = await plugin.actions.image_create.execute({
|
|
303
|
+
context: create_context(),
|
|
304
|
+
payload: {
|
|
305
|
+
content: [
|
|
306
|
+
{ type: "text", text: "use this style" },
|
|
307
|
+
{ type: "image", url: "https://example.com/input.webp", media_type: "image/webp" },
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
pluginName: "image",
|
|
311
|
+
actionName: "image_create",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
assert.equal(result.success, true);
|
|
315
|
+
assert.equal(captured_input.messages[0].content[1].url, "https://example.com/input.webp");
|
|
316
|
+
assert.equal(captured_input.messages[0].content[1].media_type, "image/webp");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("ImagePlugin image_create rejects legacy messages and data URLs", async () => {
|
|
320
|
+
const plugin = new ImagePlugin({
|
|
321
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
322
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const messages_result = await plugin.actions.image_create.execute({
|
|
326
|
+
context: create_context(),
|
|
327
|
+
payload: {
|
|
328
|
+
messages: [
|
|
329
|
+
{
|
|
330
|
+
role: "user",
|
|
331
|
+
content: [{ type: "text", text: "legacy" }],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
pluginName: "image",
|
|
336
|
+
actionName: "image_create",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
assert.equal(messages_result.success, false);
|
|
340
|
+
assert.match(messages_result.error, /messages is not supported/);
|
|
341
|
+
|
|
342
|
+
const data_url_result = await plugin.actions.image_create.execute({
|
|
343
|
+
context: create_context(),
|
|
344
|
+
payload: {
|
|
345
|
+
content: [
|
|
346
|
+
{ type: "text", text: "edit this" },
|
|
347
|
+
{ type: "image", url: "data:image/png;base64,cG5n" },
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
pluginName: "image",
|
|
351
|
+
actionName: "image_create",
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
assert.equal(data_url_result.success, false);
|
|
355
|
+
assert.match(data_url_result.error, /does not accept data URLs/);
|
|
215
356
|
});
|
|
@@ -14,9 +14,13 @@ import fs from "node:fs/promises";
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
invokePluginCallTool,
|
|
17
|
+
invokePluginReadTool,
|
|
17
18
|
setPluginToolRuntime,
|
|
18
19
|
} from "../bin/executor/tools/plugin/PluginToolBridge.js";
|
|
19
20
|
import { withSessionRunScope } from "../bin/executor/SessionRunScope.js";
|
|
21
|
+
import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
|
|
22
|
+
import { createAgentPluginRegistry } from "../bin/agent/local/AgentPluginFactory.js";
|
|
23
|
+
import { z } from "zod";
|
|
20
24
|
|
|
21
25
|
function create_run_context(project_root) {
|
|
22
26
|
return {
|
|
@@ -36,6 +40,7 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
|
|
|
36
40
|
|
|
37
41
|
setPluginToolRuntime({
|
|
38
42
|
list: () => [],
|
|
43
|
+
read: () => ({ plugins: [] }),
|
|
39
44
|
availability: async () => ({ enabled: true, available: true, reasons: [] }),
|
|
40
45
|
runAction: async () => ({
|
|
41
46
|
success: true,
|
|
@@ -65,8 +70,8 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
|
|
|
65
70
|
const result = await withSessionRunScope({ runContext: run_context }, () =>
|
|
66
71
|
invokePluginCallTool({
|
|
67
72
|
plugin: "image",
|
|
68
|
-
action: "
|
|
69
|
-
payload: {
|
|
73
|
+
action: "image_result",
|
|
74
|
+
payload: { job_id: "img_1" },
|
|
70
75
|
}),
|
|
71
76
|
);
|
|
72
77
|
|
|
@@ -86,3 +91,109 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
|
|
|
86
91
|
assert.equal(pending_parts.length, 1);
|
|
87
92
|
assert.equal(pending_parts[0].url, result.files[0].url);
|
|
88
93
|
});
|
|
94
|
+
|
|
95
|
+
test("invokePluginReadTool returns plugin action metadata", async () => {
|
|
96
|
+
setPluginToolRuntime({
|
|
97
|
+
list: () => [],
|
|
98
|
+
read: () => ({
|
|
99
|
+
name: "image",
|
|
100
|
+
title: "Image",
|
|
101
|
+
description: "Create images",
|
|
102
|
+
actions: [
|
|
103
|
+
{
|
|
104
|
+
name: "image_create",
|
|
105
|
+
description: "Create image job",
|
|
106
|
+
has_input_schema: true,
|
|
107
|
+
input_schema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
prompt: { type: "string" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
examples: [
|
|
114
|
+
{
|
|
115
|
+
title: "Text-only image",
|
|
116
|
+
payload: { prompt: "draw" },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
allow_when_disabled: false,
|
|
120
|
+
has_command: false,
|
|
121
|
+
has_api: false,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}),
|
|
125
|
+
availability: async () => ({ enabled: true, available: true, reasons: [] }),
|
|
126
|
+
runAction: async () => ({ success: false, error: "not used" }),
|
|
127
|
+
pipeline: async (_, value) => value,
|
|
128
|
+
guard: async () => {},
|
|
129
|
+
effect: async () => {},
|
|
130
|
+
resolve: async () => {
|
|
131
|
+
throw new Error("not implemented");
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await invokePluginReadTool({
|
|
136
|
+
plugin: "image",
|
|
137
|
+
action: "image_create",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.equal(result.success, true);
|
|
141
|
+
assert.equal(result.data.name, "image");
|
|
142
|
+
assert.equal(result.data.actions[0].name, "image_create");
|
|
143
|
+
assert.equal(result.data.actions[0].has_input_schema, true);
|
|
144
|
+
assert.equal(result.data.actions[0].examples[0].payload.prompt, "draw");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("PluginRegistry validates action payload with metadata schema", async () => {
|
|
148
|
+
const plugin = createPlugin({
|
|
149
|
+
name: "demo",
|
|
150
|
+
title: "Demo",
|
|
151
|
+
description: "Demo plugin",
|
|
152
|
+
actions: {
|
|
153
|
+
echo: createAction({
|
|
154
|
+
description: "Echo text",
|
|
155
|
+
input_schema: {
|
|
156
|
+
zod: z.object({
|
|
157
|
+
text: z.string(),
|
|
158
|
+
}),
|
|
159
|
+
json_schema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
required: ["text"],
|
|
162
|
+
properties: {
|
|
163
|
+
text: { type: "string" },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
execute: async ({ input }) => ({
|
|
168
|
+
success: true,
|
|
169
|
+
data: { text: input.text },
|
|
170
|
+
message: "echoed",
|
|
171
|
+
}),
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const registry = createAgentPluginRegistry({
|
|
176
|
+
plugins: [plugin],
|
|
177
|
+
get_context: () => ({ rootPath: process.cwd() }),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const metadata = registry.read({ plugin: "demo", action: "echo" });
|
|
181
|
+
assert.equal(metadata.actions[0].description, "Echo text");
|
|
182
|
+
assert.equal(metadata.actions[0].has_input_schema, true);
|
|
183
|
+
|
|
184
|
+
const invalid = await registry.runAction({
|
|
185
|
+
plugin: "demo",
|
|
186
|
+
action: "echo",
|
|
187
|
+
payload: {},
|
|
188
|
+
});
|
|
189
|
+
assert.equal(invalid.success, false);
|
|
190
|
+
assert.match(invalid.error, /Invalid payload/);
|
|
191
|
+
|
|
192
|
+
const valid = await registry.runAction({
|
|
193
|
+
plugin: "demo",
|
|
194
|
+
action: "echo",
|
|
195
|
+
payload: { text: "hello" },
|
|
196
|
+
});
|
|
197
|
+
assert.equal(valid.success, true);
|
|
198
|
+
assert.equal(valid.data.text, "hello");
|
|
199
|
+
});
|
|
@@ -184,6 +184,7 @@ export class AgentAssemblyService {
|
|
|
184
184
|
});
|
|
185
185
|
const plugins = plugin_registry;
|
|
186
186
|
if (this.should_register_plugin_call_tool(plugin_instances)) {
|
|
187
|
+
tools.plugin_read = tools.plugin_read || plugin_tools.plugin_read;
|
|
187
188
|
tools.plugin_call = tools.plugin_call || plugin_tools.plugin_call;
|
|
188
189
|
}
|
|
189
190
|
const resolve_session_model = this.resolve_session_model;
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Source: src/executor/composer/system/default/assets/plugin.prompt.ts.txt
|
|
7
|
-
const TEXT_MODULE_CONTENT = "# Plugin State\n\n你正在一个基于 plugin 的执行环境中工作。\n\n## Plugin 调用规则\n\n- 当你需要使用 plugin 能力时,优先通过可用的 tool 调用 plugin action。\n- 若当前工具集中存在 `plugin_call`,使用 `plugin_call({ plugin, action, payload })` 触发对应 plugin action。\n- `plugin_call.plugin` 是 plugin 名称,例如 `skill`、`task`、`memory`、`contact`。\n- `plugin_call.action` 是 action 名称,例如 `list`、`lookup`、`create`、`run`。\n- `plugin_call.payload` 是结构化 JSON payload;没有参数时传 `{}
|
|
7
|
+
const TEXT_MODULE_CONTENT = "# Plugin State\n\n你正在一个基于 plugin 的执行环境中工作。\n\n## Plugin 调用规则\n\n- 当你需要使用 plugin 能力时,优先通过可用的 tool 调用 plugin action。\n- 若当前工具集中存在 `plugin_read`,在不确定 plugin action、参数 schema 或示例时,先使用 `plugin_read({ plugin, action? })` 读取 metadata。\n- 若当前工具集中存在 `plugin_call`,使用 `plugin_call({ plugin, action, payload })` 触发对应 plugin action。\n- `plugin_call.plugin` 是 plugin 名称,例如 `skill`、`task`、`memory`、`contact`。\n- `plugin_call.action` 是 action 名称,例如 `list`、`lookup`、`create`、`run`。\n- `plugin_call.payload` 是结构化 JSON payload;没有参数时传 `{}`。如果 action metadata 声明了 input schema,payload 必须符合 schema。\n- ActionSchedule 是 Agent 内部用于延迟执行 plugin action 的能力,不是独立 plugin。\n\n## 可用 plugin 概览\n\n- 当前内建托管 plugin:`shell` / `chat` / `task` / `memory` / `contact`。\n- 当前内建本地 plugin:`auth` / `skill`。\n\n具体 plugin 能力以 `plugin_read` 返回的 action metadata 和该 plugin 的 system 提示为准。\n";
|
|
8
8
|
|
|
9
9
|
export default TEXT_MODULE_CONTENT;
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
## Plugin 调用规则
|
|
6
6
|
|
|
7
7
|
- 当你需要使用 plugin 能力时,优先通过可用的 tool 调用 plugin action。
|
|
8
|
+
- 若当前工具集中存在 `plugin_read`,在不确定 plugin action、参数 schema 或示例时,先使用 `plugin_read({ plugin, action? })` 读取 metadata。
|
|
8
9
|
- 若当前工具集中存在 `plugin_call`,使用 `plugin_call({ plugin, action, payload })` 触发对应 plugin action。
|
|
9
10
|
- `plugin_call.plugin` 是 plugin 名称,例如 `skill`、`task`、`memory`、`contact`。
|
|
10
11
|
- `plugin_call.action` 是 action 名称,例如 `list`、`lookup`、`create`、`run`。
|
|
11
|
-
- `plugin_call.payload` 是结构化 JSON payload;没有参数时传 `{}
|
|
12
|
+
- `plugin_call.payload` 是结构化 JSON payload;没有参数时传 `{}`。如果 action metadata 声明了 input schema,payload 必须符合 schema。
|
|
12
13
|
- ActionSchedule 是 Agent 内部用于延迟执行 plugin action 的能力,不是独立 plugin。
|
|
13
14
|
|
|
14
15
|
## 可用 plugin 概览
|
|
@@ -16,4 +17,4 @@
|
|
|
16
17
|
- 当前内建托管 plugin:`shell` / `chat` / `task` / `memory` / `contact`。
|
|
17
18
|
- 当前内建本地 plugin:`auth` / `skill`。
|
|
18
19
|
|
|
19
|
-
具体 plugin
|
|
20
|
+
具体 plugin 能力以 `plugin_read` 返回的 action metadata 和该 plugin 的 system 提示为准。
|
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
PluginCallInput,
|
|
16
16
|
PluginCallToolFileResult,
|
|
17
17
|
PluginCallToolResult,
|
|
18
|
+
PluginReadInput,
|
|
19
|
+
PluginReadToolResult,
|
|
18
20
|
} from "@/executor/tools/plugin/types/PluginTool.js";
|
|
19
21
|
import { materializeAssistantFileParts } from "@executor/messages/AssistantFileResource.js";
|
|
20
22
|
import {
|
|
@@ -206,3 +208,31 @@ export async function invokePluginCallTool(
|
|
|
206
208
|
};
|
|
207
209
|
}
|
|
208
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 读取 plugin / action metadata。
|
|
214
|
+
*/
|
|
215
|
+
export async function invokePluginReadTool(
|
|
216
|
+
input: PluginReadInput,
|
|
217
|
+
): Promise<PluginReadToolResult> {
|
|
218
|
+
try {
|
|
219
|
+
const runtime = require_plugin_tool_runtime();
|
|
220
|
+
const data = runtime.read({
|
|
221
|
+
plugin: typeof input.plugin === "string" ? input.plugin : undefined,
|
|
222
|
+
action: typeof input.action === "string" ? input.action : undefined,
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
message: "plugin metadata read",
|
|
227
|
+
data: data as unknown as JsonObject,
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
message: String(error),
|
|
233
|
+
data: {
|
|
234
|
+
error: String(error),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { tool } from "ai";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import type {
|
|
12
|
+
PluginCallInput,
|
|
13
|
+
PluginReadInput,
|
|
14
|
+
} from "@/executor/tools/plugin/types/PluginTool.js";
|
|
15
|
+
import {
|
|
16
|
+
invokePluginCallTool,
|
|
17
|
+
invokePluginReadTool,
|
|
18
|
+
setPluginToolRuntime,
|
|
19
|
+
} from "./PluginToolBridge.js";
|
|
20
|
+
import {
|
|
21
|
+
plugin_call_input_schema,
|
|
22
|
+
plugin_read_input_schema,
|
|
23
|
+
} from "./PluginToolSchemas.js";
|
|
14
24
|
|
|
15
25
|
export { setPluginToolRuntime } from "./PluginToolBridge.js";
|
|
16
26
|
|
|
@@ -19,14 +29,25 @@ export { setPluginToolRuntime } from "./PluginToolBridge.js";
|
|
|
19
29
|
*/
|
|
20
30
|
export const plugin_call = tool({
|
|
21
31
|
description:
|
|
22
|
-
"Call a registered agent plugin action. Use
|
|
32
|
+
"Call a registered agent plugin action. Use plugin_read first when you need the action list, input schema, or examples. Generated files may be attached to the final assistant message automatically.",
|
|
23
33
|
inputSchema: plugin_call_input_schema,
|
|
24
34
|
execute: async (input) => await invokePluginCallTool(input as PluginCallInput),
|
|
25
35
|
});
|
|
26
36
|
|
|
37
|
+
/**
|
|
38
|
+
* `plugin_read`:读取已注册 plugin / action metadata。
|
|
39
|
+
*/
|
|
40
|
+
export const plugin_read = tool({
|
|
41
|
+
description:
|
|
42
|
+
"Read registered agent plugin metadata, including action names, descriptions, input schemas, and examples. Use this before plugin_call when the payload shape is unclear.",
|
|
43
|
+
inputSchema: plugin_read_input_schema,
|
|
44
|
+
execute: async (input) => await invokePluginReadTool(input as PluginReadInput),
|
|
45
|
+
});
|
|
46
|
+
|
|
27
47
|
/**
|
|
28
48
|
* Plugin 工具导出集合。
|
|
29
49
|
*/
|
|
30
50
|
export const plugin_tools = {
|
|
31
51
|
plugin_call,
|
|
52
|
+
plugin_read,
|
|
32
53
|
};
|
|
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
|
|
11
11
|
export const plugin_call_input_schema = z.object({
|
|
12
12
|
plugin: z.string().describe("Registered plugin name to call, for example image."),
|
|
13
|
-
action: z.string().describe("Plugin action name to execute, for example
|
|
13
|
+
action: z.string().describe("Plugin action name to execute, for example image_create."),
|
|
14
14
|
payload: z
|
|
15
15
|
.object({})
|
|
16
16
|
.passthrough()
|
|
@@ -18,3 +18,14 @@ export const plugin_call_input_schema = z.object({
|
|
|
18
18
|
.default({})
|
|
19
19
|
.describe("JSON payload passed to the plugin action."),
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
export const plugin_read_input_schema = z.object({
|
|
23
|
+
plugin: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Registered plugin name to inspect. Omit to list plugins."),
|
|
27
|
+
action: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Plugin action name to inspect. Requires plugin."),
|
|
31
|
+
});
|