@ycodium-ai/agent-fake 0.2.3122 → 0.2.3158

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 +13 -1
  2. package/dist/index.js +47 -21
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -94,13 +94,25 @@ A `wait` polls `tools/list` on that MCP server every 250 ms for the tool name or
94
94
  tool items. A `waitFor`, such as
95
95
  `{ "waitFor": { "tool": "message_inbox_peek", "path": "items", "atLeast": 2 } }`, calls that tool
96
96
  with its optional `arguments` (none by default; placeholders are not resolved there) every 250 ms on one session until the value at the dot `path` of its result is
97
- an array at least `atLeast` long, or a number at least `atLeast`; a tool error counts as not yet.
97
+ an array at least `atLeast` long, or a number at least `atLeast`; any other value found there (for
98
+ example one entry picked by a selector such as `items[title=<encodeURIComponent value>]`) counts
99
+ as 1, and a tool error counts as not yet. Dots inside a selector's brackets do not split the path.
100
+ An optional `id` keeps the result of the ready poll so later calls can reference it with
101
+ `{{step:<id>.<path>}}`; it is not added to the history that `{{step:*…}}` searches.
98
102
  Point it only at read-only tools, since every poll is a real call. It also defaults to 30000 ms,
99
103
  replies `mcp-send waitFor <tool> <path> >= <atLeast>: ready` or `… failed: timeout`, emits no tool
100
104
  items and continues. Calls reply `mcp-send <tool>: <JSON result>` or `mcp-send <tool> failed: <error>` and
101
105
  emit paired `item.started` / `item.completed` events carrying the prefixed tool name, resolved
102
106
  input, raw result and status. Abort stops before the next step and ends the turn as `interrupted`.
103
107
 
108
+ A batch may start with `{ "expectInput": "<text>" }`: it runs only on a turn whose input contains
109
+ that text. Each turn runs the earliest batch that has not run yet and either matches the turn's
110
+ input or, without `expectInput`, is the earliest batch left; so batches keyed by their wake-up text
111
+ can run out of order, and a playbook without `expectInput` still runs batch by batch. A turn that
112
+ matches nothing falls back to the plain `mcp-send` handling below. Recorded replays key each batch
113
+ by the message that started that turn, so wake-ups arriving in a different order still reach the
114
+ right batch. `expectInput` is valid only as a batch's first step.
115
+
104
116
  Threads with no matching line, or with all batches consumed, use the original `mcp-send`
105
117
  input handling: JSON requests call tools and ordinary text gets `replyText`. Omitting
106
118
  `playbookPath` keeps the existing scripts and output unchanged.
package/dist/index.js CHANGED
@@ -666,6 +666,8 @@ var parseTimeout = (value) => {
666
666
  };
