@ycodium-ai/agent-fake 0.2.3935 → 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 +272 -191
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  // src/index.ts
2
2
  import { definePlugin } from "@ycodium-ai/plugin-api";
3
3
 
4
+ // ../../packages/plugin-api/src/version.ts
5
+ var PLUGIN_API_VERSION = 76;
6
+
4
7
  // src/card.ts
5
8
  var FAKE_SCRIPTS = [
6
9
  "plain",
@@ -98,38 +101,32 @@ var FAKE_CARD = {
98
101
  import * as NodeCrypto from "node:crypto";
99
102
 
100
103
  // src/fakeEventBus.ts
101
- var EventBus = class {
102
- #queued = [];
103
- #waiters = [];
104
- #closed = false;
105
- push(event) {
106
- const waiter = this.#waiters.shift();
107
- if (waiter) {
108
- waiter({ done: false, value: event });
109
- return;
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 });
110
113
  }
111
- this.#queued.push(event);
112
- }
113
- close() {
114
- if (this.#closed) return;
115
- this.#closed = true;
116
- while (this.#waiters.length > 0) {
117
- this.#waiters.shift()?.({ done: true, value: void 0 });
118
- }
119
- }
120
- [Symbol.asyncIterator]() {
121
- return {
122
- next: () => {
123
- const queued = this.#queued.shift();
124
- if (queued !== void 0) return Promise.resolve({ done: false, value: queued });
125
- if (this.#closed) return Promise.resolve({ done: true, value: void 0 });
126
- return new Promise((resolve) => {
127
- this.#waiters.push(resolve);
128
- });
129
- }
130
- };
131
- }
132
- };
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
+ }
133
130
 
134
131
  // src/approvalToolScript.ts
135
132
  function createApprovalToolScript({
@@ -299,7 +296,6 @@ function createFakeSessionLifecycle(input) {
299
296
  if (result.status === "rejected") throw result.reason;
300
297
  }
301
298
  await input.disposePlaybook();
302
- input.closeEvents();
303
299
  })().then(disposing.resolve, disposing.reject);
304
300
  return disposal;
305
301
  }
@@ -885,115 +881,8 @@ function createPlaybookMcp(input) {
885
881
  };
886
882
  }
887
883
 
