@ycodium-ai/agent-fake 0.2.3230 → 0.2.3232
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 +264 -127
- 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;
|
|
@@ -1239,6 +1369,112 @@ async function runVisualizeTurn(input) {
|
|
|
1239
1369
|
return resultText.length > 0 ? REPLY : "visualize failed: empty tool result";
|
|
1240
1370
|
}
|
|
1241
1371
|
|
|
1372
|
+
// src/replayScript.ts
|
|
1373
|
+
import * as NodeFSP2 from "node:fs/promises";
|
|
1374
|
+
import * as NodePath2 from "node:path";
|
|
1375
|
+
import { isExecutorLifecycleEvent } from "@ycodium-ai/plugin-api/executor";
|
|
1376
|
+
var IN_TURN_TYPES = /* @__PURE__ */ new Set([
|
|
1377
|
+
"item.started",
|
|
1378
|
+
"item.updated",
|
|
1379
|
+
"item.completed",
|
|
1380
|
+
"content.delta",
|
|
1381
|
+
"task.started",
|
|
1382
|
+
"task.progress",
|
|
1383
|
+
"task.updated",
|
|
1384
|
+
"task.completed",
|
|
1385
|
+
"turn.plan.updated",
|
|
1386
|
+
"turn.proposed.delta",
|
|
1387
|
+
"turn.proposed.completed",
|
|
1388
|
+
"turn.diff.updated",
|
|
1389
|
+
"request.opened",
|
|
1390
|
+
"request.resolved",
|
|
1391
|
+
"user-input.requested",
|
|
1392
|
+
"user-input.resolved",
|
|
1393
|
+
"hook.started",
|
|
1394
|
+
"hook.progress",
|
|
1395
|
+
"hook.completed",
|
|
1396
|
+
"tool.progress",
|
|
1397
|
+
"tool.summary",
|
|
1398
|
+
"tool.denied",
|
|
1399
|
+
"model.rerouted",
|
|
1400
|
+
"files.persisted",
|
|
1401
|
+
"assistant.attachment.produced",
|
|
1402
|
+
"assistant.artifact.produced",
|
|
1403
|
+
"runtime.warning"
|
|
1404
|
+
]);
|
|
1405
|
+
var IDENTITY_FIELDS = [
|
|
1406
|
+
["taskId", "task"],
|
|
1407
|
+
["agentId", "task"],
|
|
1408
|
+
["parentAgentId", "task"],
|
|
1409
|
+
["toolUseId", "item"],
|
|
1410
|
+
["parentToolUseId", "item"]
|
|
1411
|
+
];
|
|
1412
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1413
|
+
function parseEvents(text) {
|
|
1414
|
+
const parsed = JSON.parse(
|
|
1415
|
+
text,
|
|
1416
|
+
(_key, value) => value === "\u27E8undefined\u27E9" ? void 0 : value
|
|
1417
|
+
);
|
|
1418
|
+
const events = isRecord3(parsed) ? parsed.events : parsed;
|
|
1419
|
+
if (!Array.isArray(events))
|
|
1420
|
+
throw new Error("expected ExecutorEvent[] or { events: ExecutorEvent[] }");
|
|
1421
|
+
return events.map((event, index) => {
|
|
1422
|
+
if (!isRecord3(event) || !["type", "eventId", "provider", "threadId", "createdAt"].every(
|
|
1423
|
+
(key) => typeof event[key] === "string"
|
|
1424
|
+
) || !isRecord3(event.payload) || !["turnId", "itemId", "requestId"].every(
|
|
1425
|
+
(key) => event[key] === void 0 || typeof event[key] === "string"
|
|
1426
|
+
))
|
|
1427
|
+
throw new Error(`invalid ExecutorEvent at index ${index}`);
|
|
1428
|
+
return event;
|
|
1429
|
+
}).filter((event) => !isExecutorLifecycleEvent(event) && IN_TURN_TYPES.has(event.type));
|
|
1430
|
+
}
|
|
1431
|
+
async function runReplayTurn(input) {
|
|
1432
|
+
const config = input.config?.config;
|
|
1433
|
+
const path = config?.replayEventsPath;
|
|
1434
|
+
const duration = (key, fallback) => {
|
|
1435
|
+
const value = config?.[key] ?? fallback;
|
|
1436
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
1437
|
+
throw new Error(`replay: ${key} must be a non-negative integer`);
|
|
1438
|
+
return value;
|
|
1439
|
+
};
|
|
1440
|
+
const stepMs = duration("replayStepMs", 30);
|
|
1441
|
+
const taskHoldMs = duration("replayTaskHoldMs", 3e3);
|
|
1442
|
+
if (typeof path !== "string" || !NodePath2.isAbsolute(path))
|
|
1443
|
+
throw new Error("replay: replayEventsPath must be an absolute path");
|
|
1444
|
+
let events;
|
|
1445
|
+
try {
|
|
1446
|
+
events = parseEvents(await NodeFSP2.readFile(path, "utf8"));
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
throw new Error(`replay: ${path}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
1449
|
+
cause: error
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1453
|
+
const remap = (kind, id) => {
|
|
1454
|
+
const key = `${kind}:${id}`;
|
|
1455
|
+
if (!ids.has(key)) ids.set(key, input.nextId(kind));
|
|
1456
|
+
return ids.get(key);
|
|
1457
|
+
};
|
|
1458
|
+
let text = "";
|
|
1459
|
+
for (const [index, event] of events.entries()) {
|
|
1460
|
+
if (index > 0) await hold(stepMs, input.signal);
|
|
1461
|
+
if (input.signal.aborted) return null;
|
|
1462
|
+
const payload = { ...event.payload };
|
|
1463
|
+
for (const [key, kind] of IDENTITY_FIELDS) {
|
|
1464
|
+
if (typeof payload[key] === "string") payload[key] = remap(kind, payload[key]);
|
|
1465
|
+
}
|
|
1466
|
+
input.emit(event.type, input.threadId, payload, {
|
|
1467
|
+
turnId: input.turnId,
|
|
1468
|
+
...event.itemId === void 0 ? {} : { itemId: remap("item", event.itemId) },
|
|
1469
|
+
...event.requestId === void 0 ? {} : { requestId: remap("request", event.requestId) }
|
|
1470
|
+
});
|
|
1471
|
+
if (event.type === "content.delta" && payload.streamKind === "assistant_text" && typeof payload.delta === "string")
|
|
1472
|
+
text += payload.delta;
|
|
1473
|
+
if (event.type === "task.started") await hold(taskHoldMs, input.signal);
|
|
1474
|
+
}
|
|
1475
|
+
return input.signal.aborted ? null : text;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1242
1478
|
// src/fakeInstance.ts
|
|
1243
1479
|
var yieldTick = () => Promise.resolve();
|
|
1244
1480
|
var FAKE_ASYNC_QUESTIONS = [
|
|
@@ -1265,7 +1501,7 @@ var FAKE_ASYNC_QUESTIONS = [
|
|
|
1265
1501
|
function parseScript(input) {
|
|
1266
1502
|
const envelope = input;
|
|
1267
1503
|
const script = envelope?.config?.script;
|
|
1268
|
-
return
|
|
1504
|
+
return FAKE_SCRIPTS.find((name) => name === script) ?? "plain";
|
|
1269
1505
|
}
|
|
1270
1506
|
var parseReplyText = (input) => {
|
|
1271
1507
|
const envelope = input;
|
|
@@ -1370,121 +1606,12 @@ var createFakeInstance = (config, host) => {
|
|
|
1370
1606
|
if (server === null) return "mcp-send: no MCP server was handed to this session";
|
|
1371
1607
|
return runMcpCalls({ fetch: host.fetch, server, calls, signal });
|
|
1372
1608
|
};
|
|
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
|
-
};
|
|
1609
|
+
const runApprovalToolScript = createApprovalToolScript({
|
|
1610
|
+
emit,
|
|
1611
|
+
nextId,
|
|
1612
|
+
pendingDecisions,
|
|
1613
|
+
yieldTick
|
|
1614
|
+
});
|
|
1488
1615
|
const runScriptedTurn = async (booked, threadId, turnId, signal, input) => {
|
|
1489
1616
|
const aborted = () => signal.aborted;
|
|
1490
1617
|
await yieldTick();
|
|
@@ -1572,6 +1699,16 @@ var createFakeInstance = (config, host) => {
|
|
|
1572
1699
|
]);
|
|
1573
1700
|
return;
|
|
1574
1701
|
}
|
|
1702
|
+
if (script === "replay") {
|
|
1703
|
+
const text = await runReplayTurn({ config, threadId, turnId, signal, emit, nextId });
|
|
1704
|
+
finishTurn(
|
|
1705
|
+
booked,
|
|
1706
|
+
turnId,
|
|
1707
|
+
aborted() || text === null ? "interrupted" : "completed",
|
|
1708
|
+
text === null || text === "" ? [] : [{ kind: "assistant", text }]
|
|
1709
|
+
);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1575
1712
|
if (script === "lane") {
|
|
1576
1713
|
const text = await runLaneTurn({
|
|
1577
1714
|
threadId,
|
|
@@ -1693,7 +1830,7 @@ var createFakeInstance = (config, host) => {
|
|
|
1693
1830
|
signal?.removeEventListener("abort", abortTurn);
|
|
1694
1831
|
if (inflight.get(input.threadId)?.turnId === turnId) inflight.delete(input.threadId);
|
|
1695
1832
|
};
|
|
1696
|
-
const background = script === "approval-tool" || script === "stream" || script === "mcp-send" || script === "visualize" || script === "lane";
|
|
1833
|
+
const background = script === "approval-tool" || script === "stream" || script === "replay" || script === "mcp-send" || script === "visualize" || script === "lane";
|
|
1697
1834
|
if (background) signal?.removeEventListener("abort", abortTurn);
|
|
1698
1835
|
booked.turnWork = runFakeTurn(
|
|
1699
1836
|
run,
|
|
@@ -1751,7 +1888,7 @@ var executors = [
|
|
|
1751
1888
|
];
|
|
1752
1889
|
var index_default = definePlugin({
|
|
1753
1890
|
id: "agent-fake",
|
|
1754
|
-
apiVersion:
|
|
1891
|
+
apiVersion: 24,
|
|
1755
1892
|
executors,
|
|
1756
1893
|
activate(ctx) {
|
|
1757
1894
|
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.3232",
|
|
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.3232",
|
|
25
25
|
"ycodium-executor-testing": "0.0.0"
|
|
26
26
|
},
|
|
27
27
|
"ycodium": {
|