@ycodium-ai/agent-fake 0.2.3265 → 0.2.3306
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 +9 -1
- package/dist/index.js +66 -11
- 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), `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)
|
|
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, and a steer whose target turn has ended is refused with `code: "provider-rejected"`; a playbook instance accepts steers too (see [Playbook mode](#playbook-mode)); `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`
|
|
@@ -61,6 +61,14 @@ Replay recorded conversations or drive multi-thread acceptance tests with
|
|
|
61
61
|
machine. Use an absolute path. This option is outside the settings form and applies only to
|
|
62
62
|
`mcp-send`. Each turn runs the next batch, ignoring its input, including host wake-ups and follow-ups.
|
|
63
63
|
|
|
64
|
+
A playbook instance accepts a steer, as a real provider does: a second `sendTurn` carrying the
|
|
65
|
+
running turn's id returns that turn, and when the current batch finishes the next batch runs inside
|
|
66
|
+
the same turn, one steer at a time in arrival order (each still subject to `expectInput`). A steer
|
|
67
|
+
the playbook has no batch for is logged as `playbook has no step for a steered input` and noted in
|
|
68
|
+
the reply. A steer whose target turn has already ended, or is not the running turn, is refused with
|
|
69
|
+
an error carrying `code: "provider-rejected"`, so the host puts the message back in the queue. A plain
|
|
70
|
+
`mcp-send` instance without a playbook still refuses a second turn while one is running.
|
|
71
|
+
|
|
64
72
|
This example assigns one script by thread ID and another by title:
|
|
65
73
|
|
|
66
74
|
```json
|
package/dist/index.js
CHANGED
|
@@ -805,9 +805,6 @@ async function runMcpCalls(input) {
|
|
|
805
805
|
return lines.join("\n");
|
|
806
806
|
}
|
|
807
807
|
|
|
808
|
-
// src/playbook.ts
|
|
809
|
-
import * as NodeFSP from "node:fs/promises";
|
|
810
|
-
|
|
811
808
|
// src/hold.ts
|
|
812
809
|
function hold(ms, signal) {
|
|
813
810
|
if (ms === 0 || signal.aborted) return Promise.resolve();
|
|
@@ -823,6 +820,9 @@ function hold(ms, signal) {
|
|
|
823
820
|
});
|
|
824
821
|
}
|
|
825
822
|
|
|
823
|
+
// src/playbook.ts
|
|
824
|
+
import * as NodeFSP from "node:fs/promises";
|
|
825
|
+
|
|
826
826
|
// src/playbookMcp.ts
|
|
827
827
|
function redactMcpUrls(value, servers) {
|
|
828
828
|
if (typeof value === "string") {
|
|
@@ -895,6 +895,13 @@ function parseStep(value) {
|
|
|
895
895
|
if (!isRecord2(value)) return null;
|
|
896
896
|
if (Object.hasOwn(value, "expectInput"))
|
|
897
897
|
return onlyKeys(value, ["expectInput"]) && isName(value.expectInput) ? { expectInput: value.expectInput } : null;
|
|
898
|
+
if (Object.hasOwn(value, "awaitSteer")) {
|
|
899
|
+
const wait = value.awaitSteer;
|
|
900
|
+
if (!onlyKeys(value, ["awaitSteer"]) || !isRecord2(wait) || !onlyKeys(wait, ["timeoutMs"]))
|
|
901
|
+
return null;
|
|
902
|
+
const timeoutMs = parseTimeout(wait.timeoutMs);
|
|
903
|
+
return timeoutMs === null ? null : { awaitSteer: { timeoutMs } };
|
|
904
|
+
}
|
|
898
905
|
if (Object.hasOwn(value, "wait")) {
|
|
899
906
|
const wait = value.wait;
|
|
900
907
|
if (!onlyKeys(value, ["wait"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
|
|
@@ -962,7 +969,8 @@ function parsePlaybook(text) {
|
|
|
962
969
|
const steps = [];
|
|
963
970
|
for (const entry of batch) {
|
|
964
971
|
const step = parseStep(entry);
|
|
965
|
-
|
|
972
|
+
const last = steps.at(-1);
|
|
973
|
+
if (step === null || "expectInput" in step && steps.length > 0 || last !== void 0 && "awaitSteer" in last)
|
|
966
974
|
return { ok: false, reason: `threads[${index}].turns[${turn}] has an invalid step` };
|
|
967
975
|
steps.push(step);
|
|
968
976
|
}
|
|
@@ -1255,7 +1263,11 @@ function createPlaybookRunner(input) {
|
|
|
1255
1263
|
for (const step of batch) {
|
|
1256
1264
|
if (turn.signal.aborted) break;
|
|
1257
1265
|
if ("expectInput" in step) continue;
|
|
1258
|
-
if ("
|
|
1266
|
+
if ("awaitSteer" in step) {
|
|
1267
|
+
const ready = await turn.awaitSteer?.(step.awaitSteer.timeoutMs, turn.signal) ?? false;
|
|
1268
|
+
if (turn.signal.aborted) break;
|
|
1269
|
+
lines.push(`mcp-send awaitSteer${ready ? ": ready" : " failed: timeout"}`);
|
|
1270
|
+
} else if ("wait" in step) {
|
|
1259
1271
|
const missing = opened.missing(step.wait.server);
|
|
1260
1272
|
if (step.wait.server !== void 0 && missing !== void 0) {
|
|
1261
1273
|
lines.push(`mcp-send ${step.wait.tool} failed: ${missing}`);
|
|
@@ -1540,6 +1552,19 @@ var createFakeInstance = (config, host) => {
|
|
|
1540
1552
|
const previewHoldMs = parsePreviewHoldMs(config);
|
|
1541
1553
|
const laneOptions = laneTurnOptions(config, streamOptions);
|
|
1542
1554
|
const steeredInputs = /* @__PURE__ */ new Map();
|
|
1555
|
+
const steerWaiters = /* @__PURE__ */ new Map();
|
|
1556
|
+
const awaitSteer = (threadId, timeoutMs, signal) => {
|
|
1557
|
+
if ((steeredInputs.get(threadId)?.length ?? 0) > 0) return Promise.resolve(true);
|
|
1558
|
+
const waiters = steerWaiters.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
1559
|
+
steerWaiters.set(threadId, waiters);
|
|
1560
|
+
const arrived = new AbortController();
|
|
1561
|
+
const wake = () => arrived.abort();
|
|
1562
|
+
waiters.add(wake);
|
|
1563
|
+
return hold(timeoutMs, AbortSignal.any([signal, arrived.signal])).then(() => {
|
|
1564
|
+
waiters.delete(wake);
|
|
1565
|
+
return arrived.signal.aborted;
|
|
1566
|
+
});
|
|
1567
|
+
};
|
|
1543
1568
|
const pendingDecisions = /* @__PURE__ */ new Map();
|
|
1544
1569
|
let seq = 0;
|
|
1545
1570
|
let streamRound = 0;
|
|
@@ -1639,7 +1664,8 @@ var createFakeInstance = (config, host) => {
|
|
|
1639
1664
|
fetch: host.fetch,
|
|
1640
1665
|
mcpServers: booked.mcpServers,
|
|
1641
1666
|
emit,
|
|
1642
|
-
nextItemId: () => nextId("item")
|
|
1667
|
+
nextItemId: () => nextId("item"),
|
|
1668
|
+
awaitSteer: (timeoutMs, waitSignal) => awaitSteer(threadId, timeoutMs, waitSignal)
|
|
1643
1669
|
});
|
|
1644
1670
|
const failure = playbookText === null ? parseFailInput(input) : void 0;
|
|
1645
1671
|
if (failure !== void 0) {
|
|
@@ -1689,9 +1715,29 @@ var createFakeInstance = (config, host) => {
|
|
|
1689
1715
|
return;
|
|
1690
1716
|
}
|
|
1691
1717
|
if (script === "mcp-send") {
|
|
1692
|
-
|
|
1718
|
+
let text = playbookText ?? await runMcpSendScript(booked, input, signal);
|
|
1693
1719
|
emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
|
|
1694
|
-
|
|
1720
|
+
for (; ; ) {
|
|
1721
|
+
await yieldTick();
|
|
1722
|
+
const steered = steeredInputs.get(threadId)?.shift();
|
|
1723
|
+
if (steered === void 0 || aborted()) break;
|
|
1724
|
+
const more = await playbook?.runTurn({
|
|
1725
|
+
threadId,
|
|
1726
|
+
turnId,
|
|
1727
|
+
signal,
|
|
1728
|
+
input: steered,
|
|
1729
|
+
fetch: host.fetch,
|
|
1730
|
+
mcpServers: booked.mcpServers,
|
|
1731
|
+
emit,
|
|
1732
|
+
nextItemId: () => nextId("item")
|
|
1733
|
+
});
|
|
1734
|
+
if (more == null)
|
|
1735
|
+
host.log.warn("playbook has no step for a steered input", { threadId, turnId });
|
|
1736
|
+
const delta = `
|
|
1737
|
+
${more ?? "playbook: steered input not handled"}`;
|
|
1738
|
+
text += delta;
|
|
1739
|
+
emit("content.delta", threadId, { streamKind: "assistant_text", delta }, { turnId });
|
|
1740
|
+
}
|
|
1695
1741
|
finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
|
|
1696
1742
|
{ kind: "assistant", text }
|
|
1697
1743
|
]);
|
|
@@ -1814,15 +1860,24 @@ var createFakeInstance = (config, host) => {
|
|
|
1814
1860
|
const booked = requireThread(input.threadId);
|
|
1815
1861
|
if (booked.closing !== void 0) throw new Error("session closing");
|
|
1816
1862
|
const running = inflight.get(input.threadId);
|
|
1863
|
+
const steerable = script === "lane" || playbook !== void 0;
|
|
1817
1864
|
const steered = acceptLaneSteer({
|
|
1818
1865
|
running,
|
|
1819
|
-
enabled:
|
|
1866
|
+
enabled: steerable,
|
|
1820
1867
|
expectedTurnId: input.expectedTurnId,
|
|
1821
1868
|
text: input.input,
|
|
1822
1869
|
threadId: input.threadId,
|
|
1823
1870
|
steered: steeredInputs
|
|
1824
1871
|
});
|
|
1825
|
-
if (steered !== null)
|
|
1872
|
+
if (steered !== null) {
|
|
1873
|
+
if ((steeredInputs.get(input.threadId)?.length ?? 0) > 0)
|
|
1874
|
+
for (const wake of steerWaiters.get(input.threadId) ?? []) wake();
|
|
1875
|
+
return steered;
|
|
1876
|
+
}
|
|
1877
|
+
if (steerable && input.expectedTurnId !== void 0)
|
|
1878
|
+
throw Object.assign(new Error("The target turn has ended. The message was not sent."), {
|
|
1879
|
+
code: "provider-rejected"
|
|
1880
|
+
});
|
|
1826
1881
|
if (running !== void 0) throw new Error("turn already in flight");
|
|
1827
1882
|
const turnId = input.expectedTurnId ?? nextId("turn");
|
|
1828
1883
|
const turnAbort = new AbortController();
|
|
@@ -1901,7 +1956,7 @@ var executors = [
|
|
|
1901
1956
|
];
|
|
1902
1957
|
var index_default = definePlugin({
|
|
1903
1958
|
id: "agent-fake",
|
|
1904
|
-
apiVersion:
|
|
1959
|
+
apiVersion: 34,
|
|
1905
1960
|
executors,
|
|
1906
1961
|
activate(ctx) {
|
|
1907
1962
|
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.3306",
|
|
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.3306",
|
|
25
25
|
"ycodium-executor-testing": "0.0.0"
|
|
26
26
|
},
|
|
27
27
|
"ycodium": {
|