888
- // src/playbook.ts
889
- 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
890
885
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
891
- var isName = (value) => typeof value === "string" && value.length > 0;
892
- var onlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
893
- var parseTimeout = (value) => {
894
- const timeoutMs = value === void 0 ? 3e4 : value;
895
- return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : null;
896
- };
897
- function parseStep(value) {
898
- if (!isRecord2(value)) return null;
899
- if (Object.hasOwn(value, "expectInput"))
900
- return onlyKeys(value, ["expectInput"]) && isName(value.expectInput) ? { expectInput: value.expectInput } : null;
901
- if (Object.hasOwn(value, "awaitSteer")) {
902
- const wait = value.awaitSteer;
903
- if (!onlyKeys(value, ["awaitSteer"]) || !isRecord2(wait) || !onlyKeys(wait, ["timeoutMs", "continueOnTimeout"]) || Object.hasOwn(wait, "continueOnTimeout") && wait.continueOnTimeout !== true)
904
- return null;
905
- const timeoutMs = parseTimeout(wait.timeoutMs);
906
- if (timeoutMs === null) return null;
907
- const onTimeout = wait.continueOnTimeout === true ? { continueOnTimeout: true } : {};
908
- return { awaitSteer: { timeoutMs, ...onTimeout } };
909
- }
910
- if (Object.hasOwn(value, "wait")) {
911
- const wait = value.wait;
912
- if (!onlyKeys(value, ["wait"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
913
- if (!onlyKeys(wait, ["server", "tool", "timeoutMs"]) || wait.server !== void 0 && !isName(wait.server))
914
- return null;
915
- const timeoutMs = parseTimeout(wait.timeoutMs);
916
- return timeoutMs === null ? null : {
917
- wait: {
918
- tool: wait.tool,
919
- timeoutMs,
920
- ...wait.server === void 0 ? {} : { server: wait.server }
921
- }
922
- };
923
- }
924
- if (Object.hasOwn(value, "waitFor")) {
925
- const wait = value.waitFor;
926
- if (!onlyKeys(value, ["waitFor"]) || !isRecord2(wait) || !isName(wait.tool)) return null;
927
- 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))
928
- return null;
929
- if (typeof wait.atLeast !== "number" || !Number.isInteger(wait.atLeast) || wait.atLeast < 0)
930
- return null;
931
- const timeoutMs = parseTimeout(wait.timeoutMs);
932
- return timeoutMs === null ? null : {
933
- waitFor: {
934
- tool: wait.tool,
935
- path: wait.path,
936
- atLeast: wait.atLeast,
937
- timeoutMs,
938
- ...wait.id === void 0 ? {} : { id: wait.id },
939
- ...wait.server === void 0 ? {} : { server: wait.server },
940
- ...wait.arguments === void 0 ? {} : { arguments: wait.arguments }
941
- }
942
- };
943
- }
944
- if (!onlyKeys(value, ["id", "server", "tool", "arguments"]) || !isName(value.tool) || value.server !== void 0 && !isName(value.server))
945
- return null;
946
- if (value.id !== void 0 && typeof value.id !== "string") return null;
947
- if (value.arguments !== void 0 && !isRecord2(value.arguments)) return null;
948
- return {
949
- tool: value.tool,
950
- ...value.server === void 0 ? {} : { server: value.server },
951
- ...value.id === void 0 ? {} : { id: value.id },
952
- ...value.arguments === void 0 ? {} : { arguments: value.arguments }
953
- };
954
- }
955
- function parsePlaybook(text) {
956
- try {
957
- const value = JSON.parse(text);
958
- if (!isRecord2(value) || value.version !== 1) return { ok: false, reason: "version must be 1" };
959
- if (!onlyKeys(value, ["version", "threads"]) || !Array.isArray(value.threads))
960
- return { ok: false, reason: "threads must be an array" };
961
- const threads = [];
962
- for (const [index, line] of value.threads.entries()) {
963
- if (!isRecord2(line) || !onlyKeys(line, ["match", "turns"]) || !isRecord2(line.match))
964
- return { ok: false, reason: `threads[${index}] must have match and turns` };
965
- const match = line.match;
966
- 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")
967
- return { ok: false, reason: `threads[${index}].match needs threadId or title` };
968
- if (!Array.isArray(line.turns))
969
- return { ok: false, reason: `threads[${index}].turns must be an array` };
970
- const turns = [];
971
- for (const [turn, batch] of line.turns.entries()) {
972
- if (!Array.isArray(batch))
973
- return { ok: false, reason: `threads[${index}].turns[${turn}] must be an array` };
974
- const steps = [];
975
- for (const entry of batch) {
976
- const step = parseStep(entry);
977
- const last = steps.at(-1);
978
- if (step === null || "expectInput" in step && steps.length > 0 || last !== void 0 && "awaitSteer" in last)
979
- return { ok: false, reason: `threads[${index}].turns[${turn}] has an invalid step` };
980
- steps.push(step);
981
- }
982
- turns.push(steps);
983
- }
984
- threads.push({
985
- match: {
986
- ...typeof match.threadId === "string" ? { threadId: new RegExp(match.threadId) } : {},
987
- ...typeof match.title === "string" ? { title: match.title } : {}
988
- },
989
- turns
990
- });
991
- }
992
- return { ok: true, threads };
993
- } catch (error) {
994
- return { ok: false, reason: String(error) };
995
- }
996
- }
997
886
  var UnresolvedPlaceholder = class extends Error {
998
887
  };
999
888
  var splitPath = (path) => path.split(/\.(?![^\[]*\])/u);
@@ -1099,6 +988,127 @@ function stepResult(result) {
1099
988
  return first.text;
1100
989
  }
1101
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
+ }
1102
1112
  function pickBatch(line, consumed, input) {
1103
1113
  let earliest = true;
1104
1114
  for (const [index, batch] of line.turns.entries()) {
@@ -1140,25 +1150,38 @@ var waitForTool = (step, input, clients) => poll(step.wait.timeoutMs, input, asy
1140
1150
  (tool) => isRecord2(tool) && (tool.name === step.wait.tool || tool.name === `mcp__${step.wait.server ?? "ycodium"}__${step.wait.tool}`)
1141
1151
  );
1142
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
+ }
1143
1166
  async function waitForCount(step, args, input, clients) {
1144
1167
  let ready;
1145
1168
  const ok = await poll(step.waitFor.timeoutMs, input, async (signal) => {
1146
- const called = await clients.request(
1147
- step.waitFor.server,
1148
- "tools/call",
1149
- { name: step.waitFor.tool, arguments: args },
1150
- signal
1151
- );
1152
- if (isRecord2(called) && called.isError === true) return false;
1153
- const result = stepResult(called);
1154
- const value = valueAt(result, splitPath(step.waitFor.path));
1155
- const count = Array.isArray(value) ? value.length : typeof value === "number" ? value : value === void 0 || value === null ? 0 : 1;
1156
- if (count < step.waitFor.atLeast) return false;
1157
- ready = { result };
1158
- return true;
1169
+ ready = await readCount(step, args, clients, signal);
1170
+ return ready !== void 0;
1159
1171
  });
1160
1172
  return ok ? ready : void 0;
1161
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
+ }
1162
1185
  async function runCall(step, playback, input, clients) {
1163
1186
  let args = step.arguments ?? {};
1164
1187
  let failure;
@@ -1248,22 +1271,21 @@ function createPlaybookRunner(input) {
1248
1271
  if (playback === void 0) {
1249
1272
  const title = titles.get(turn.threadId);
1250
1273
  const index = parsed.threads.findIndex(
1251
- (line, index2) => !claimed.has(index2) && lineClaimsSession(line, turn.threadId, title)
1274
+ (line2, index2) => !claimed.has(index2) && lineClaimsSession(line2, turn.threadId, title)
1252
1275
  );
1253
1276
  if (index >= 0) claimed.add(index);
1254
1277
  playback = {
1255
1278
  line: parsed.threads[index] ?? null,
1256
1279
  consumed: /* @__PURE__ */ new Set(),
1280
+ owed: /* @__PURE__ */ new Set(),
1257
1281
  results: /* @__PURE__ */ new Map(),
1258
1282
  history: []
1259
1283
  };
1260
1284
  sessions.set(turn.threadId, playback);
1261
1285
  }
1262
- const picked = playback.line === null ? void 0 : pickBatch(playback.line, playback.consumed, turn.input ?? "");
1263
- const batch = picked === void 0 ? void 0 : playback.line?.turns[picked];
1264
- if (picked === void 0 || batch === void 0) return null;
1265
- playback.consumed.add(picked);
1266
- 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;
1267
1289
  let opened = clients.get(turn.threadId);
1268
1290
  if (opened === void 0) {
1269
1291
  opened = createPlaybookMcp({ fetch: turn.fetch, servers: turn.mcpServers });
@@ -1275,16 +1297,75 @@ function createPlaybookRunner(input) {
1275
1297
  turn.emit(args[0], args[1], redactMcpUrls(args[2], turn.mcpServers), args[3]);
1276
1298
  }
1277
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
+ };
1278
1317
  const lines = [];
1279
- const queue = [...batch];
1280
- 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()) {
1281
1362
  if (turn.signal.aborted) break;
1282
1363
  if ("expectInput" in step) continue;
1283
1364
  if ("awaitSteer" in step) {
1284
1365
  const ready = await turn.awaitSteer?.(step.awaitSteer.timeoutMs, turn.signal) ?? false;
1285
1366
  if (turn.signal.aborted) break;
1286
1367
  lines.push(`mcp-send awaitSteer${ready ? ": ready" : " failed: timeout"}`);
1287
- const after = (playback.current ?? picked) + 1;
1368
+ const after = (playback.current ?? -1) + 1;
1288
1369
  const following = playback.line?.turns[after];
1289
1370
  if (!ready && step.awaitSteer.continueOnTimeout === true && following !== void 0 && !playback.consumed.has(after)) {
1290
1371
  playback.consumed.add(after);
@@ -1301,9 +1382,12 @@ function createPlaybookRunner(input) {
1301
1382
  if (turn.signal.aborted) break;
1302
1383
  lines.push(`mcp-send wait ${step.wait.tool}${ready ? ": ready" : " failed: timeout"}`);
1303
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;
1304
1387
  const missing = opened.missing(step.waitFor.server);
1305
1388
  if (step.waitFor.server !== void 0 && missing !== void 0) {
1306
- lines.push(`mcp-send ${step.waitFor.tool} failed: ${missing}`);
1389
+ lines.push(`mcp-send ${tool} failed: ${missing}`);
1390
+ if (required) haltRequired(step, missing);
1307
1391
  continue;
1308
1392
  }
1309
1393
  let args;
@@ -1315,20 +1399,23 @@ function createPlaybookRunner(input) {
1315
1399
  playback.history
1316
1400
  );
1317
1401
  } catch (error) {
1318
- 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));
1319
1404
  continue;
1320
1405
  }
1321
1406
  const ready = await waitForCount(step, args, safeTurn, opened);
1322
1407
  if (turn.signal.aborted) break;
1323
- const { id, tool, path: path2, atLeast } = step.waitFor;
1324
1408
  if (ready !== void 0 && id !== void 0) playback.results.set(id, ready.result);
1325
1409
  lines.push(
1326
1410
  `mcp-send waitFor ${tool} ${path2} >= ${String(atLeast)}${ready ? ": ready" : " failed: timeout"}`
1327
1411
  );
1412
+ if (ready === void 0 && required)
1413
+ haltRequired(step, `not reached within ${String(step.waitFor.timeoutMs)} ms`);
1328
1414
  } else {
1329
1415
  lines.push(await runCall(step, playback, safeTurn, opened));
1330
1416
  }
1331
1417
  }
1418
+ if (!ran) return null;
1332
1419
  return redactMcpUrls(lines.join("\n"), turn.mcpServers);
1333
1420
  }
