@ycodium-ai/agent-fake 0.2.3230 → 0.2.3236
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/README.md +32 -1
- package/dist/index.js +273 -128
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A minimal executor plugin. It starts no CLI and replays a fixed event script:
|
|
|
4
4
|
|
|
5
5
|
- `startSession` emits `session.started`
|
|
6
6
|
- `sendTurn` emits `turn.started`, one `content.delta`, then `turn.completed` (the host binds the turn by `turn.started`)
|
|
7
|
-
- `config.script` picks the script: `plain` (default), `approval-tool` (two approvals before the reply), `async-question` (on a thread's first turn, the reply plus one non-blocking `user-input.requested` with `responseMode: "message"`), `stream` (a Markdown answer in `config.streamChunks` deltas, default 200, one every `config.streamIntervalMs`, default 30; the UI probe `interaction-table.ts` measures streaming with it), `mcp-send` (real calls through the built-in MCP server the host handed to `startSession`, the same path a real agent takes: a turn whose input is a JSON object is passed as the arguments of one `send_message` call, and a JSON array of `{ "tool": "…", "arguments": { … } }` is called step by step, for example `ycodium_plugin_define`, then `ycodium_plugin_run`, then a tool the mounted plugin contributes; the reply has one line per call with its result or error, and any other input gets the plain reply; see [Playbook mode](#playbook-mode) for per-thread scripts that ignore turn input), `visualize` (emits one in-progress builtin tool row, holds for `config.previewHoldMs` milliseconds so the input preview can paint, then one real `visualize_visualize` call on that same server; the tool row’s result is the MCP confirmation text, and the reply is “The figure is in the card above.”), or `lane` (the UI probe `lane1-journey.ts` script: the user message's `lane:stream|plain|approve|question|attach|hold` picks the turn; `hold` accepts a second `sendTurn` with the in-flight turn id as a steer and folds that text into the reply; `config.holdMs` is how long `hold` waits, default 15000)
|
|
7
|
+
- `config.script` picks the script: `plain` (default), `approval-tool` (two approvals before the reply), `async-question` (on a thread's first turn, the reply plus one non-blocking `user-input.requested` with `responseMode: "message"`), `stream` (a Markdown answer in `config.streamChunks` deltas, default 200, one every `config.streamIntervalMs`, default 30; the UI probe `interaction-table.ts` measures streaming with it), `replay` (recorded events; see [Event replay](#event-replay)), `mcp-send` (real calls through the built-in MCP server the host handed to `startSession`, the same path a real agent takes: a turn whose input is a JSON object is passed as the arguments of one `send_message` call, and a JSON array of `{ "tool": "…", "arguments": { … } }` is called step by step, for example `ycodium_plugin_define`, then `ycodium_plugin_run`, then a tool the mounted plugin contributes; the reply has one line per call with its result or error, and any other input gets the plain reply; see [Playbook mode](#playbook-mode) for per-thread scripts that ignore turn input), `visualize` (emits one in-progress builtin tool row, holds for `config.previewHoldMs` milliseconds so the input preview can paint, then one real `visualize_visualize` call on that same server; the tool row’s result is the MCP confirmation text, and the reply is “The figure is in the card above.”), or `lane` (the UI probe `lane1-journey.ts` script: the user message's `lane:stream|plain|approve|question|attach|hold` picks the turn; `hold` accepts a second `sendTurn` with the in-flight turn id as a steer and folds that text into the reply; `config.holdMs` is how long `hold` waits, default 15000)
|
|
8
8
|
- `interruptTurn` (or an aborted `AbortSignal`) ends the in-flight turn as `interrupted`
|
|
9
9
|
- `readThread` returns the bookkeeping the instance recorded
|
|
10
10
|
- `probe` reports two models, `fake` (default) and `fake-large`
|
|
@@ -23,6 +23,37 @@ This package is **not** in the default bundle and **not** a first-party add-grid
|
|
|
23
23
|
|
|
24
24
|
Develop with `vp run --filter @ycodium-ai/agent-fake typecheck`, `build`, and `test`.
|
|
25
25
|
|
|
26
|
+
## Event replay
|
|
27
|
+
|
|
28
|
+
Set `config.script: "replay"` to replay recorded `ExecutorEvent` output through the fake's
|
|
29
|
+
event emitter. `config.replayEventsPath` must be an absolute path on the executor's machine
|
|
30
|
+
to a JSON array or an adapter golden object with an `events` array. The file is read on each
|
|
31
|
+
turn; an unreadable file, invalid event envelope or malformed JSON fails that turn with a
|
|
32
|
+
`replay:` error. The plugin does not bundle recordings or depend on another executor plugin.
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"script": "replay",
|
|
37
|
+
"replayEventsPath": "/absolute/path/to/recorded-events.json",
|
|
38
|
+
"replayStepMs": 50,
|
|
39
|
+
"replayTaskHoldMs": 4000
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`replayStepMs` sets the interval between retained events (default 30 ms).
|
|
44
|
+
`replayTaskHoldMs` adds a hold after every `task.started` (default 3000 ms), so observers can
|
|
45
|
+
read the working state before completion. Both are non-negative integers; zero disables
|
|
46
|
+
that delay. These settings are outside the settings form. Timing is configured, not recorded.
|
|
47
|
+
|
|
48
|
+
Replay keeps items, content, tasks, plans and other turn-local events. Lifecycle events
|
|
49
|
+
(`isExecutorLifecycleEvent` in the plugin API: session, thread and turn lifecycle, plus
|
|
50
|
+
`runtime.error`, which the host turns into a session error) are owned by the fake and never
|
|
51
|
+
replayed, so a recording cannot fail the turn behind the fake's back. See the allowlist in
|
|
52
|
+
[`replayScript.ts`](src/replayScript.ts). Events get the current thread and turn IDs and fresh
|
|
53
|
+
event metadata. Item, request and task IDs are fresh per replay, with tool/task links kept
|
|
54
|
+
consistent. Adapter golden `⟨undefined⟩` fields are omitted. Assistant text is also recorded
|
|
55
|
+
by `readThread`. Interrupting a replay cancels its delays and ends the turn as `interrupted`.
|
|
56
|
+
|
|
26
57
|
## Playbook mode
|
|
27
58
|
|
|
28
59
|
Replay recorded conversations or drive multi-thread acceptance tests with
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
3
|
|
|
4
4
|
// src/card.ts
|
|
5
|
+
var FAKE_SCRIPTS = [
|
|
6
|
+
"plain",
|
|
7
|
+
"approval-tool",
|
|
8
|
+
"async-question",
|
|
9
|
+
"stream",
|
|
10
|
+
"replay",
|
|
11
|
+
"mcp-send",
|
|
12
|
+
"visualize",
|
|
13
|
+
"lane"
|
|
14
|
+
];
|
|
5
15
|
var FAKE_DRIVER_KIND = "fake";
|
|
6
16
|
var FAKE_ASSISTANT_TEXT = "fake turn complete";
|
|
7
17
|
var FAKE_CARD = {
|
|
@@ -45,18 +55,13 @@ var FAKE_CARD = {
|
|
|
45
55
|
properties: {
|
|
46
56
|
script: {
|
|
47
57
|
type: "string",
|
|
48
|
-
enum: [
|
|
49
|
-
"plain",
|
|
50
|
-
"approval-tool",
|
|
51
|
-
"async-question",
|
|
52
|
-
"stream",
|
|
53
|
-
"mcp-send",
|
|
54
|
-
"visualize",
|
|
55
|
-
"lane"
|
|
56
|
-
]
|
|
58
|
+
enum: [...FAKE_SCRIPTS]
|
|
57
59
|
},
|
|
58
60
|
streamChunks: { type: "integer", minimum: 1 },
|
|
59
61
|
streamIntervalMs: { type: "integer", minimum: 0 },
|
|
62
|
+
replayEventsPath: { type: "string" },
|
|
63
|
+
replayStepMs: { type: "integer", minimum: 0 },
|
|
64
|
+
replayTaskHoldMs: { type: "integer", minimum: 0 },
|
|
60
65
|
// 仅探针与测试故意违约:stop 收完后台轮但不发 session.exited,事件流仍打开;默认关闭,不进设置表单。
|
|
61
66
|
stopWithoutExit: { type: "boolean" },
|
|
62
67
|
// lane 的 hold 最多等插话多久;不进设置表单。
|
|
@@ -125,6 +130,131 @@ var EventBus = class {
|
|
|
125
130
|
}
|
|
126
131
|
};
|
|
127
132
|
|
|
133
|
+
// src/approvalToolScript.ts
|
|
134
|
+
function createApprovalToolScript({
|
|
135
|
+
emit,
|
|
136
|
+
nextId,
|
|
137
|
+
pendingDecisions,
|
|
138
|
+
yieldTick: yieldTick2
|
|
139
|
+
}) {
|
|
140
|
+
const runApprovalToolScript = async (threadId, turnId, signal) => {
|
|
141
|
+
const isAborted = () => signal?.aborted === true;
|
|
142
|
+
const fakeReply = "\u597D";
|
|
143
|
+
emit(
|
|
144
|
+
"content.delta",
|
|
145
|
+
threadId,
|
|
146
|
+
{
|
|
147
|
+
streamKind: "reasoning_text",
|
|
148
|
+
delta: "\u5148\u60F3\u6E05\u695A\u6B65\u9AA4\uFF1A\u7B2C\u4E00\u6B65 ls \u5F53\u524D\u76EE\u5F55\uFF1B\u7B2C\u4E8C\u6B65\u8BFB README.md \u7684\u7B2C\u4E00\u884C\uFF1B\u6700\u540E\u53EA\u56DE\u4E00\u4E2A\u5B57\u3002"
|
|
149
|
+
},
|
|
150
|
+
{ turnId }
|
|
151
|
+
);
|
|
152
|
+
await yieldTick2();
|
|
153
|
+
if (isAborted()) return null;
|
|
154
|
+
const approve = async (request) => {
|
|
155
|
+
emit(
|
|
156
|
+
"item.started",
|
|
157
|
+
threadId,
|
|
158
|
+
{
|
|
159
|
+
itemType: request.itemType,
|
|
160
|
+
status: "inProgress",
|
|
161
|
+
title: request.title,
|
|
162
|
+
data: request.args
|
|
163
|
+
},
|
|
164
|
+
{ turnId, itemId: request.itemId }
|
|
165
|
+
);
|
|
166
|
+
emit(
|
|
167
|
+
"request.opened",
|
|
168
|
+
threadId,
|
|
169
|
+
{
|
|
170
|
+
requestType: request.requestType,
|
|
171
|
+
toolName: request.toolName,
|
|
172
|
+
detail: request.detail,
|
|
173
|
+
args: request.args
|
|
174
|
+
},
|
|
175
|
+
{ turnId, requestId: request.requestId }
|
|
176
|
+
);
|
|
177
|
+
const decision = await new Promise((resolve) => {
|
|
178
|
+
let settled = false;
|
|
179
|
+
const settle = (value) => {
|
|
180
|
+
if (settled) return;
|
|
181
|
+
settled = true;
|
|
182
|
+
signal?.removeEventListener("abort", onAbort);
|
|
183
|
+
pendingDecisions.delete(request.requestId);
|
|
184
|
+
resolve(value);
|
|
185
|
+
};
|
|
186
|
+
const onAbort = () => settle("cancel");
|
|
187
|
+
signal?.addEventListener("abort", onAbort);
|
|
188
|
+
if (isAborted()) {
|
|
189
|
+
settle("cancel");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
pendingDecisions.set(request.requestId, settle);
|
|
193
|
+
});
|
|
194
|
+
emit(
|
|
195
|
+
"request.resolved",
|
|
196
|
+
threadId,
|
|
197
|
+
{
|
|
198
|
+
requestType: request.requestType,
|
|
199
|
+
decision: decision === "allow" ? "accept" : decision === "deny" ? "decline" : "cancel"
|
|
200
|
+
},
|
|
201
|
+
{ turnId, requestId: request.requestId }
|
|
202
|
+
);
|
|
203
|
+
if (decision === "allow") {
|
|
204
|
+
emit(
|
|
205
|
+
"content.delta",
|
|
206
|
+
threadId,
|
|
207
|
+
{ streamKind: "command_output", delta: request.output },
|
|
208
|
+
{ turnId, itemId: request.itemId }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
emit(
|
|
212
|
+
"item.completed",
|
|
213
|
+
threadId,
|
|
214
|
+
{
|
|
215
|
+
itemType: request.itemType,
|
|
216
|
+
status: decision === "allow" ? "completed" : "declined",
|
|
217
|
+
title: request.title,
|
|
218
|
+
data: { ...request.args, ...decision === "allow" ? { output: request.output } : {} }
|
|
219
|
+
},
|
|
220
|
+
{ turnId, itemId: request.itemId }
|
|
221
|
+
);
|
|
222
|
+
return decision;
|
|
223
|
+
};
|
|
224
|
+
const first = await approve({
|
|
225
|
+
itemId: nextId("item"),
|
|
226
|
+
requestId: nextId("req"),
|
|
227
|
+
requestType: "exec_command_approval",
|
|
228
|
+
toolName: "shell",
|
|
229
|
+
detail: "ls",
|
|
230
|
+
args: { command: "ls" },
|
|
231
|
+
itemType: "command_execution",
|
|
232
|
+
title: "ls",
|
|
233
|
+
output: "README.md\n"
|
|
234
|
+
});
|
|
235
|
+
if (first === "cancel") return null;
|
|
236
|
+
if (first === "allow") {
|
|
237
|
+
const second = await approve({
|
|
238
|
+
itemId: nextId("item"),
|
|
239
|
+
requestId: nextId("req"),
|
|
240
|
+
requestType: "file_read_approval",
|
|
241
|
+
toolName: "Read",
|
|
242
|
+
detail: "README.md",
|
|
243
|
+
args: { file_path: "README.md" },
|
|
244
|
+
itemType: "dynamic_tool_call",
|
|
245
|
+
title: "Read README.md",
|
|
246
|
+
output: "# hands-probe-workspace\n"
|
|
247
|
+
});
|
|
248
|
+
if (second === "cancel") return null;
|
|
249
|
+
}
|
|
250
|
+
if (isAborted()) return null;
|
|
251
|
+
emit("content.delta", threadId, { streamKind: "assistant_text", delta: fakeReply }, { turnId });
|
|
252
|
+
await yieldTick2();
|
|
253
|
+
return isAborted() ? null : fakeReply;
|
|
254
|
+
};
|
|
255
|
+
return runApprovalToolScript;
|
|
256
|
+
}
|
|
257
|
+
|
|
128
258
|
// src/fakeSessionLifecycle.ts
|
|
129
259
|
var parseStopWithoutExit = (input) => {
|
|
130
260
|
const envelope = input;
|
|
@@ -628,10 +758,18 @@ async function openMcpSession(input) {
|
|
|
628
758
|
};
|
|
629
759
|
}
|
|
630
760
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
761
|
+
var HOST_BLOCK = /^<(ycodium-memory|ycodium-skill)(?:\s[^>]*)?>[\s\S]*?<\/\1>\n\n/u;
|
|
762
|
+
function stripHostBlocks(input) {
|
|
763
|
+
let rest = input;
|
|
764
|
+
for (let block = HOST_BLOCK.exec(rest); block !== null; block = HOST_BLOCK.exec(rest)) {
|
|
765
|
+
rest = rest.slice(block[0].length);
|
|
766
|
+
}
|
|
767
|
+
return rest;
|
|
768
|
+
}
|
|
631
769
|
function parseMcpSendInput(input) {
|
|
632
770
|
let parsed;
|
|
633
771
|
try {
|
|
634
|
-
parsed = JSON.parse(input ?? "");
|
|
772
|
+
parsed = JSON.parse(stripHostBlocks(input ?? ""));
|
|
635
773
|
} catch {
|
|
636
774
|
return null;
|
|
637
775
|
}
|
|
@@ -1239,6 +1377,112 @@ async function runVisualizeTurn(input) {
|
|
|
1239
1377
|
return resultText.length > 0 ? REPLY : "visualize failed: empty tool result";
|
|
1240
1378
|
}
|
|
1241
1379
|
|
|
1380
|
+
// src/replayScript.ts
|
|
1381
|
+
import * as NodeFSP2 from "node:fs/promises";
|
|
1382
|
+
import * as NodePath2 from "node:path";
|
|
1383
|
+
import { isExecutorLifecycleEvent } from "@ycodium-ai/plugin-api/executor";
|
|
1384
|
+
var IN_TURN_TYPES = /* @__PURE__ */ new Set([
|
|
1385
|
+
"item.started",
|
|
1386
|
+
"item.updated",
|
|
1387
|
+
"item.completed",
|
|
1388
|
+
"content.delta",
|
|
1389
|
+
"task.started",
|
|
1390
|
+
"task.progress",
|
|
1391
|
+
"task.updated",
|
|
1392
|
+
"task.completed",
|
|
1393
|
+
"turn.plan.updated",
|
|
1394
|
+
"turn.proposed.delta",
|
|
1395
|
+
"turn.proposed.completed",
|
|
1396
|
+
"turn.diff.updated",
|
|
1397
|
+
"request.opened",
|
|
1398
|
+
"request.resolved",
|
|
1399
|
+
"user-input.requested",
|
|
1400
|
+
"user-input.resolved",
|
|
1401
|
+
"hook.started",
|
|
1402
|
+
"hook.progress",
|
|
1403
|
+
"hook.completed",
|
|
1404
|
+
"tool.progress",
|
|
1405
|
+
"tool.summary",
|
|
1406
|
+
"tool.denied",
|
|
1407
|
+
"model.rerouted",
|
|
1408
|
+
"files.persisted",
|
|
1409
|
+
"assistant.attachment.produced",
|
|
1410
|
+
"assistant.artifact.produced",
|
|
1411
|
+
"runtime.warning"
|
|
1412
|
+
]);
|
|
1413
|
+
var IDENTITY_FIELDS = [
|
|
1414
|
+
["taskId", "task"],
|
|
1415
|
+
["agentId", "task"],
|
|
1416
|
+
["parentAgentId", "task"],
|
|
1417
|
+
["toolUseId", "item"],
|
|
1418
|
+
["parentToolUseId", "item"]
|
|
1419
|
+
];
|
|
1420
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1421
|
+
function parseEvents(text) {
|
|
1422
|
+
const parsed = JSON.parse(
|
|
1423
|
+
text,
|
|
1424
|
+
(_key, value) => value === "\u27E8undefined\u27E9" ? void 0 : value
|
|
1425
|
+
);
|
|
1426
|
+
const events = isRecord3(parsed) ? parsed.events : parsed;
|
|
1427
|
+
if (!Array.isArray(events))
|
|
1428
|
+
throw new Error("expected ExecutorEvent[] or { events: ExecutorEvent[] }");
|
|
1429
|
+
return events.map((event, index) => {
|
|
1430
|
+
if (!isRecord3(event) || !["type", "eventId", "provider", "threadId", "createdAt"].every(
|
|
1431
|
+
(key) => typeof event[key] === "string"
|
|
1432
|
+
) || !isRecord3(event.payload) || !["turnId", "itemId", "requestId"].every(
|
|
1433
|
+
(key) => event[key] === void 0 || typeof event[key] === "string"
|
|
1434
|
+
))
|
|
1435
|
+
throw new Error(`invalid ExecutorEvent at index ${index}`);
|
|
1436
|
+
return event;
|
|
1437
|
+
}).filter((event) => !isExecutorLifecycleEvent(event) && IN_TURN_TYPES.has(event.type));
|
|
1438
|
+
}
|
|
1439
|
+
async function runReplayTurn(input) {
|
|
1440
|
+
const config = input.config?.config;
|
|
1441
|
+
const path = config?.replayEventsPath;
|
|
1442
|
+
const duration = (key, fallback) => {
|
|
1443
|
+
const value = config?.[key] ?? fallback;
|
|
1444
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
1445
|
+
throw new Error(`replay: ${key} must be a non-negative integer`);
|
|
1446
|
+
return value;
|
|
1447
|
+
};
|
|
1448
|
+
const stepMs = duration("replayStepMs", 30);
|
|
1449
|
+
const taskHoldMs = duration("replayTaskHoldMs", 3e3);
|
|
1450
|
+
if (typeof path !== "string" || !NodePath2.isAbsolute(path))
|
|
1451
|
+
throw new Error("replay: replayEventsPath must be an absolute path");
|
|
1452
|
+
let events;
|
|
1453
|
+
try {
|
|
1454
|
+
events = parseEvents(await NodeFSP2.readFile(path, "utf8"));
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
throw new Error(`replay: ${path}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
1457
|
+
cause: error
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1461
|
+
const remap = (kind, id) => {
|
|
1462
|
+
const key = `${kind}:${id}`;
|
|
1463
|
+
if (!ids.has(key)) ids.set(key, input.nextId(kind));
|
|
1464
|
+
return ids.get(key);
|
|
1465
|
+
};
|
|
1466
|
+
let text = "";
|
|
1467
|
+
for (const [index, event] of events.entries()) {
|
|
1468
|
+
if (index > 0) await hold(stepMs, input.signal);
|
|
1469
|
+
if (input.signal.aborted) return null;
|
|
1470
|
+
const payload = { ...event.payload };
|
|
1471
|
+
for (const [key, kind] of IDENTITY_FIELDS) {
|
|
1472
|
+
if (typeof payload[key] === "string") payload[key] = remap(kind, payload[key]);
|
|
1473
|
+
}
|
|
1474
|
+
input.emit(event.type, input.threadId, payload, {
|
|
1475
|
+
turnId: input.turnId,
|
|
1476
|
+
...event.itemId === void 0 ? {} : { itemId: remap("item", event.itemId) },
|
|
1477
|
+
...event.requestId === void 0 ? {} : { requestId: remap("request", event.requestId) }
|
|
1478
|
+
});
|
|
1479
|
+
if (event.type === "content.delta" && payload.streamKind === "assistant_text" && typeof payload.delta === "string")
|
|
1480
|
+
text += payload.delta;
|
|
1481
|
+
if (event.type === "task.started") await hold(taskHoldMs, input.signal);
|
|
1482
|
+
}
|
|
1483
|
+
return input.signal.aborted ? null : text;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1242
1486
|
// src/fakeInstance.ts
|
|
1243
1487
|
var yieldTick = () => Promise.resolve();
|
|
1244
1488
|
var FAKE_ASYNC_QUESTIONS = [
|
|
@@ -1265,7 +1509,7 @@ var FAKE_ASYNC_QUESTIONS = [
|
|
|
1265
1509
|
function parseScript(input) {
|
|
1266
1510
|
const envelope = input;
|
|
1267
1511
|
const script = envelope?.config?.script;
|
|
1268
|
-
return
|
|
1512
|
+
return FAKE_SCRIPTS.find((name) => name === script) ?? "plain";
|
|
1269
1513
|
}
|
|
1270
1514
|
var parseReplyText = (input) => {
|
|
1271
1515
|
const envelope = input;
|
|
@@ -1370,121 +1614,12 @@ var createFakeInstance = (config, host) => {
|
|
|
1370
1614
|
if (server === null) return "mcp-send: no MCP server was handed to this session";
|
|
1371
1615
|
return runMcpCalls({ fetch: host.fetch, server, calls, signal });
|
|
1372
1616
|
};
|
|
1373
|
-
const runApprovalToolScript =
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
{
|
|
1380
|
-
streamKind: "reasoning_text",
|
|
1381
|
-
delta: "\u5148\u60F3\u6E05\u695A\u6B65\u9AA4\uFF1A\u7B2C\u4E00\u6B65 ls \u5F53\u524D\u76EE\u5F55\uFF1B\u7B2C\u4E8C\u6B65\u8BFB README.md \u7684\u7B2C\u4E00\u884C\uFF1B\u6700\u540E\u53EA\u56DE\u4E00\u4E2A\u5B57\u3002"
|
|
1382
|
-
},
|
|
1383
|
-
{ turnId }
|
|
1384
|
-
);
|
|
1385
|
-
await yieldTick();
|
|
1386
|
-
if (isAborted()) return null;
|
|
1387
|
-
const approve = async (request) => {
|
|
1388
|
-
emit(
|
|
1389
|
-
"item.started",
|
|
1390
|
-
threadId,
|
|
1391
|
-
{
|
|
1392
|
-
itemType: request.itemType,
|
|
1393
|
-
status: "inProgress",
|
|
1394
|
-
title: request.title,
|
|
1395
|
-
data: request.args
|
|
1396
|
-
},
|
|
1397
|
-
{ turnId, itemId: request.itemId }
|
|
1398
|
-
);
|
|
1399
|
-
emit(
|
|
1400
|
-
"request.opened",
|
|
1401
|
-
threadId,
|
|
1402
|
-
{
|
|
1403
|
-
requestType: request.requestType,
|
|
1404
|
-
toolName: request.toolName,
|
|
1405
|
-
detail: request.detail,
|
|
1406
|
-
args: request.args
|
|
1407
|
-
},
|
|
1408
|
-
{ turnId, requestId: request.requestId }
|
|
1409
|
-
);
|
|
1410
|
-
const decision = await new Promise((resolve) => {
|
|
1411
|
-
let settled = false;
|
|
1412
|
-
const settle = (value) => {
|
|
1413
|
-
if (settled) return;
|
|
1414
|
-
settled = true;
|
|
1415
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1416
|
-
pendingDecisions.delete(request.requestId);
|
|
1417
|
-
resolve(value);
|
|
1418
|
-
};
|
|
1419
|
-
const onAbort = () => settle("cancel");
|
|
1420
|
-
signal?.addEventListener("abort", onAbort);
|
|
1421
|
-
if (isAborted()) {
|
|
1422
|
-
settle("cancel");
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
pendingDecisions.set(request.requestId, settle);
|
|
1426
|
-
});
|
|
1427
|
-
emit(
|
|
1428
|
-
"request.resolved",
|
|
1429
|
-
threadId,
|
|
1430
|
-
{
|
|
1431
|
-
requestType: request.requestType,
|
|
1432
|
-
decision: decision === "allow" ? "accept" : decision === "deny" ? "decline" : "cancel"
|
|
1433
|
-
},
|
|
1434
|
-
{ turnId, requestId: request.requestId }
|
|
1435
|
-
);
|
|
1436
|
-
if (decision === "allow") {
|
|
1437
|
-
emit(
|
|
1438
|
-
"content.delta",
|
|
1439
|
-
threadId,
|
|
1440
|
-
{ streamKind: "command_output", delta: request.output },
|
|
1441
|
-
{ turnId, itemId: request.itemId }
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
emit(
|
|
1445
|
-
"item.completed",
|
|
1446
|
-
threadId,
|
|
1447
|
-
{
|
|
1448
|
-
itemType: request.itemType,
|
|
1449
|
-
status: decision === "allow" ? "completed" : "declined",
|
|
1450
|
-
title: request.title,
|
|
1451
|
-
data: { ...request.args, ...decision === "allow" ? { output: request.output } : {} }
|
|
1452
|
-
},
|
|
1453
|
-
{ turnId, itemId: request.itemId }
|
|
1454
|
-
);
|
|
1455
|
-
return decision;
|
|
1456
|
-
};
|
|
1457
|
-
const first = await approve({
|
|
1458
|
-
itemId: nextId("item"),
|
|
1459
|
-
requestId: nextId("req"),
|
|
1460
|
-
requestType: "exec_command_approval",
|
|
1461
|
-
toolName: "shell",
|
|
1462
|
-
detail: "ls",
|
|
1463
|
-
args: { command: "ls" },
|
|
1464
|
-
itemType: "command_execution",
|
|
1465
|
-
title: "ls",
|
|
1466
|
-
output: "README.md\n"
|
|
1467
|
-
});
|
|
1468
|
-
if (first === "cancel") return null;
|
|
1469
|
-
if (first === "allow") {
|
|
1470
|
-
const second = await approve({
|
|
1471
|
-
itemId: nextId("item"),
|
|
1472
|
-
requestId: nextId("req"),
|
|
1473
|
-
requestType: "file_read_approval",
|
|
1474
|
-
toolName: "Read",
|
|
1475
|
-
detail: "README.md",
|
|
1476
|
-
args: { file_path: "README.md" },
|
|
1477
|
-
itemType: "dynamic_tool_call",
|
|
1478
|
-
title: "Read README.md",
|
|
1479
|
-
output: "# hands-probe-workspace\n"
|
|
1480
|
-
});
|
|
1481
|
-
if (second === "cancel") return null;
|
|
1482
|
-
}
|
|
1483
|
-
if (isAborted()) return null;
|
|
1484
|
-
emit("content.delta", threadId, { streamKind: "assistant_text", delta: fakeReply }, { turnId });
|
|
1485
|
-
await yieldTick();
|
|
1486
|
-
return isAborted() ? null : fakeReply;
|
|
1487
|
-
};
|
|
1617
|
+
const runApprovalToolScript = createApprovalToolScript({
|
|
1618
|
+
emit,
|
|
1619
|
+
nextId,
|
|
1620
|
+
pendingDecisions,
|
|
1621
|
+
yieldTick
|
|
1622
|
+
});
|
|
1488
1623
|
const runScriptedTurn = async (booked, threadId, turnId, signal, input) => {
|
|
1489
1624
|
const aborted = () => signal.aborted;
|
|
1490
1625
|
await yieldTick();
|
|
@@ -1572,6 +1707,16 @@ var createFakeInstance = (config, host) => {
|
|
|
1572
1707
|
]);
|
|
1573
1708
|
return;
|
|
1574
1709
|
}
|
|
1710
|
+
if (script === "replay") {
|
|
1711
|
+
const text = await runReplayTurn({ config, threadId, turnId, signal, emit, nextId });
|
|
1712
|
+
finishTurn(
|
|
1713
|
+
booked,
|
|
1714
|
+
turnId,
|
|
1715
|
+
aborted() || text === null ? "interrupted" : "completed",
|
|
1716
|
+
text === null || text === "" ? [] : [{ kind: "assistant", text }]
|
|
1717
|
+
);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1575
1720
|
if (script === "lane") {
|
|
1576
1721
|
const text = await runLaneTurn({
|
|
1577
1722
|
threadId,
|
|
@@ -1693,7 +1838,7 @@ var createFakeInstance = (config, host) => {
|
|
|
1693
1838
|
signal?.removeEventListener("abort", abortTurn);
|
|
1694
1839
|
if (inflight.get(input.threadId)?.turnId === turnId) inflight.delete(input.threadId);
|
|
1695
1840
|
};
|
|
1696
|
-
const background = script === "approval-tool" || script === "stream" || script === "mcp-send" || script === "visualize" || script === "lane";
|
|
1841
|
+
const background = script === "approval-tool" || script === "stream" || script === "replay" || script === "mcp-send" || script === "visualize" || script === "lane";
|
|
1697
1842
|
if (background) signal?.removeEventListener("abort", abortTurn);
|
|
1698
1843
|
booked.turnWork = runFakeTurn(
|
|
1699
1844
|
run,
|
|
@@ -1751,7 +1896,7 @@ var executors = [
|
|
|
1751
1896
|
];
|
|
1752
1897
|
var index_default = definePlugin({
|
|
1753
1898
|
id: "agent-fake",
|
|
1754
|
-
apiVersion:
|
|
1899
|
+
apiVersion: 25,
|
|
1755
1900
|
executors,
|
|
1756
1901
|
activate(ctx) {
|
|
1757
1902
|
ctx.log.info("fake executor ready");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ycodium-ai/agent-fake",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3236",
|
|
4
4
|
"description": "Minimal fake executor for plugin authors and host orchestration tests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ycodium",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@types/node": "24.12.4",
|
|
22
22
|
"esbuild": "0.28.1",
|
|
23
23
|
"vite-plus": "0.2.2",
|
|
24
|
-
"@ycodium-ai/plugin-api": "0.2.
|
|
24
|
+
"@ycodium-ai/plugin-api": "0.2.3236",
|
|
25
25
|
"ycodium-executor-testing": "0.0.0"
|
|
26
26
|
},
|
|
27
27
|
"ycodium": {
|