@ycodium-ai/agent-fake 0.2.3976 → 0.2.3982

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 (2) hide show
  1. package/dist/index.js +269 -191
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { definePlugin } from "@ycodium-ai/plugin-api";
3
3
 
4
4
  // ../../packages/plugin-api/src/version.ts
5
- var PLUGIN_API_VERSION = 75;
5
+ var PLUGIN_API_VERSION = 76;
6
6
 
7
7
  // src/card.ts
8
8
  var FAKE_SCRIPTS = [
@@ -101,38 +101,32 @@ var FAKE_CARD = {
101
101
  import * as NodeCrypto from "node:crypto";
102
102
 
103
103
  // src/fakeEventBus.ts
104
- var EventBus = class {
105
- #queued = [];
106
- #waiters = [];
107
- #closed = false;
108
- push(event) {
109
- const waiter = this.#waiters.shift();
110
- if (waiter) {
111
- waiter({ done: false, value: event });
112
- return;
113
- }
114
- this.#queued.push(event);
115
- }
116
- close() {
117
- if (this.#closed) return;
118
- this.#closed = true;
119
- while (this.#waiters.length > 0) {
120
- this.#waiters.shift()?.({ done: true, value: void 0 });
104
+ function createFakeEmitter(input) {
105
+ return (type, threadId, payload, extra) => {
106
+ const context = input.thread(threadId);
107
+ if (!context) return;
108
+ if (type.startsWith("task.")) {
109
+ const task = payload;
110
+ if (type === "task.completed" || ["completed", "failed", "cancelled", "interrupted", "idle"].includes(task.status ?? ""))
111
+ context.tasks.delete(task.taskId);
112
+ else context.tasks.set(task.taskId, { ...context.tasks.get(task.taskId), ...task });
121
113
  }
122
- }
123
- [Symbol.asyncIterator]() {
124
- return {
125
- next: () => {
126
- const queued = this.#queued.shift();
127
- if (queued !== void 0) return Promise.resolve({ done: false, value: queued });
128
- if (this.#closed) return Promise.resolve({ done: true, value: void 0 });
129
- return new Promise((resolve) => {
130
- this.#waiters.push(resolve);
131
- });
132
- }
133
- };
134
- }
135
- };
114
+ context.events.emit({
115
+ eventId: input.nextId(),
116
+ provider: "fake",
117
+ providerInstanceId: input.instanceId,
118
+ threadId,
119
+ createdAt: input.now(),
120
+ type,
121
+ payload,
122
+ ...extra
123
+ });
124
+ };
125
+ }
126
+ function settleFakeTasks(context, threadId, emit) {
127
+ for (const [taskId, task] of context.tasks)
128
+ emit("task.completed", threadId, { ...task, taskId, status: "stopped" });
129
+ }
136
130
 
137
131
  // src/approvalToolScript.ts
138
132
  function createApprovalToolScript({
@@ -302,7 +296,6 @@ function createFakeSessionLifecycle(input) {
302
296
  if (result.status === "rejected") throw result.reason;
303
297
  }
304
298
  await input.disposePlaybook();
305
- input.closeEvents();
306
299
  })().then(disposing.resolve, disposing.reject);
307
300
  return disposal;
308
301
  }
@@ -888,115 +881,8 @@ function createPlaybookMcp(input) {
888
881
  };
889
882
  }
890
883
 
891
- // src/playbook.ts
892
- var lineClaimsSession = (line, threadId, title) => (line.match.threadId === void 0 || line.match.threadId.test(threadId)) && (line.match.title === void 0 || title !== void 0 && title.includes(line.match.title));
884
+ // src/playbookResolve.ts
893
885
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
894
- var isName = (value) => typeof value === "string" && value.length > 0;
895
- var onlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
896
- var parseTimeout = (value) => {
897
- const timeoutMs = value === void 0 ? 3e4 : value;
898
- return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : null;
899
- };
900
- function parseStep(value) {
901
- if (!isRecord2(value)) return null;
902
- if (Object.hasOwn(value, "expectInput"))
903
- return onlyKeys(value, ["expectInput"]) && isName(value.expectInput) ? { expectInput: value.expectInput } : null;
904
- if (Object.hasOwn(value, "awaitSteer")) {
905
- const wait = value.awaitSteer;
906
- if (!onlyKeys(value, ["awaitSteer"]) || !isRecord2(wait) || !onlyKeys(wait, ["timeoutMs", "continueOnTimeout"]) || Object.hasOwn(wait, "continueOnTimeout") && wait.continueOnTimeout !== true)
907
- return null;
908
- const timeoutMs = parseTimeout(wait.timeoutMs);
909
- if (timeoutMs === null) return null;
910
- const onTimeout = wait.continueOnTimeout === true ? { continueOnTimeout: true } : {};
911
- return { awaitSteer: { timeoutMs, ...onTimeout } };
912
- }
913
- if (Object.hasOwn(value, "wait")) {
914
- const wait = value.wait;
915
- if (!onlyKeys(value, ["wait"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
916
- if (!onlyKeys(wait, ["server", "tool", "timeoutMs"]) || wait.server !== void 0 && !isName(wait.server))
917
- return null;
918
- const timeoutMs = parseTimeout(wait.timeoutMs);
919
- return timeoutMs === null ? null : {
920
- wait: {
921
- tool: wait.tool,
922
- timeoutMs,
923
- ...wait.server === void 0 ? {} : { server: wait.server }
924
- }
925
- };
926
- }
927
- if (Object.hasOwn(value, "waitFor")) {
928
- const wait = value.waitFor;
929
- if (!onlyKeys(value, ["waitFor"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
930
- 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))
931
- return null;
932
- if (typeof wait.atLeast !== "number" || !Number.isInteger(wait.atLeast) || wait.atLeast < 0)
933
- return null;
934
- const timeoutMs = parseTimeout(wait.timeoutMs);
935
- return timeoutMs === null ? null : {
936
- waitFor: {
937
- tool: wait.tool,
938
- path: wait.path,
939
- atLeast: wait.atLeast,
940
- timeoutMs,
941
- ...wait.id === void 0 ? {} : { id: wait.id },
942
- ...wait.server === void 0 ? {} : { server: wait.server },
943
- ...wait.arguments === void 0 ? {} : { arguments: wait.arguments }
944
- }
945
- };
946
- }
947
- if (!onlyKeys(value, ["id", "server", "tool", "arguments"]) || !isName(value.tool) || value.server !== void 0 && !isName(value.server))
948
- return null;
949
- if (value.id !== void 0 && typeof value.id !== "string") return null;
950
- if (value.arguments !== void 0 && !isRecord2(value.arguments)) return null;
951
- return {
952
- tool: value.tool,
953
- ...value.server === void 0 ? {} : { server: value.server },
954
- ...value.id === void 0 ? {} : { id: value.id },
955
- ...value.arguments === void 0 ? {} : { arguments: value.arguments }
956
- };
957
- }
958
- function parsePlaybook(text) {
959
- try {
960
- const value = JSON.parse(text);
961
- if (!isRecord2(value) || value.version !== 1) return { ok: false, reason: "version must be 1" };
962
- if (!onlyKeys(value, ["version", "threads"]) || !Array.isArray(value.threads))
963
- return { ok: false, reason: "threads must be an array" };
964
- const threads = [];
965
- for (const [index, line] of value.threads.entries()) {
966
- if (!isRecord2(line) || !onlyKeys(line, ["match", "turns"]) || !isRecord2(line.match))
967
- return { ok: false, reason: `threads[${index}] must have match and turns` };
968
- const match = line.match;
969
- 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")
970
- return { ok: false, reason: `threads[${index}].match needs threadId or title` };
971
- if (!Array.isArray(line.turns))
972
- return { ok: false, reason: `threads[${index}].turns must be an array` };
973
- const turns = [];
974
- for (const [turn, batch] of line.turns.entries()) {
975
- if (!Array.isArray(batch))
976
- return { ok: false, reason: `threads[${index}].turns[${turn}] must be an array` };
977
- const steps = [];
978
- for (const entry of batch) {
979
- const step = parseStep(entry);
980
- const last = steps.at(-1);
981
- if (step === null || "expectInput" in step && steps.length > 0 || last !== void 0 && "awaitSteer" in last)
982
- return { ok: false, reason: `threads[${index}].turns[${turn}] has an invalid step` };
983
- steps.push(step);
984
- }
985
- turns.push(steps);
986
- }
987
- threads.push({
988
- match: {
989
- ...typeof match.threadId === "string" ? { threadId: new RegExp(match.threadId) } : {},
990
- ...typeof match.title === "string" ? { title: match.title } : {}
991
- },
992
- turns
993
- });
994
- }
995
- return { ok: true, threads };
996
- } catch (error) {
997
- return { ok: false, reason: String(error) };
998
- }
999
- }
1000
886
  var UnresolvedPlaceholder = class extends Error {
1001
887
  };
1002
888
  var splitPath = (path) => path.split(/\.(?![^\[]*\])/u);
@@ -1102,6 +988,127 @@ function stepResult(result) {
1102
988
  return first.text;
1103
989
  }
1104
990
  }
991
+
992
+ // src/playbook.ts
993
+ var lineClaimsSession = (line, threadId, title) => (line.match.threadId === void 0 || line.match.threadId.test(threadId)) && (line.match.title === void 0 || title !== void 0 && title.includes(line.match.title));
994
+ var isName = (value) => typeof value === "string" && value.length > 0;
995
+ var onlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
996
+ var parseTimeout = (value) => {
997
+ const timeoutMs = value === void 0 ? 3e4 : value;
998
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : null;
999
+ };
1000
+ function parseStep(value) {
1001
+ if (!isRecord2(value)) return null;
1002
+ if (Object.hasOwn(value, "expectInput"))
1003
+ return onlyKeys(value, ["expectInput"]) && isName(value.expectInput) ? { expectInput: value.expectInput } : null;
1004
+ if (Object.hasOwn(value, "awaitSteer")) {
1005
+ const wait = value.awaitSteer;
1006
+ if (!onlyKeys(value, ["awaitSteer"]) || !isRecord2(wait) || !onlyKeys(wait, ["timeoutMs", "continueOnTimeout"]) || Object.hasOwn(wait, "continueOnTimeout") && wait.continueOnTimeout !== true)
1007
+ return null;
1008
+ const timeoutMs = parseTimeout(wait.timeoutMs);
1009
+ if (timeoutMs === null) return null;
1010
+ const onTimeout = wait.continueOnTimeout === true ? { continueOnTimeout: true } : {};
1011
+ return { awaitSteer: { timeoutMs, ...onTimeout } };
1012
+ }
1013
+ if (Object.hasOwn(value, "wait")) {
1014
+ const wait = value.wait;
1015
+ if (!onlyKeys(value, ["wait"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
1016
+ if (!onlyKeys(wait, ["server", "tool", "timeoutMs"]) || wait.server !== void 0 && !isName(wait.server))
1017
+ return null;
1018
+ const timeoutMs = parseTimeout(wait.timeoutMs);
1019
+ return timeoutMs === null ? null : {
1020
+ wait: {
1021
+ tool: wait.tool,
1022
+ timeoutMs,
1023
+ ...wait.server === void 0 ? {} : { server: wait.server }
1024
+ }
1025
+ };
1026
+ }
1027
+ if (Object.hasOwn(value, "waitFor")) {
1028
+ const wait = value.waitFor;
1029
+ if (!onlyKeys(value, ["waitFor"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
1030
+ if (!onlyKeys(wait, [
1031
+ "id",
1032
+ "server",
1033
+ "tool",
1034
+ "arguments",
1035
+ "path",
1036
+ "atLeast",
1037
+ "timeoutMs",
1038
+ "turnGate",
1039
+ "continueOnTimeout"
1040
+ ]) || wait.continueOnTimeout !== void 0 && wait.continueOnTimeout !== true || wait.turnGate !== void 0 && wait.turnGate !== true || !isName(wait.path) || wait.id !== void 0 && !isName(wait.id) || wait.arguments !== void 0 && !isRecord2(wait.arguments) || wait.server !== void 0 && !isName(wait.server))
1041
+ return null;
1042
+ if (typeof wait.atLeast !== "number" || !Number.isInteger(wait.atLeast) || wait.atLeast < 0)
1043
+ return null;
1044
+ const timeoutMs = parseTimeout(wait.timeoutMs);
1045
+ return timeoutMs === null ? null : {
1046
+ waitFor: {
1047
+ tool: wait.tool,
1048
+ path: wait.path,
1049
+ atLeast: wait.atLeast,
1050
+ timeoutMs,
1051
+ ...wait.id === void 0 ? {} : { id: wait.id },
1052
+ ...wait.server === void 0 ? {} : { server: wait.server },
1053
+ ...wait.arguments === void 0 ? {} : { arguments: wait.arguments },
1054
+ ...wait.turnGate === true ? { turnGate: true } : {},
1055
+ ...wait.continueOnTimeout === true ? { continueOnTimeout: true } : {}
1056
+ }
1057
+ };
1058
+ }
1059
+ if (!onlyKeys(value, ["id", "server", "tool", "arguments"]) || !isName(value.tool) || value.server !== void 0 && !isName(value.server))
1060
+ return null;
1061
+ if (value.id !== void 0 && typeof value.id !== "string") return null;
1062
+ if (value.arguments !== void 0 && !isRecord2(value.arguments)) return null;
1063
+ return {
1064
+ tool: value.tool,
1065
+ ...value.server === void 0 ? {} : { server: value.server },
1066
+ ...value.id === void 0 ? {} : { id: value.id },
1067
+ ...value.arguments === void 0 ? {} : { arguments: value.arguments }
1068
+ };
1069
+ }
1070
+ function parsePlaybook(text) {
1071
+ try {
1072
+ const value = JSON.parse(text);
1073
+ if (!isRecord2(value) || value.version !== 1) return { ok: false, reason: "version must be 1" };
1074
+ if (!onlyKeys(value, ["version", "threads"]) || !Array.isArray(value.threads))
1075
+ return { ok: false, reason: "threads must be an array" };
1076
+ const threads = [];
1077
+ for (const [index, line] of value.threads.entries()) {
1078
+ if (!isRecord2(line) || !onlyKeys(line, ["match", "turns"]) || !isRecord2(line.match))
1079
+ return { ok: false, reason: `threads[${index}] must have match and turns` };
1080
+ const match = line.match;
1081
+ 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")
1082
+ return { ok: false, reason: `threads[${index}].match needs threadId or title` };
1083
+ if (!Array.isArray(line.turns))
1084
+ return { ok: false, reason: `threads[${index}].turns must be an array` };
1085
+ const turns = [];
1086
+ for (const [turn, batch] of line.turns.entries()) {
1087
+ if (!Array.isArray(batch))
1088
+ return { ok: false, reason: `threads[${index}].turns[${turn}] must be an array` };
1089
+ const steps = [];
1090
+ for (const entry of batch) {
1091
+ const step = parseStep(entry);
1092
+ const last = steps.at(-1);
1093
+ if (step === null || "expectInput" in step && steps.length > 0 || last !== void 0 && "awaitSteer" in last)
1094
+ return { ok: false, reason: `threads[${index}].turns[${turn}] has an invalid step` };
1095
+ steps.push(step);
1096
+ }
1097
+ turns.push(steps);
1098
+ }
1099
+ threads.push({
1100
+ match: {
1101
+ ...typeof match.threadId === "string" ? { threadId: new RegExp(match.threadId) } : {},
1102
+ ...typeof match.title === "string" ? { title: match.title } : {}
1103
+ },
1104
+ turns
1105
+ });
1106
+ }
1107
+ return { ok: true, threads };
1108
+ } catch (error) {
1109
+ return { ok: false, reason: String(error) };
1110
+ }
1111
+ }
1105
1112
  function pickBatch(line, consumed, input) {
1106
1113
  let earliest = true;
1107
1114
  for (const [index, batch] of line.turns.entries()) {
@@ -1143,25 +1150,38 @@ var waitForTool = (step, input, clients) => poll(step.wait.timeoutMs, input, asy
1143
1150
  (tool) => isRecord2(tool) && (tool.name === step.wait.tool || tool.name === `mcp__${step.wait.server ?? "ycodium"}__${step.wait.tool}`)
1144
1151
  );
1145
1152
  });
1153
+ async function readCount(step, args, clients, signal) {
1154
+ const called = await clients.request(
1155
+ step.waitFor.server,
1156
+ "tools/call",
1157
+ { name: step.waitFor.tool, arguments: args },
1158
+ signal
1159
+ );
1160
+ if (isRecord2(called) && called.isError === true) return void 0;
1161
+ const result = stepResult(called);
1162
+ const value = valueAt(result, splitPath(step.waitFor.path));
1163
+ const count = Array.isArray(value) ? value.length : typeof value === "number" ? value : value === void 0 || value === null ? 0 : 1;
1164
+ return count < step.waitFor.atLeast ? void 0 : { result };
1165
+ }
1146
1166
  async function waitForCount(step, args, input, clients) {
1147
1167
  let ready;
1148
1168
  const ok = await poll(step.waitFor.timeoutMs, input, async (signal) => {
1149
- const called = await clients.request(
1150
- step.waitFor.server,
1151
- "tools/call",
1152
- { name: step.waitFor.tool, arguments: args },
1153
- signal
1154
- );
1155
- if (isRecord2(called) && called.isError === true) return false;
1156
- const result = stepResult(called);
1157
- const value = valueAt(result, splitPath(step.waitFor.path));
1158
- const count = Array.isArray(value) ? value.length : typeof value === "number" ? value : value === void 0 || value === null ? 0 : 1;
1159
- if (count < step.waitFor.atLeast) return false;
1160
- ready = { result };
1161
- return true;
1169
+ ready = await readCount(step, args, clients, signal);
1170
+ return ready !== void 0;
1162
1171
  });
1163
1172
  return ok ? ready : void 0;
1164
1173
  }
1174
+ var REQUIRED_WAIT_TOOLS = /* @__PURE__ */ new Set([
1175
+ "work_history",
1176
+ "work_list_comments",
1177
+ "work_list_items"
1178
+ ]);
1179
+ var GATE_PROBE_TIMEOUT_MS = 5e3;
1180
+ var REQUIRED_WAIT_FAILED = "playbook required wait failed:";
1181
+ function turnGateOf(batch) {
1182
+ const first = batch.find((step) => !("expectInput" in step));
1183
+ return first !== void 0 && "waitFor" in first && first.waitFor.turnGate === true ? first : void 0;
1184
+ }
1165
1185
  async function runCall(step, playback, input, clients) {
1166
1186
  let args = step.arguments ?? {};
1167
1187
  let failure;
@@ -1251,22 +1271,21 @@ function createPlaybookRunner(input) {
1251
1271
  if (playback === void 0) {
1252
1272
  const title = titles.get(turn.threadId);
1253
1273
  const index = parsed.threads.findIndex(
1254
- (line, index2) => !claimed.has(index2) && lineClaimsSession(line, turn.threadId, title)
1274
+ (line2, index2) => !claimed.has(index2) && lineClaimsSession(line2, turn.threadId, title)
1255
1275
  );
1256
1276
  if (index >= 0) claimed.add(index);
1257
1277
  playback = {
1258
1278
  line: parsed.threads[index] ?? null,
1259
1279
  consumed: /* @__PURE__ */ new Set(),
1280
+ owed: /* @__PURE__ */ new Set(),
1260
1281
  results: /* @__PURE__ */ new Map(),
1261
1282
  history: []
1262
1283
  };
1263
1284
  sessions.set(turn.threadId, playback);
1264
1285
  }
1265
- const picked = playback.line === null ? void 0 : pickBatch(playback.line, playback.consumed, turn.input ?? "");
1266
- const batch = picked === void 0 ? void 0 : playback.line?.turns[picked];
1267
- if (picked === void 0 || batch === void 0) return null;
1268
- playback.consumed.add(picked);
1269
- playback.current = picked;
1286
+ const { line } = playback;
1287
+ const picked = line === null ? void 0 : pickBatch(line, playback.consumed, turn.input ?? "");
1288
+ if (picked === void 0 && playback.owed.size === 0) return null;
1270
1289
  let opened = clients.get(turn.threadId);
1271
1290
  if (opened === void 0) {
1272
1291
  opened = createPlaybookMcp({ fetch: turn.fetch, servers: turn.mcpServers });
@@ -1278,16 +1297,75 @@ function createPlaybookRunner(input) {
1278
1297
  turn.emit(args[0], args[1], redactMcpUrls(args[2], turn.mcpServers), args[3]);
1279
1298
  }
1280
1299
  };
1300
+ const gateMet = async (gate) => {
1301
+ try {
1302
+ const args = resolveArguments(
1303
+ gate.waitFor.arguments ?? {},
1304
+ turn.threadId,
1305
+ playback.results,
1306
+ playback.history
1307
+ );
1308
+ const signal = AbortSignal.any([
1309
+ turn.signal,
1310
+ AbortSignal.timeout(Math.min(GATE_PROBE_TIMEOUT_MS, gate.waitFor.timeoutMs))
1311
+ ]);
1312
+ return await readCount(gate, args, opened, signal) !== void 0;
1313
+ } catch {
1314
+ return false;
1315
+ }
1316
+ };
1281
1317
  const lines = [];
1282
- const queue = [...batch];
1283
- for (let step = queue.shift(); step !== void 0; step = queue.shift()) {
1318
+ const queue = [];
1319
+ const pickedBatch = picked === void 0 ? void 0 : line?.turns[picked];
1320
+ if (picked !== void 0 && pickedBatch !== void 0) {
1321
+ const gate = turnGateOf(pickedBatch);
1322
+ const aheadPending = [...line?.turns.keys() ?? []].some(
1323
+ (index) => index < picked && !playback.consumed.has(index)
1324
+ );
1325
+ if (gate !== void 0 && aheadPending && !await gateMet(gate)) {
1326
+ if (turn.signal.aborted) return "";
1327
+ playback.owed.add(picked);
1328
+ return `mcp-send deferred batch ${String(picked)}: ${gate.waitFor.tool} ${gate.waitFor.path} < ${String(gate.waitFor.atLeast)}`;
1329
+ }
1330
+ playback.consumed.add(picked);
1331
+ playback.owed.delete(picked);
1332
+ playback.current = picked;
1333
+ queue.push(...pickedBatch);
1334
+ }
1335
+ let ran = pickedBatch !== void 0;
1336
+ let halted = false;
1337
+ const next = async () => {
1338
+ while (queue.length === 0 && !halted && !turn.signal.aborted) {
1339
+ const [owed] = [...playback.owed].sort((left, right) => left - right);
1340
+ const batch = owed === void 0 ? void 0 : line?.turns[owed];
1341
+ if (owed === void 0 || batch === void 0) break;
1342
+ const gate = turnGateOf(batch);
1343
+ if (gate !== void 0 && !await gateMet(gate)) break;
1344
+ playback.owed.delete(owed);
1345
+ playback.consumed.add(owed);
1346
+ playback.current = owed;
1347
+ queue.push(...batch);
1348
+ ran = true;
1349
+ }
1350
+ return queue.shift();
1351
+ };
1352
+ const haltRequired = (step, reason) => {
1353
+ const { tool, path: path2, atLeast } = step.waitFor;
1354
+ const who = line?.match.title ?? line?.match.threadId?.source ?? "";
1355
+ lines.push(
1356
+ `${REQUIRED_WAIT_FAILED} session ${turn.threadId} line ${who} batch ${String(playback.current)}: ${tool} ${path2} >= ${String(atLeast)} ${reason}`
1357
+ );
1358
+ halted = true;
1359
+ queue.length = 0;
1360
+ };
1361
+ for (let step = await next(); step !== void 0; step = await next()) {
1284
1362
  if (turn.signal.aborted) break;
1285
1363
  if ("expectInput" in step) continue;
1286
1364
  if ("awaitSteer" in step) {
1287
1365
  const ready = await turn.awaitSteer?.(step.awaitSteer.timeoutMs, turn.signal) ?? false;
1288
1366
  if (turn.signal.aborted) break;
1289
1367
  lines.push(`mcp-send awaitSteer${ready ? ": ready" : " failed: timeout"}`);
1290
- const after = (playback.current ?? picked) + 1;
1368
+ const after = (playback.current ?? -1) + 1;
1291
1369
  const following = playback.line?.turns[after];
1292
1370
  if (!ready && step.awaitSteer.continueOnTimeout === true && following !== void 0 && !playback.consumed.has(after)) {
1293
1371
  playback.consumed.add(after);
@@ -1304,9 +1382,12 @@ function createPlaybookRunner(input) {
1304
1382
  if (turn.signal.aborted) break;
1305
1383
  lines.push(`mcp-send wait ${step.wait.tool}${ready ? ": ready" : " failed: timeout"}`);
1306
1384
  } else if ("waitFor" in step) {
1385
+ const { id, tool, path: path2, atLeast } = step.waitFor;
1386
+ const required = REQUIRED_WAIT_TOOLS.has(tool) && step.waitFor.continueOnTimeout !== true;
1307
1387
  const missing = opened.missing(step.waitFor.server);
1308
1388
  if (step.waitFor.server !== void 0 && missing !== void 0) {
1309
- lines.push(`mcp-send ${step.waitFor.tool} failed: ${missing}`);
1389
+ lines.push(`mcp-send ${tool} failed: ${missing}`);
1390
+ if (required) haltRequired(step, missing);
1310
1391
  continue;
1311
1392
  }
1312
1393
  let args;
@@ -1318,20 +1399,23 @@ function createPlaybookRunner(input) {
1318
1399
  playback.history
1319
1400
  );
1320
1401
  } catch (error) {
1321
- lines.push(`mcp-send ${step.waitFor.tool} failed: ${String(error)}`);
1402
+ lines.push(`mcp-send ${tool} failed: ${String(error)}`);
1403
+ if (required) haltRequired(step, String(error));
1322
1404
  continue;
1323
1405
  }
1324
1406
  const ready = await waitForCount(step, args, safeTurn, opened);
1325
1407
  if (turn.signal.aborted) break;
1326
- const { id, tool, path: path2, atLeast } = step.waitFor;
1327
1408
  if (ready !== void 0 && id !== void 0) playback.results.set(id, ready.result);
1328
1409
  lines.push(
1329
1410
  `mcp-send waitFor ${tool} ${path2} >= ${String(atLeast)}${ready ? ": ready" : " failed: timeout"}`
1330
1411
  );
1412
+ if (ready === void 0 && required)
1413
+ haltRequired(step, `not reached within ${String(step.waitFor.timeoutMs)} ms`);
1331
1414
  } else {
1332
1415
  lines.push(await runCall(step, playback, safeTurn, opened));
1333
1416
  }
1334
1417
  }
1418
+ if (!ran) return null;
1335
1419
  return redactMcpUrls(lines.join("\n"), turn.mcpServers);
1336
1420
  }
1337
1421
  };
@@ -1580,7 +1664,6 @@ var FAKE_MODELS = [
1580
1664
  { slug: "fake-large", name: "Fake Large" }
1581
1665
  ];
1582
1666
  var createFakeInstance = (config, host) => {
1583
- const events = new EventBus();
1584
1667
  const threads = /* @__PURE__ */ new Map();
1585
1668
  const script = parseScript(config);
1586
1669
  const playbook = script === "mcp-send" ? createPlaybookRunner(config) : void 0;
@@ -1611,22 +1694,12 @@ var createFakeInstance = (config, host) => {
1611
1694
  let disposed = false;
1612
1695
  const nextId = (prefix) => `${prefix}-fake-${++seq}-${NodeCrypto.randomUUID()}`;
1613
1696
  const now = () => (/* @__PURE__ */ new Date()).toISOString();
1614
- const emit = (type, threadId, payload, extra) => {
1615
- events.push({
1616
- eventId: nextId("evt"),
1617
- provider: FAKE_DRIVER_KIND,
1618
- providerInstanceId: host.instanceId,
1619
- threadId,
1620
- createdAt: now(),
1621
- type,
1622
- payload,
1623
- ...extra?.turnId === void 0 ? {} : { turnId: extra.turnId },
1624
- ...extra?.itemId === void 0 ? {} : { itemId: extra.itemId },
1625
- ...extra?.requestId === void 0 ? {} : { requestId: extra.requestId },
1626
- ...extra?.originBindingGeneration === void 0 ? {} : { originBindingGeneration: extra.originBindingGeneration },
1627
- ...extra?.originResumeCursor === void 0 ? {} : { originResumeCursor: extra.originResumeCursor }
1628
- });
1629
- };
1697
+ const emit = createFakeEmitter({
1698
+ thread: (threadId) => threads.get(threadId),
1699
+ nextId: () => nextId("evt"),
1700
+ now,
1701
+ instanceId: host.instanceId
1702
+ });
1630
1703
  const lifecycle = createFakeSessionLifecycle({
1631
1704
  threads,
1632
1705
  abort: (threadId) => inflight.get(threadId)?.abort(),
@@ -1634,19 +1707,17 @@ var createFakeInstance = (config, host) => {
1634
1707
  // 代次与续接游标取被关闭的那份 session,不在发送时重查当前会话。
1635
1708
  exited: (session) => {
1636
1709
  if (stopWithoutExit) return;
1710
+ const context = threads.get(session.threadId);
1711
+ if (context) settleFakeTasks(context, session.threadId, emit);
1637
1712
  emit(
1638
1713
  "session.exited",
1639
1714
  session.threadId,
1640
1715
  { reason: "stopped" },
1641
- {
1642
- ...session.bindingGeneration === void 0 ? {} : { originBindingGeneration: session.bindingGeneration },
1643
- ...session.resumeCursor === void 0 ? {} : { originResumeCursor: session.resumeCursor }
1644
- }
1716
+ session.resumeCursor === void 0 ? {} : { originResumeCursor: session.resumeCursor }
1645
1717
  );
1646
1718
  },
1647
1719
  forget: (threadId) => steeredInputs.delete(threadId),
1648
- disposePlaybook: () => playbook?.dispose(),
1649
- closeEvents: () => events.close()
1720
+ disposePlaybook: () => playbook?.dispose()
1650
1721
  });
1651
1722
  const requireThread = (threadId) => {
1652
1723
  const booked = threads.get(threadId);
@@ -1869,6 +1940,8 @@ ${more ?? "playbook: steered input not handled"}`;
1869
1940
  if (signal?.aborted) throw new Error("aborted");
1870
1941
  if (threads.get(input.threadId)?.closing !== void 0) throw new Error("session closing");
1871
1942
  if (failStartSession !== void 0) throw new Error(failStartSession);
1943
+ const previous = threads.get(input.threadId);
1944
+ if (previous) await lifecycle.closeBooked(input.threadId, previous);
1872
1945
  const cwd = input.cwd ?? host.workspacePath(input.threadId);
1873
1946
  if (cwd.trim() === "") throw new Error("workspace path missing");
1874
1947
  const createdAt = now();
@@ -1884,7 +1957,13 @@ ${more ?? "playbook: steered input not handled"}`;
1884
1957
  ...input.discardResumeCursor === true || input.resumeCursor === void 0 ? {} : { resumeCursor: input.resumeCursor },
1885
1958
  ...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
1886
1959
  };
1887
- threads.set(input.threadId, { session, turns: [], mcpServers: input.mcpServers });
1960
+ threads.set(input.threadId, {
1961
+ tasks: /* @__PURE__ */ new Map(),
1962
+ events: input.events,
1963
+ session,
1964
+ turns: [],
1965
+ mcpServers: input.mcpServers
1966
+ });
1888
1967
  playbook?.startSession(input);
1889
1968
  emit(
1890
1969
  "session.started",
@@ -1974,7 +2053,6 @@ ${more ?? "playbook: steered input not handled"}`;
1974
2053
  const booked = requireThread(threadId);
1975
2054
  return { threadId, turns: booked.turns };
1976
2055
  },
1977
- events,
1978
2056
  usage: createFakeUsage({ instanceId: host.instanceId, script: parseUsageScript(config) }),
1979
2057
  dispose: async () => {
1980
2058
  disposed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-fake",
3
- "version": "0.2.3976",
3
+ "version": "0.2.3982",
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.3976",
24
+ "@ycodium-ai/plugin-api": "0.2.3982",
25
25
  "ycodium-executor-testing": "0.0.0"
26
26
  },
27
27
  "ycodium": {