@ycodium-ai/agent-fake 0.2.3272 → 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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/index.js +66 -11
  3. 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
- if (step === null || "expectInput" in step && steps.length > 0)
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 ("wait" in step) {
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
- const text = playbookText ?? await runMcpSendScript(booked, input, signal);
1718
+ let text = playbookText ?? await runMcpSendScript(booked, input, signal);
1693
1719
  emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
1694
- await yieldTick();
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: script === "lane",
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) return steered;
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: 32,
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.3272",
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.3272",
24
+ "@ycodium-ai/plugin-api": "0.2.3306",
25
25
  "ycodium-executor-testing": "0.0.0"
26
26
  },
27
27
  "ycodium": {