1334
1421
  };
@@ -1577,7 +1664,6 @@ var FAKE_MODELS = [
1577
1664
  { slug: "fake-large", name: "Fake Large" }
1578
1665
  ];
1579
1666
  var createFakeInstance = (config, host) => {
1580
- const events = new EventBus();
1581
1667
  const threads = /* @__PURE__ */ new Map();
1582
1668
  const script = parseScript(config);
1583
1669
  const playbook = script === "mcp-send" ? createPlaybookRunner(config) : void 0;
@@ -1608,22 +1694,12 @@ var createFakeInstance = (config, host) => {
1608
1694
  let disposed = false;
1609
1695
  const nextId = (prefix) => `${prefix}-fake-${++seq}-${NodeCrypto.randomUUID()}`;
1610
1696
  const now = () => (/* @__PURE__ */ new Date()).toISOString();
1611
- const emit = (type, threadId, payload, extra) => {
1612
- events.push({
1613
- eventId: nextId("evt"),
1614
- provider: FAKE_DRIVER_KIND,
1615
- providerInstanceId: host.instanceId,
1616
- threadId,
1617
- createdAt: now(),
1618
- type,
1619
- payload,
1620
- ...extra?.turnId === void 0 ? {} : { turnId: extra.turnId },
1621
- ...extra?.itemId === void 0 ? {} : { itemId: extra.itemId },
1622
- ...extra?.requestId === void 0 ? {} : { requestId: extra.requestId },
1623
- ...extra?.originBindingGeneration === void 0 ? {} : { originBindingGeneration: extra.originBindingGeneration },
1624
- ...extra?.originResumeCursor === void 0 ? {} : { originResumeCursor: extra.originResumeCursor }
1625
- });
1626
- };
1697
+ const emit = createFakeEmitter({
1698
+ thread: (threadId) => threads.get(threadId),
1699
+ nextId: () => nextId("evt"),
1700
+ now,
1701
+ instanceId: host.instanceId
1702
+ });
1627
1703
  const lifecycle = createFakeSessionLifecycle({
1628
1704
  threads,
1629
1705
  abort: (threadId) => inflight.get(threadId)?.abort(),
@@ -1631,19 +1707,17 @@ var createFakeInstance = (config, host) => {
1631
1707
  // 代次与续接游标取被关闭的那份 session,不在发送时重查当前会话。
1632
1708
  exited: (session) => {
1633
1709
  if (stopWithoutExit) return;
1710
+ const context = threads.get(session.threadId);
1711
+ if (context) settleFakeTasks(context, session.threadId, emit);
1634
1712
  emit(
1635
1713
  "session.exited",
1636
1714
  session.threadId,
1637
1715
  { reason: "stopped" },
1638
- {
1639
- ...session.bindingGeneration === void 0 ? {} : { originBindingGeneration: session.bindingGeneration },
1640
- ...session.resumeCursor === void 0 ? {} : { originResumeCursor: session.resumeCursor }
1641
- }
1716
+ session.resumeCursor === void 0 ? {} : { originResumeCursor: session.resumeCursor }
1642
1717
  );
1643
1718
  },
1644
1719
  forget: (threadId) => steeredInputs.delete(threadId),
1645
- disposePlaybook: () => playbook?.dispose(),
1646
- closeEvents: () => events.close()
1720
+ disposePlaybook: () => playbook?.dispose()
1647
1721
  });
1648
1722
  const requireThread = (threadId) => {
1649
1723
  const booked = threads.get(threadId);
@@ -1866,6 +1940,8 @@ ${more ?? "playbook: steered input not handled"}`;
1866
1940
  if (signal?.aborted) throw new Error("aborted");
1867
1941
  if (threads.get(input.threadId)?.closing !== void 0) throw new Error("session closing");
1868
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);
1869
1945
  const cwd = input.cwd ?? host.workspacePath(input.threadId);
1870
1946
  if (cwd.trim() === "") throw new Error("workspace path missing");
1871
1947
  const createdAt = now();
@@ -1881,7 +1957,13 @@ ${more ?? "playbook: steered input not handled"}`;
1881
1957
  ...input.discardResumeCursor === true || input.resumeCursor === void 0 ? {} : { resumeCursor: input.resumeCursor },
1882
1958
  ...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
1883
1959
  };
1884
- 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
+ });
1885
1967
  playbook?.startSession(input);
1886
1968
  emit(
1887
1969
  "session.started",
@@ -1971,7 +2053,6 @@ ${more ?? "playbook: steered input not handled"}`;
1971
2053
  const booked = requireThread(threadId);
1972
2054
  return { threadId, turns: booked.turns };
1973
2055
  },
1974
- events,
1975
2056
  usage: createFakeUsage({ instanceId: host.instanceId, script: parseUsageScript(config) }),
1976
2057
  dispose: async () => {
1977
2058
  disposed = true;
@@ -1994,7 +2075,7 @@ var executors = [
1994
2075
  ];
1995
2076
  var index_default = definePlugin({
1996
2077
  id: "agent-fake",
1997
- apiVersion: 69,
2078
+ apiVersion: PLUGIN_API_VERSION,
1998
2079
  executors,
1999
2080
  activate(ctx) {
2000
2081
  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.3935",
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.3935",
24
+ "@ycodium-ai/plugin-api": "0.2.3982",
25
25
  "ycodium-executor-testing": "0.0.0"
26
26
  },
27
27
  "ycodium": {