667
667
  function parseStep(value) {
668
668
  if (!isRecord2(value)) return null;
669
+ if (Object.hasOwn(value, "expectInput"))
670
+ return onlyKeys(value, ["expectInput"]) && isName(value.expectInput) ? { expectInput: value.expectInput } : null;
669
671
  if (Object.hasOwn(value, "wait")) {
670
672
  const wait = value.wait;
671
673
  if (!onlyKeys(value, ["wait"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
@@ -683,7 +685,7 @@ function parseStep(value) {
683
685
  if (Object.hasOwn(value, "waitFor")) {
684
686
  const wait = value.waitFor;
685
687
  if (!onlyKeys(value, ["waitFor"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
686
- if (!onlyKeys(wait, ["server", "tool", "arguments", "path", "atLeast", "timeoutMs"]) || !isName(wait.path) || wait.arguments !== void 0 && !isRecord2(wait.arguments) || wait.server !== void 0 && !isName(wait.server))
688
+ if (!onlyKeys(wait, ["id", "server", "tool", "arguments", "path", "atLeast", "timeoutMs"]) || !isName(wait.path) || wait.id !== void 0 && !isName(wait.id) || wait.arguments !== void 0 && !isRecord2(wait.arguments) || wait.server !== void 0 && !isName(wait.server))
687
689
  return null;
688
690
  if (typeof wait.atLeast !== "number" || !Number.isInteger(wait.atLeast) || wait.atLeast < 0)
689
691
  return null;
@@ -694,6 +696,7 @@ function parseStep(value) {
694
696
  path: wait.path,
695
697
  atLeast: wait.atLeast,
696
698
  timeoutMs,
699
+ ...wait.id === void 0 ? {} : { id: wait.id },
697
700
  ...wait.server === void 0 ? {} : { server: wait.server },
698
701
  ...wait.arguments === void 0 ? {} : { arguments: wait.arguments }
699
702
  }
@@ -732,7 +735,7 @@ function parsePlaybook(text) {
732
735
  const steps = [];
733
736
  for (const entry of batch) {
734
737
  const step = parseStep(entry);
735
- if (step === null)
738
+ if (step === null || "expectInput" in step && steps.length > 0)
736
739
  return { ok: false, reason: `threads[${index}].turns[${turn}] has an invalid step` };
737
740
  steps.push(step);
738
741
  }
@@ -753,9 +756,10 @@ function parsePlaybook(text) {
753
756
  }
754
757
  var UnresolvedPlaceholder = class extends Error {
755
758
  };
759
+ var splitPath = (path) => path.split(/\.(?![^\[]*\])/u);
756
760
  function placeholderValue(raw, threadId, results, history) {
757
761
  if (raw === "{{thread}}") return threadId;
758
- const [id, ...path] = raw.slice(7, -2).split(/\.(?![^\[]*\])/u);
762
+ const [id, ...path] = splitPath(raw.slice(7, -2));
759
763
  const unresolved = () => new UnresolvedPlaceholder(`unresolved placeholder ${raw}`);
760
764
  const nthAt = path.findIndex((part) => {
761
765
  const selector = parseSelector(part);
@@ -855,6 +859,17 @@ function stepResult(result) {
855
859
  return first.text;
856
860
  }
857
861
  }
862
+ function pickBatch(line, consumed, input) {
863
+ let earliest = true;
864
+ for (const [index, batch] of line.turns.entries()) {
865
+ if (consumed.has(index)) continue;
866
+ const first = batch[0];
867
+ const expected = first !== void 0 && "expectInput" in first ? first.expectInput : void 0;
868
+ if (expected === void 0 ? earliest : input.includes(expected)) return index;
869
+ earliest = false;
870
+ }
871
+ return void 0;
872
+ }
858
873
  async function poll(timeoutMs, input, ready) {
859
874
  const deadline = Date.now() + timeoutMs;
860
875
  const controller = new AbortController();
@@ -885,18 +900,25 @@ var waitForTool = (step, input, clients) => poll(step.wait.timeoutMs, input, asy
885
900
  (tool) => isRecord2(tool) && (tool.name === step.wait.tool || tool.name === `mcp__${step.wait.server ?? "ycodium"}__${step.wait.tool}`)
886
901
  );
887
902
  });
888
- var waitForCount = (step, input, clients) => poll(step.waitFor.timeoutMs, input, async (signal) => {
889
- const called = await clients.request(
890
- step.waitFor.server,
891
- "tools/call",
892
- { name: step.waitFor.tool, arguments: step.waitFor.arguments ?? {} },
893
- signal
894
- );
895
- if (isRecord2(called) && called.isError === true) return false;
896
- const value = valueAt(stepResult(called), step.waitFor.path.split("."));
897
- const count = Array.isArray(value) ? value.length : value;
898
- return typeof count === "number" && count >= step.waitFor.atLeast;
899
- });
903
+ async function waitForCount(step, input, clients) {
904
+ let ready;
905
+ const ok = await poll(step.waitFor.timeoutMs, input, async (signal) => {
906
+ const called = await clients.request(
907
+ step.waitFor.server,
908
+ "tools/call",
909
+ { name: step.waitFor.tool, arguments: step.waitFor.arguments ?? {} },
910
+ signal
911
+ );
912
+ if (isRecord2(called) && called.isError === true) return false;
913
+ const result = stepResult(called);
914
+ const value = valueAt(result, splitPath(step.waitFor.path));
915
+ const count = Array.isArray(value) ? value.length : typeof value === "number" ? value : value === void 0 || value === null ? 0 : 1;
916
+ if (count < step.waitFor.atLeast) return false;
917
+ ready = { result };
918
+ return true;
919
+ });
920
+ return ok ? ready : void 0;
921
+ }
900
922
  async function runCall(step, playback, input, clients) {
901
923
  let args = step.arguments ?? {};
902
924
  let failure;
@@ -981,15 +1003,16 @@ function createPlaybookRunner(input) {
981
1003
  if (index >= 0) claimed.add(index);
982
1004
  playback = {
983
1005
  line: parsed.threads[index] ?? null,
984
- cursor: 0,
1006
+ consumed: /* @__PURE__ */ new Set(),
985
1007
  results: /* @__PURE__ */ new Map(),
986
1008
  history: []
987
1009
  };
988
1010
  sessions.set(turn.threadId, playback);
989
1011
  }
990
- const batch = playback.line?.turns[playback.cursor];
991
- if (batch === void 0) return null;
992
- playback.cursor += 1;
1012
+ const picked = playback.line === null ? void 0 : pickBatch(playback.line, playback.consumed, turn.input ?? "");
1013
+ const batch = picked === void 0 ? void 0 : playback.line?.turns[picked];
1014
+ if (picked === void 0 || batch === void 0) return null;
1015
+ playback.consumed.add(picked);
993
1016
  let opened = clients.get(turn.threadId);
994
1017
  if (opened === void 0) {
995
1018
  opened = createPlaybookMcp({ fetch: turn.fetch, servers: turn.mcpServers });
@@ -1004,6 +1027,7 @@ function createPlaybookRunner(input) {
1004
1027
  const lines = [];
1005
1028
  for (const step of batch) {
1006
1029
  if (turn.signal.aborted) break;
1030
+ if ("expectInput" in step) continue;
1007
1031
  if ("wait" in step) {
1008
1032
  const missing = opened.missing(step.wait.server);
1009
1033
  if (step.wait.server !== void 0 && missing !== void 0) {
@@ -1021,7 +1045,8 @@ function createPlaybookRunner(input) {
1021
1045
  }
1022
1046
  const ready = await waitForCount(step, safeTurn, opened);
1023
1047
  if (turn.signal.aborted) break;
1024
- const { tool, path: path2, atLeast } = step.waitFor;
1048
+ const { id, tool, path: path2, atLeast } = step.waitFor;
1049
+ if (ready !== void 0 && id !== void 0) playback.results.set(id, ready.result);
1025
1050
  lines.push(
1026
1051
  `mcp-send waitFor ${tool} ${path2} >= ${String(atLeast)}${ready ? ": ready" : " failed: timeout"}`
1027
1052
  );
@@ -1391,6 +1416,7 @@ var createFakeInstance = (config, host) => {
1391
1416
  threadId,
1392
1417
  turnId,
1393
1418
  signal,
1419
+ ...input === void 0 ? {} : { input },
1394
1420
  fetch: host.fetch,
1395
1421
  mcpServers: booked.mcpServers,
1396
1422
  emit,
@@ -1652,7 +1678,7 @@ var executors = [
1652
1678
  ];
1653
1679
  var index_default = definePlugin({
1654
1680
  id: "agent-fake",
1655
- apiVersion: 13,
1681
+ apiVersion: 15,
1656
1682
  executors,
1657
1683
  activate(ctx) {
1658
1684
  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.3122",
3
+ "version": "0.2.3158",
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.3122",
24
+ "@ycodium-ai/plugin-api": "0.2.3158",
25
25
  "ycodium-executor-testing": "0.0.0"
26
26
  },
27
27
  "ycodium": {