@ycodium-ai/agent-fake 0.2.3034 → 0.2.3056
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 +77 -1
- package/dist/index.js +326 -27
- 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), `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), `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,82 @@ 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
|
+
## Playbook mode
|
|
27
|
+
|
|
28
|
+
Replay recorded conversations or drive multi-thread acceptance tests with
|
|
29
|
+
`config.script: "mcp-send"` and `config.playbookPath` pointing to a JSON file on the executor's
|
|
30
|
+
machine. Use an absolute path. This option is outside the settings form and applies only to
|
|
31
|
+
`mcp-send`. Each turn runs the next batch, ignoring its input, including host wake-ups and follow-ups.
|
|
32
|
+
|
|
33
|
+
This example assigns one script by thread ID and another by title:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"version": 1,
|
|
38
|
+
"threads": [
|
|
39
|
+
{
|
|
40
|
+
"match": { "threadId": "^team-member:[^:]+:[^:]+:B$" },
|
|
41
|
+
"turns": [
|
|
42
|
+
[{ "id": "list", "tool": "work_list_items" }],
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
"tool": "work_claim_item",
|
|
46
|
+
"arguments": { "workItemId": "{{step:list.items.0.workItemId}}" }
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"match": { "title": "Coordinator" },
|
|
53
|
+
"turns": [
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
"tool": "send_message",
|
|
57
|
+
"arguments": { "threadId": "team-member:demo:run:B", "text": "Continue" }
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
{ "wait": { "tool": "my-plugin_hello", "timeoutMs": 30000 } },
|
|
62
|
+
{ "tool": "my-plugin_hello" }
|
|
63
|
+
]
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`version` must be `1`. Matches need a `threadId` regex (supply `^…$` for whole-ID matching)
|
|
71
|
+
or a case-sensitive `title` substring. Both conditions must hold if both exist; missing titles
|
|
72
|
+
cannot match. On its first turn, a thread claims the first unclaimed matching line in file order.
|
|
73
|
+
Claims, cursors and results survive `stopSession` within the instance. The file is read once
|
|
74
|
+
on first use, including failures: an unreadable or invalid file yields `playbook: <reason>`
|
|
75
|
+
every turn without tools. A new instance is required to reload it.
|
|
76
|
+
|
|
77
|
+
Each batch runs steps in order. Calls take `tool`, optional `id`, and object `arguments`
|
|
78
|
+
(default `{}`). String values at any depth support `{{thread}}` and `{{step:<id>.<path>}}`.
|
|
79
|
+
References can cross turns; dot paths use numeric array indexes, and omitting the path takes
|
|
80
|
+
the whole value. Results prefer `structuredContent`, then JSON parsed from `content[0].text`,
|
|
81
|
+
then that text. A whole-string placeholder preserves types; interpolation JSON-stringifies
|
|
82
|
+
non-strings. Unresolved references skip that call and reply
|
|
83
|
+
`mcp-send <tool> failed: unresolved placeholder <placeholder>`, then continue.
|
|
84
|
+
|
|
85
|
+
A `wait` polls `tools/list` on that MCP server every 250 ms for the tool name or its
|
|
86
|
+
`mcp__ycodium__` prefix, defaulting to a 30000 ms timeout. It replies
|
|
87
|
+
`mcp-send wait <tool>: ready` or `mcp-send wait <tool> failed: timeout`, then continues, with no
|
|
88
|
+
tool items. A `waitFor`, such as
|
|
89
|
+
`{ "waitFor": { "tool": "message_inbox_peek", "path": "items", "atLeast": 2 } }`, calls that tool
|
|
90
|
+
with no arguments every 250 ms on one session until the value at the dot `path` of its result is
|
|
91
|
+
an array at least `atLeast` long, or a number at least `atLeast`; a tool error counts as not yet.
|
|
92
|
+
Point it only at read-only tools, since every poll is a real call. It also defaults to 30000 ms,
|
|
93
|
+
replies `mcp-send waitFor <tool> <path> >= <atLeast>: ready` or `… failed: timeout`, emits no tool
|
|
94
|
+
items and continues. Calls reply `mcp-send <tool>: <JSON result>` or `mcp-send <tool> failed: <error>` and
|
|
95
|
+
emit paired `item.started` / `item.completed` events carrying the prefixed tool name, resolved
|
|
96
|
+
input, raw result and status. Abort stops before the next step and ends the turn as `interrupted`.
|
|
97
|
+
|
|
98
|
+
Threads with no matching line, or with all batches consumed, use the original `mcp-send`
|
|
99
|
+
input handling: JSON requests call tools and ordinary text gets `replyText`. Omitting
|
|
100
|
+
`playbookPath` keeps the existing scripts and output unchanged.
|
|
101
|
+
|
|
26
102
|
## License
|
|
27
103
|
|
|
28
104
|
UNLICENSED.
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,8 @@ var FAKE_CARD = {
|
|
|
60
60
|
holdMs: { type: "integer", minimum: 0 },
|
|
61
61
|
// stream 脚本循环切片的源文本;性能探针用它换上可控的长回复,不进设置表单。
|
|
62
62
|
streamText: { type: "string" },
|
|
63
|
+
// mcp-send 的逐轮剧本文件路径;不进设置表单。
|
|
64
|
+
playbookPath: { type: "string" },
|
|
63
65
|
// 额度与用量怎么答(见 fakeUsage.ts);不进设置表单,开发与验收时手改实例配置。
|
|
64
66
|
usageScript: {
|
|
65
67
|
type: "string",
|
|
@@ -463,7 +465,12 @@ function parseMessage(text) {
|
|
|
463
465
|
return json.length === 0 ? null : JSON.parse(json);
|
|
464
466
|
}
|
|
465
467
|
async function callMcpTool(input) {
|
|
468
|
+
const session = await openMcpSession(input);
|
|
469
|
+
return session.request("tools/call", { name: input.name, arguments: input.arguments });
|
|
470
|
+
}
|
|
471
|
+
async function openMcpSession(input) {
|
|
466
472
|
const post = async (headers, body) => {
|
|
473
|
+
input.signal?.throwIfAborted();
|
|
467
474
|
const response = await input.fetch(input.server.transport.url, {
|
|
468
475
|
method: "POST",
|
|
469
476
|
headers: {
|
|
@@ -471,7 +478,8 @@ async function callMcpTool(input) {
|
|
|
471
478
|
accept: "application/json, text/event-stream",
|
|
472
479
|
...headers
|
|
473
480
|
},
|
|
474
|
-
body: JSON.stringify({ jsonrpc: "2.0", ...body })
|
|
481
|
+
body: JSON.stringify({ jsonrpc: "2.0", ...body }),
|
|
482
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
475
483
|
});
|
|
476
484
|
const text = await response.text();
|
|
477
485
|
if (!response.ok)
|
|
@@ -497,16 +505,16 @@ async function callMcpTool(input) {
|
|
|
497
505
|
...sessionId === null ? {} : { "mcp-session-id": sessionId }
|
|
498
506
|
};
|
|
499
507
|
await post(session, { method: "notifications/initialized" });
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
508
|
+
let nextId = 2;
|
|
509
|
+
return {
|
|
510
|
+
request: async (method, params) => {
|
|
511
|
+
const called = await post(session, { id: nextId++, method, params });
|
|
512
|
+
const label = method === "tools/call" ? `tools/call ${String(params.name)}` : method;
|
|
513
|
+
if (called.message?.error !== void 0)
|
|
514
|
+
throw new Error(`MCP ${label} failed: ${called.message.error.message ?? "error"}`);
|
|
515
|
+
return called.message?.result;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
510
518
|
}
|
|
511
519
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
512
520
|
function parseMcpSendInput(input) {
|
|
@@ -545,6 +553,299 @@ async function runMcpCalls(input) {
|
|
|
545
553
|
return lines.join("\n");
|
|
546
554
|
}
|
|
547
555
|
|
|
556
|
+
// src/playbook.ts
|
|
557
|
+
import * as NodeFSP from "node:fs/promises";
|
|
558
|
+
|
|
559
|
+
// src/hold.ts
|
|
560
|
+
function hold(ms, signal) {
|
|
561
|
+
if (ms === 0 || signal.aborted) return Promise.resolve();
|
|
562
|
+
return new Promise((resolve) => {
|
|
563
|
+
let timer;
|
|
564
|
+
const finish = () => {
|
|
565
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
566
|
+
signal.removeEventListener("abort", finish);
|
|
567
|
+
resolve();
|
|
568
|
+
};
|
|
569
|
+
timer = setTimeout(finish, ms);
|
|
570
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/playbook.ts
|
|
575
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
576
|
+
var isName = (value) => typeof value === "string" && value.length > 0;
|
|
577
|
+
var onlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
578
|
+
var parseTimeout = (value) => {
|
|
579
|
+
const timeoutMs = value === void 0 ? 3e4 : value;
|
|
580
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : null;
|
|
581
|
+
};
|
|
582
|
+
function parseStep(value) {
|
|
583
|
+
if (!isRecord2(value)) return null;
|
|
584
|
+
if (Object.hasOwn(value, "wait")) {
|
|
585
|
+
const wait = value.wait;
|
|
586
|
+
if (!onlyKeys(value, ["wait"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
|
|
587
|
+
if (!onlyKeys(wait, ["tool", "timeoutMs"])) return null;
|
|
588
|
+
const timeoutMs = parseTimeout(wait.timeoutMs);
|
|
589
|
+
return timeoutMs === null ? null : { wait: { tool: wait.tool, timeoutMs } };
|
|
590
|
+
}
|
|
591
|
+
if (Object.hasOwn(value, "waitFor")) {
|
|
592
|
+
const wait = value.waitFor;
|
|
593
|
+
if (!onlyKeys(value, ["waitFor"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
|
|
594
|
+
if (!onlyKeys(wait, ["tool", "path", "atLeast", "timeoutMs"]) || !isName(wait.path))
|
|
595
|
+
return null;
|
|
596
|
+
if (typeof wait.atLeast !== "number" || !Number.isInteger(wait.atLeast) || wait.atLeast < 0)
|
|
597
|
+
return null;
|
|
598
|
+
const timeoutMs = parseTimeout(wait.timeoutMs);
|
|
599
|
+
return timeoutMs === null ? null : { waitFor: { tool: wait.tool, path: wait.path, atLeast: wait.atLeast, timeoutMs } };
|
|
600
|
+
}
|
|
601
|
+
if (!onlyKeys(value, ["id", "tool", "arguments"]) || !isName(value.tool)) return null;
|
|
602
|
+
if (value.id !== void 0 && typeof value.id !== "string") return null;
|
|
603
|
+
if (value.arguments !== void 0 && !isRecord2(value.arguments)) return null;
|
|
604
|
+
return {
|
|
605
|
+
tool: value.tool,
|
|
606
|
+
...value.id === void 0 ? {} : { id: value.id },
|
|
607
|
+
...value.arguments === void 0 ? {} : { arguments: value.arguments }
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function parsePlaybook(text) {
|
|
611
|
+
try {
|
|
612
|
+
const value = JSON.parse(text);
|
|
613
|
+
if (!isRecord2(value) || value.version !== 1) return { ok: false, reason: "version must be 1" };
|
|
614
|
+
if (!onlyKeys(value, ["version", "threads"]) || !Array.isArray(value.threads))
|
|
615
|
+
return { ok: false, reason: "threads must be an array" };
|
|
616
|
+
const threads = [];
|
|
617
|
+
for (const [index, line] of value.threads.entries()) {
|
|
618
|
+
if (!isRecord2(line) || !onlyKeys(line, ["match", "turns"]) || !isRecord2(line.match))
|
|
619
|
+
return { ok: false, reason: `threads[${index}] must have match and turns` };
|
|
620
|
+
const match = line.match;
|
|
621
|
+
if (!onlyKeys(match, ["threadId", "title"]) || match.threadId === void 0 && match.title === void 0 || match.threadId !== void 0 && typeof match.threadId !== "string" || match.title !== void 0 && typeof match.title !== "string")
|
|
622
|
+
return { ok: false, reason: `threads[${index}].match needs threadId or title` };
|
|
623
|
+
if (!Array.isArray(line.turns))
|
|
624
|
+
return { ok: false, reason: `threads[${index}].turns must be an array` };
|
|
625
|
+
const turns = [];
|
|
626
|
+
for (const [turn, batch] of line.turns.entries()) {
|
|
627
|
+
if (!Array.isArray(batch))
|
|
628
|
+
return { ok: false, reason: `threads[${index}].turns[${turn}] must be an array` };
|
|
629
|
+
const steps = [];
|
|
630
|
+
for (const entry of batch) {
|
|
631
|
+
const step = parseStep(entry);
|
|
632
|
+
if (step === null)
|
|
633
|
+
return { ok: false, reason: `threads[${index}].turns[${turn}] has an invalid step` };
|
|
634
|
+
steps.push(step);
|
|
635
|
+
}
|
|
636
|
+
turns.push(steps);
|
|
637
|
+
}
|
|
638
|
+
threads.push({
|
|
639
|
+
match: {
|
|
640
|
+
...typeof match.threadId === "string" ? { threadId: new RegExp(match.threadId) } : {},
|
|
641
|
+
...typeof match.title === "string" ? { title: match.title } : {}
|
|
642
|
+
},
|
|
643
|
+
turns
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return { ok: true, threads };
|
|
647
|
+
} catch (error) {
|
|
648
|
+
return { ok: false, reason: String(error) };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
var UnresolvedPlaceholder = class extends Error {
|
|
652
|
+
};
|
|
653
|
+
function placeholderValue(raw, threadId, results) {
|
|
654
|
+
if (raw === "{{thread}}") return threadId;
|
|
655
|
+
const [id, ...path] = raw.slice(7, -2).split(".");
|
|
656
|
+
const value = id === void 0 || !results.has(id) ? void 0 : valueAt(results.get(id), path);
|
|
657
|
+
if (value === void 0) throw new UnresolvedPlaceholder(`unresolved placeholder ${raw}`);
|
|
658
|
+
return value;
|
|
659
|
+
}
|
|
660
|
+
function valueAt(start, path) {
|
|
661
|
+
let value = start;
|
|
662
|
+
for (const part of path) {
|
|
663
|
+
if (Array.isArray(value) && /^\d+$/.test(part) && Object.hasOwn(value, Number(part))) {
|
|
664
|
+
value = value[Number(part)];
|
|
665
|
+
} else if (isRecord2(value) && Object.hasOwn(value, part)) {
|
|
666
|
+
value = value[part];
|
|
667
|
+
} else {
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
function resolveValue(value, threadId, results) {
|
|
674
|
+
if (typeof value === "string") {
|
|
675
|
+
const placeholders = [...value.matchAll(/\{\{(?:thread|step:[^{}]*)\}\}/g)];
|
|
676
|
+
if (placeholders.length === 1 && placeholders[0]?.[0] === value)
|
|
677
|
+
return placeholderValue(value, threadId, results);
|
|
678
|
+
return value.replace(/\{\{(?:thread|step:[^{}]*)\}\}/g, (raw) => {
|
|
679
|
+
const resolved = placeholderValue(raw, threadId, results);
|
|
680
|
+
return typeof resolved === "string" ? resolved : JSON.stringify(resolved);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (Array.isArray(value)) return value.map((entry) => resolveValue(entry, threadId, results));
|
|
684
|
+
if (isRecord2(value)) return resolveArguments(value, threadId, results);
|
|
685
|
+
return value;
|
|
686
|
+
}
|
|
687
|
+
function resolveArguments(args, threadId, results) {
|
|
688
|
+
return Object.fromEntries(
|
|
689
|
+
Object.entries(args).map(([key, value]) => [key, resolveValue(value, threadId, results)])
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
function stepResult(result) {
|
|
693
|
+
if (!isRecord2(result)) return void 0;
|
|
694
|
+
if (Object.hasOwn(result, "structuredContent")) return result.structuredContent;
|
|
695
|
+
const first = Array.isArray(result.content) ? result.content[0] : void 0;
|
|
696
|
+
if (!isRecord2(first) || typeof first.text !== "string") return void 0;
|
|
697
|
+
try {
|
|
698
|
+
return JSON.parse(first.text);
|
|
699
|
+
} catch {
|
|
700
|
+
return first.text;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async function poll(timeoutMs, input, ready) {
|
|
704
|
+
const server = builtinMcpServer(input.mcpServers);
|
|
705
|
+
const deadline = Date.now() + timeoutMs;
|
|
706
|
+
const controller = new AbortController();
|
|
707
|
+
const abort = () => controller.abort();
|
|
708
|
+
input.signal.addEventListener("abort", abort, { once: true });
|
|
709
|
+
if (input.signal.aborted) abort();
|
|
710
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
711
|
+
try {
|
|
712
|
+
let session;
|
|
713
|
+
while (!controller.signal.aborted && Date.now() < deadline) {
|
|
714
|
+
try {
|
|
715
|
+
if (server !== null)
|
|
716
|
+
session ??= await openMcpSession({
|
|
717
|
+
fetch: input.fetch,
|
|
718
|
+
server,
|
|
719
|
+
signal: controller.signal
|
|
720
|
+
});
|
|
721
|
+
const done = session !== void 0 && await ready(session);
|
|
722
|
+
if (controller.signal.aborted || Date.now() >= deadline) return false;
|
|
723
|
+
if (done) return true;
|
|
724
|
+
} catch {
|
|
725
|
+
session = void 0;
|
|
726
|
+
if (controller.signal.aborted) return false;
|
|
727
|
+
}
|
|
728
|
+
await hold(Math.min(250, Math.max(0, deadline - Date.now())), controller.signal);
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
} finally {
|
|
732
|
+
clearTimeout(timer);
|
|
733
|
+
input.signal.removeEventListener("abort", abort);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
var waitForTool = (step, input) => poll(step.wait.timeoutMs, input, async (session) => {
|
|
737
|
+
const listed = await session.request("tools/list", {});
|
|
738
|
+
return isRecord2(listed) && Array.isArray(listed.tools) && listed.tools.some(
|
|
739
|
+
(tool) => isRecord2(tool) && (tool.name === step.wait.tool || tool.name === `mcp__ycodium__${step.wait.tool}`)
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
var waitForCount = (step, input) => poll(step.waitFor.timeoutMs, input, async (session) => {
|
|
743
|
+
const called = await session.request("tools/call", { name: step.waitFor.tool, arguments: {} });
|
|
744
|
+
if (isRecord2(called) && called.isError === true) return false;
|
|
745
|
+
const value = valueAt(stepResult(called), step.waitFor.path.split("."));
|
|
746
|
+
const count = Array.isArray(value) ? value.length : value;
|
|
747
|
+
return typeof count === "number" && count >= step.waitFor.atLeast;
|
|
748
|
+
});
|
|
749
|
+
async function runCall(step, playback, input) {
|
|
750
|
+
let args = step.arguments ?? {};
|
|
751
|
+
let failure;
|
|
752
|
+
try {
|
|
753
|
+
args = resolveArguments(args, input.threadId, playback.results);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
failure = error instanceof UnresolvedPlaceholder ? error.message : String(error);
|
|
756
|
+
}
|
|
757
|
+
const toolName = `mcp__ycodium__${step.tool}`;
|
|
758
|
+
const extra = { turnId: input.turnId, itemId: input.nextItemId() };
|
|
759
|
+
const payload = { itemType: "mcp_tool_call", title: toolName, data: { toolName, input: args } };
|
|
760
|
+
input.emit("item.started", input.threadId, { ...payload, status: "inProgress" }, extra);
|
|
761
|
+
let result;
|
|
762
|
+
if (failure === void 0) {
|
|
763
|
+
try {
|
|
764
|
+
const server = builtinMcpServer(input.mcpServers);
|
|
765
|
+
if (server === null) throw new Error("no MCP server was handed to this session");
|
|
766
|
+
result = await callMcpTool({
|
|
767
|
+
fetch: input.fetch,
|
|
768
|
+
server,
|
|
769
|
+
name: step.tool,
|
|
770
|
+
arguments: args,
|
|
771
|
+
signal: input.signal
|
|
772
|
+
});
|
|
773
|
+
} catch (error) {
|
|
774
|
+
failure = String(error);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (failure !== void 0) result = { isError: true, content: [{ type: "text", text: failure }] };
|
|
778
|
+
if (step.id !== void 0) playback.results.set(step.id, stepResult(result));
|
|
779
|
+
const failed = failure !== void 0 || isRecord2(result) && result.isError === true;
|
|
780
|
+
input.emit(
|
|
781
|
+
"item.completed",
|
|
782
|
+
input.threadId,
|
|
783
|
+
{
|
|
784
|
+
...payload,
|
|
785
|
+
status: failed ? "failed" : "completed",
|
|
786
|
+
data: { ...payload.data, result }
|
|
787
|
+
},
|
|
788
|
+
extra
|
|
789
|
+
);
|
|
790
|
+
return failure === void 0 ? `mcp-send ${step.tool}: ${JSON.stringify(result)}` : `mcp-send ${step.tool} failed: ${failure}`;
|
|
791
|
+
}
|
|
792
|
+
function createPlaybookRunner(input) {
|
|
793
|
+
const config = isRecord2(input) && isRecord2(input.config) ? input.config : void 0;
|
|
794
|
+
const path = config?.playbookPath;
|
|
795
|
+
if (typeof path !== "string") return void 0;
|
|
796
|
+
const titles = /* @__PURE__ */ new Map();
|
|
797
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
798
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
799
|
+
let loaded;
|
|
800
|
+
return {
|
|
801
|
+
startSession: (session) => {
|
|
802
|
+
titles.set(session.threadId, session.title);
|
|
803
|
+
},
|
|
804
|
+
runTurn: async (turn) => {
|
|
805
|
+
if (turn.signal.aborted) return "";
|
|
806
|
+
loaded ??= NodeFSP.readFile(path, "utf8").then(
|
|
807
|
+
parsePlaybook,
|
|
808
|
+
(error) => ({ ok: false, reason: String(error) })
|
|
809
|
+
);
|
|
810
|
+
const parsed = await loaded;
|
|
811
|
+
if (turn.signal.aborted) return "";
|
|
812
|
+
if (!parsed.ok) return `playbook: ${parsed.reason}`;
|
|
813
|
+
let playback = sessions.get(turn.threadId);
|
|
814
|
+
if (playback === void 0) {
|
|
815
|
+
const title = titles.get(turn.threadId);
|
|
816
|
+
const index = parsed.threads.findIndex(
|
|
817
|
+
(line, index2) => !claimed.has(index2) && (line.match.threadId === void 0 || line.match.threadId.test(turn.threadId)) && (line.match.title === void 0 || title !== void 0 && title.includes(line.match.title))
|
|
818
|
+
);
|
|
819
|
+
if (index >= 0) claimed.add(index);
|
|
820
|
+
playback = { line: parsed.threads[index] ?? null, cursor: 0, results: /* @__PURE__ */ new Map() };
|
|
821
|
+
sessions.set(turn.threadId, playback);
|
|
822
|
+
}
|
|
823
|
+
const batch = playback.line?.turns[playback.cursor];
|
|
824
|
+
if (batch === void 0) return null;
|
|
825
|
+
playback.cursor += 1;
|
|
826
|
+
const lines = [];
|
|
827
|
+
for (const step of batch) {
|
|
828
|
+
if (turn.signal.aborted) break;
|
|
829
|
+
if ("wait" in step) {
|
|
830
|
+
const ready = await waitForTool(step, turn);
|
|
831
|
+
if (turn.signal.aborted) break;
|
|
832
|
+
lines.push(`mcp-send wait ${step.wait.tool}${ready ? ": ready" : " failed: timeout"}`);
|
|
833
|
+
} else if ("waitFor" in step) {
|
|
834
|
+
const ready = await waitForCount(step, turn);
|
|
835
|
+
if (turn.signal.aborted) break;
|
|
836
|
+
const { tool, path: path2, atLeast } = step.waitFor;
|
|
837
|
+
lines.push(
|
|
838
|
+
`mcp-send waitFor ${tool} ${path2} >= ${String(atLeast)}${ready ? ": ready" : " failed: timeout"}`
|
|
839
|
+
);
|
|
840
|
+
} else {
|
|
841
|
+
lines.push(await runCall(step, playback, turn));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return lines.join("\n");
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
548
849
|
// src/streamScript.ts
|
|
549
850
|
var FAKE_STREAM_TEXT = [
|
|
550
851
|
"Here is the plan for the change, step by step.\n\n",
|
|
@@ -589,19 +890,6 @@ function confirmationText(result) {
|
|
|
589
890
|
}
|
|
590
891
|
return chunks.join("\n");
|
|
591
892
|
}
|
|
592
|
-
function hold(ms, signal) {
|
|
593
|
-
if (ms === 0 || signal.aborted) return Promise.resolve();
|
|
594
|
-
return new Promise((resolve) => {
|
|
595
|
-
let timer;
|
|
596
|
-
const finish = () => {
|
|
597
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
598
|
-
signal.removeEventListener("abort", finish);
|
|
599
|
-
resolve();
|
|
600
|
-
};
|
|
601
|
-
timer = setTimeout(finish, ms);
|
|
602
|
-
signal.addEventListener("abort", finish, { once: true });
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
893
|
async function runVisualizeTurn(input) {
|
|
606
894
|
const args = JSON.stringify({ action: "create", title: TITLE, fragment: FRAGMENT });
|
|
607
895
|
const tool = (status, result) => ({
|
|
@@ -728,6 +1016,7 @@ var createFakeInstance = (config, host) => {
|
|
|
728
1016
|
const events = new EventBus();
|
|
729
1017
|
const threads = /* @__PURE__ */ new Map();
|
|
730
1018
|
const script = parseScript(config);
|
|
1019
|
+
const playbook = script === "mcp-send" ? createPlaybookRunner(config) : void 0;
|
|
731
1020
|
const replyText = parseReplyText(config);
|
|
732
1021
|
const failStartSession = parseFailStartSession(config);
|
|
733
1022
|
const streamOptions = parseStreamOptions(config);
|
|
@@ -910,7 +1199,16 @@ var createFakeInstance = (config, host) => {
|
|
|
910
1199
|
finishTurn(booked, turnId, "interrupted", []);
|
|
911
1200
|
return;
|
|
912
1201
|
}
|
|
913
|
-
const
|
|
1202
|
+
const playbookText = playbook === void 0 ? null : await playbook.runTurn({
|
|
1203
|
+
threadId,
|
|
1204
|
+
turnId,
|
|
1205
|
+
signal,
|
|
1206
|
+
fetch: host.fetch,
|
|
1207
|
+
mcpServers: booked.mcpServers,
|
|
1208
|
+
emit,
|
|
1209
|
+
nextItemId: () => nextId("item")
|
|
1210
|
+
});
|
|
1211
|
+
const failure = playbookText === null ? parseFailInput(input) : void 0;
|
|
914
1212
|
if (failure !== void 0) {
|
|
915
1213
|
const { category, message } = failure;
|
|
916
1214
|
emit("runtime.error", threadId, { message, failureCategory: category }, { turnId });
|
|
@@ -958,7 +1256,7 @@ var createFakeInstance = (config, host) => {
|
|
|
958
1256
|
return;
|
|
959
1257
|
}
|
|
960
1258
|
if (script === "mcp-send") {
|
|
961
|
-
const text = await runMcpSendScript(booked, input);
|
|
1259
|
+
const text = playbookText ?? await runMcpSendScript(booked, input);
|
|
962
1260
|
emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
|
|
963
1261
|
await yieldTick();
|
|
964
1262
|
finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
|
|
@@ -1057,6 +1355,7 @@ var createFakeInstance = (config, host) => {
|
|
|
1057
1355
|
...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
|
|
1058
1356
|
};
|
|
1059
1357
|
threads.set(input.threadId, { session, turns: [], mcpServers: input.mcpServers });
|
|
1358
|
+
playbook?.startSession(input);
|
|
1060
1359
|
emit(
|
|
1061
1360
|
"session.started",
|
|
1062
1361
|
input.threadId,
|
|
@@ -1163,7 +1462,7 @@ var executors = [
|
|
|
1163
1462
|
];
|
|
1164
1463
|
var index_default = definePlugin({
|
|
1165
1464
|
id: "agent-fake",
|
|
1166
|
-
apiVersion:
|
|
1465
|
+
apiVersion: 11,
|
|
1167
1466
|
executors,
|
|
1168
1467
|
activate(ctx) {
|
|
1169
1468
|
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.3056",
|
|
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.3056",
|
|
25
25
|
"ycodium-executor-testing": "0.0.0"
|
|
26
26
|
},
|
|
27
27
|
"ycodium": {
|