@ycodium-ai/agent-fake 0.2.2944 → 0.2.2948

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 +1 -1
  2. package/dist/index.js +309 -26
  3. package/package.json +3 -3
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), `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), or `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.”)
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), `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), `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)
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`
package/dist/index.js CHANGED
@@ -29,7 +29,9 @@ var FAKE_CARD = {
29
29
  everydayHarness: false,
30
30
  capabilities: {
31
31
  sessionModelSwitch: "unsupported",
32
- runtimeModeSwitch: "next-turn"
32
+ runtimeModeSwitch: "next-turn",
33
+ // lane 脚本会把插话并进正在跑的那一轮;不声明的话界面不给「发送到当前轮次」。
34
+ turnSteering: true
33
35
  },
34
36
  credentialSources: [],
35
37
  diagnostics: [],
@@ -42,10 +44,20 @@ var FAKE_CARD = {
42
44
  properties: {
43
45
  script: {
44
46
  type: "string",
45
- enum: ["plain", "approval-tool", "async-question", "stream", "mcp-send", "visualize"]
47
+ enum: [
48
+ "plain",
49
+ "approval-tool",
50
+ "async-question",
51
+ "stream",
52
+ "mcp-send",
53
+ "visualize",
54
+ "lane"
55
+ ]
46
56
  },
47
57
  streamChunks: { type: "integer", minimum: 1 },
48
58
  streamIntervalMs: { type: "integer", minimum: 0 },
59
+ // lane 的 hold 最多等插话多久;不进设置表单。
60
+ holdMs: { type: "integer", minimum: 0 },
49
61
  // stream 脚本循环切片的源文本;性能探针用它换上可控的长回复,不进设置表单。
50
62
  streamText: { type: "string" },
51
63
  // 额度与用量怎么答(见 fakeUsage.ts);不进设置表单,开发与验收时手改实例配置。
@@ -74,6 +86,235 @@ var FAKE_CARD = {
74
86
  // src/fakeInstance.ts
75
87
  import * as NodeCrypto from "node:crypto";
76
88
 
89
+ // src/laneScript.ts
90
+ import * as NodeFS from "node:fs";
91
+ import * as NodePath from "node:path";
92
+ var LANE_STREAM_DONE = "LANE_STREAM_DONE";
93
+ var LANE_PLAIN_DONE = "LANE_PLAIN_DONE";
94
+ var LANE_APPROVAL_DONE = "LANE_APPROVAL_DONE";
95
+ var LANE_APPROVAL_DECLINED = "LANE_APPROVAL_DECLINED";
96
+ var LANE_QUESTION_ASKED = "LANE_QUESTION_ASKED";
97
+ var LANE_QUESTION_TEXT = "Which database should I use?";
98
+ var LANE_ATTACH_DONE = "LANE_ATTACH_DONE";
99
+ var LANE_HOLD_DONE = "LANE_HOLD_DONE";
100
+ var LANE_STEERED_PREFIX = "LANE_STEERED:";
101
+ var LANE_IMAGE_NAME = "lane1.png";
102
+ var LANE_DOCUMENT_NAME = "lane1.pdf";
103
+ var LANE_STREAM_TEXT = [
104
+ "Here is a longer note so the timeline can scroll.",
105
+ "The first paragraph stays on screen while the rest arrives in pieces.",
106
+ "Each piece is short on purpose: the probe sends the next message before this turn ends.",
107
+ "Scroll up after this reply and the position should survive a tab switch."
108
+ ].join(" ");
109
+ var LANE_PNG = Buffer.from(
110
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
111
+ "base64"
112
+ );
113
+ var LANE_PDF = Buffer.from(
114
+ "%PDF-1.1\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj\n3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 200 200]>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF\n"
115
+ );
116
+ function parseLaneKind(input) {
117
+ const match = /lane:(stream|plain|approve|question|attach|hold)\b/.exec(input ?? "");
118
+ const kind = match?.[1];
119
+ return kind === "stream" || kind === "plain" || kind === "approve" || kind === "question" || kind === "attach" || kind === "hold" ? kind : "plain";
120
+ }
121
+ function laneTurnOptions(input, stream) {
122
+ const holdMs = input?.config?.holdMs;
123
+ return {
124
+ streamChunks: stream.chunks,
125
+ streamIntervalMs: stream.intervalMs,
126
+ holdMs: typeof holdMs === "number" && holdMs >= 0 ? holdMs : 15e3
127
+ };
128
+ }
129
+ async function openShellApproval(input) {
130
+ const itemId = input.nextItemId();
131
+ const args = { command: input.detail };
132
+ input.emit(
133
+ "item.started",
134
+ { itemType: "command_execution", status: "inProgress", title: input.detail, data: args },
135
+ { itemId }
136
+ );
137
+ input.emit(
138
+ "request.opened",
139
+ {
140
+ requestType: "exec_command_approval",
141
+ toolName: "shell",
142
+ detail: input.detail,
143
+ args
144
+ },
145
+ { requestId: input.requestId }
146
+ );
147
+ const decision = await new Promise((resolve) => {
148
+ let settled = false;
149
+ const settle = (value) => {
150
+ if (settled) return;
151
+ settled = true;
152
+ input.signal.removeEventListener("abort", onAbort);
153
+ input.pending.delete(input.requestId);
154
+ resolve(value);
155
+ };
156
+ const onAbort = () => settle("cancel");
157
+ input.signal.addEventListener("abort", onAbort);
158
+ if (input.signal.aborted) {
159
+ settle("cancel");
160
+ return;
161
+ }
162
+ input.pending.set(input.requestId, settle);
163
+ });
164
+ input.emit(
165
+ "request.resolved",
166
+ {
167
+ requestType: "exec_command_approval",
168
+ decision: decision === "allow" ? "accept" : decision === "deny" ? "decline" : "cancel"
169
+ },
170
+ { requestId: input.requestId }
171
+ );
172
+ input.emit(
173
+ "item.completed",
174
+ {
175
+ itemType: "command_execution",
176
+ status: decision === "allow" ? "completed" : "declined",
177
+ title: input.detail,
178
+ data: args
179
+ },
180
+ { itemId }
181
+ );
182
+ return decision;
183
+ }
184
+ var delay = (ms, signal) => new Promise((resolve) => {
185
+ if (signal.aborted || ms <= 0) {
186
+ resolve();
187
+ return;
188
+ }
189
+ const timer = setTimeout(resolve, ms);
190
+ signal.addEventListener(
191
+ "abort",
192
+ () => {
193
+ clearTimeout(timer);
194
+ resolve();
195
+ },
196
+ { once: true }
197
+ );
198
+ });
199
+ var assistant = (emit, delta) => {
200
+ emit("content.delta", { streamKind: "assistant_text", delta });
201
+ };
202
+ function acceptLaneSteer(input) {
203
+ const { running } = input;
204
+ if (running === void 0 || !input.enabled) return null;
205
+ if (input.expectedTurnId !== void 0 && input.expectedTurnId !== running.turnId) return null;
206
+ const text = input.text?.trim() ?? "";
207
+ if (text !== "") {
208
+ const parts = input.steered.get(input.threadId) ?? [];
209
+ parts.push(text);
210
+ input.steered.set(input.threadId, parts);
211
+ }
212
+ return { threadId: input.threadId, turnId: running.turnId };
213
+ }
214
+ async function runLaneTurn(ctx) {
215
+ const kind = parseLaneKind(ctx.input);
216
+ if (ctx.signal.aborted) return null;
217
+ switch (kind) {
218
+ case "stream":
219
+ return streamTurn(ctx);
220
+ case "approve":
221
+ return approveTurn(ctx);
222
+ case "question":
223
+ return questionTurn(ctx);
224
+ case "attach":
225
+ return attachTurn(ctx);
226
+ case "hold":
227
+ return holdTurn(ctx);
228
+ case "plain":
229
+ assistant(ctx.emit, LANE_PLAIN_DONE);
230
+ await delay(0, ctx.signal);
231
+ return ctx.signal.aborted ? null : LANE_PLAIN_DONE;
232
+ default: {
233
+ const unreachable = kind;
234
+ return unreachable;
235
+ }
236
+ }
237
+ }
238
+ async function streamTurn(ctx) {
239
+ let text = "";
240
+ const pieces = Math.max(1, ctx.options.streamChunks);
241
+ for (let index = 0; index < pieces; index += 1) {
242
+ if (ctx.signal.aborted) return null;
243
+ const start = index * 24 % LANE_STREAM_TEXT.length;
244
+ const delta = LANE_STREAM_TEXT.slice(start, start + 24) || LANE_STREAM_TEXT.slice(0, 24);
245
+ text += delta;
246
+ assistant(ctx.emit, delta);
247
+ await delay(ctx.options.streamIntervalMs, ctx.signal);
248
+ }
249
+ if (ctx.signal.aborted) return null;
250
+ assistant(ctx.emit, LANE_STREAM_DONE);
251
+ return `${text}${LANE_STREAM_DONE}`;
252
+ }
253
+ async function approveTurn(ctx) {
254
+ const requestId = ctx.nextId("req");
255
+ const decision = await ctx.approve({ requestId, detail: "echo lane1-approve" });
256
+ if (decision === "cancel" || ctx.signal.aborted) return null;
257
+ const text = decision === "allow" ? LANE_APPROVAL_DONE : LANE_APPROVAL_DECLINED;
258
+ assistant(ctx.emit, text);
259
+ await delay(0, ctx.signal);
260
+ return ctx.signal.aborted ? null : text;
261
+ }
262
+ function questionTurn(ctx) {
263
+ if (ctx.signal.aborted) return null;
264
+ assistant(ctx.emit, LANE_QUESTION_ASKED);
265
+ ctx.emit(
266
+ "user-input.requested",
267
+ {
268
+ questions: [
269
+ {
270
+ id: "q0",
271
+ header: LANE_QUESTION_TEXT,
272
+ question: LANE_QUESTION_TEXT,
273
+ options: [
274
+ { label: "Postgres", description: "" },
275
+ { label: "SQLite", description: "" }
276
+ ],
277
+ allowFreeform: true,
278
+ multiSelect: false
279
+ }
280
+ ],
281
+ responseMode: "message"
282
+ },
283
+ { requestId: ctx.nextId("question") }
284
+ );
285
+ return LANE_QUESTION_ASKED;
286
+ }
287
+ function attachTurn(ctx) {
288
+ if (ctx.signal.aborted) return null;
289
+ NodeFS.mkdirSync(ctx.cwd, { recursive: true });
290
+ const imagePath = NodePath.join(ctx.cwd, LANE_IMAGE_NAME);
291
+ const documentPath = NodePath.join(ctx.cwd, LANE_DOCUMENT_NAME);
292
+ NodeFS.writeFileSync(imagePath, LANE_PNG);
293
+ NodeFS.writeFileSync(documentPath, LANE_PDF);
294
+ ctx.emit("assistant.artifact.produced", {
295
+ artifacts: [
296
+ { path: imagePath, name: LANE_IMAGE_NAME, mimeType: "image/png" },
297
+ { path: documentPath, name: LANE_DOCUMENT_NAME, mimeType: "application/pdf" }
298
+ ]
299
+ });
300
+ assistant(ctx.emit, LANE_ATTACH_DONE);
301
+ return LANE_ATTACH_DONE;
302
+ }
303
+ async function holdTurn(ctx) {
304
+ assistant(ctx.emit, "holding the turn");
305
+ const deadline = Date.now() + ctx.options.holdMs;
306
+ let steered = "";
307
+ while (!ctx.signal.aborted && Date.now() < deadline) {
308
+ steered = ctx.takeSteered();
309
+ if (steered !== "") break;
310
+ await delay(50, ctx.signal);
311
+ }
312
+ if (ctx.signal.aborted) return null;
313
+ const text = steered === "" ? LANE_HOLD_DONE : `${LANE_HOLD_DONE} ${LANE_STEERED_PREFIX}${steered}`;
314
+ assistant(ctx.emit, text);
315
+ return text;
316
+ }
317
+
77
318
  // src/fakeUsage.ts
78
319
  var FAKE_USAGE_SCRIPTS = [
79
320
  "available",
@@ -287,6 +528,29 @@ async function runMcpCalls(input) {
287
528
  return lines.join("\n");
288
529
  }
289
530
 
531
+ // src/streamScript.ts
532
+ var FAKE_STREAM_TEXT = [
533
+ "Here is the plan for the change, step by step.\n\n",
534
+ "1. Read the current implementation and note the hot path.\n",
535
+ "2. Move the measurement out of the commit phase.\n",
536
+ "3. Verify the numbers on a production build.\n\n",
537
+ "```ts\nexport function measure(root: HTMLElement) {\n return root.getBoundingClientRect().height;\n}\n```\n\n",
538
+ "The **important** part is that the timeline only re-renders the streaming row, ",
539
+ "not the whole list, so each delta stays cheap even on a long conversation. "
540
+ ].join("");
541
+ var STREAM_CHUNK_CHARS = 12;
542
+ var parseStreamOptions = (input) => {
543
+ const config = input?.config ?? {};
544
+ const customText = typeof config.streamText === "string" && config.streamText !== "" ? config.streamText : null;
545
+ const chunks = typeof config.streamChunks === "number" && config.streamChunks >= 1 ? Math.floor(config.streamChunks) : customText === null ? 200 : Math.ceil(customText.length / STREAM_CHUNK_CHARS);
546
+ const intervalMs = typeof config.streamIntervalMs === "number" && config.streamIntervalMs >= 0 ? config.streamIntervalMs : 30;
547
+ return { chunks, intervalMs, text: customText ?? FAKE_STREAM_TEXT };
548
+ };
549
+ var fakeStreamChunk = (text, index) => {
550
+ const start = index * STREAM_CHUNK_CHARS % text.length;
551
+ return text.slice(start, start + STREAM_CHUNK_CHARS) || text.slice(0, STREAM_CHUNK_CHARS);
552
+ };
553
+
290
554
  // src/visualizeScript.ts
291
555
  var WIRE_NAME = "mcp__ycodium__visualize_visualize";
292
556
  var TITLE = "Monthly sales";
@@ -427,29 +691,8 @@ var FAKE_ASYNC_QUESTIONS = [
427
691
  function parseScript(input) {
428
692
  const envelope = input;
429
693
  const script = envelope?.config?.script;
430
- return script === "approval-tool" || script === "async-question" || script === "stream" || script === "mcp-send" || script === "visualize" ? script : "plain";
694
+ return script === "approval-tool" || script === "async-question" || script === "stream" || script === "mcp-send" || script === "visualize" || script === "lane" ? script : "plain";
431
695
  }
432
- var FAKE_STREAM_TEXT = [
433
- "Here is the plan for the change, step by step.\n\n",
434
- "1. Read the current implementation and note the hot path.\n",
435
- "2. Move the measurement out of the commit phase.\n",
436
- "3. Verify the numbers on a production build.\n\n",
437
- "```ts\nexport function measure(root: HTMLElement) {\n return root.getBoundingClientRect().height;\n}\n```\n\n",
438
- "The **important** part is that the timeline only re-renders the streaming row, ",
439
- "not the whole list, so each delta stays cheap even on a long conversation. "
440
- ].join("");
441
- var STREAM_CHUNK_CHARS = 12;
442
- var parseStreamOptions = (input) => {
443
- const config = input?.config ?? {};
444
- const customText = typeof config.streamText === "string" && config.streamText !== "" ? config.streamText : null;
445
- const chunks = typeof config.streamChunks === "number" && config.streamChunks >= 1 ? Math.floor(config.streamChunks) : customText === null ? 200 : Math.ceil(customText.length / STREAM_CHUNK_CHARS);
446
- const intervalMs = typeof config.streamIntervalMs === "number" && config.streamIntervalMs >= 0 ? config.streamIntervalMs : 30;
447
- return { chunks, intervalMs, text: customText ?? FAKE_STREAM_TEXT };
448
- };
449
- var fakeStreamChunk = (text, index) => {
450
- const start = index * STREAM_CHUNK_CHARS % text.length;
451
- return text.slice(start, start + STREAM_CHUNK_CHARS) || text.slice(0, STREAM_CHUNK_CHARS);
452
- };
453
696
  var parseReplyText = (input) => {
454
697
  const envelope = input;
455
698
  const replyText = envelope?.config?.replyText;
@@ -472,6 +715,8 @@ var createFakeInstance = (config, host) => {
472
715
  const failStartSession = parseFailStartSession(config);
473
716
  const streamOptions = parseStreamOptions(config);
474
717
  const previewHoldMs = parsePreviewHoldMs(config);
718
+ const laneOptions = laneTurnOptions(config, streamOptions);
719
+ const steeredInputs = /* @__PURE__ */ new Map();
475
720
  const pendingDecisions = /* @__PURE__ */ new Map();
476
721
  let seq = 0;
477
722
  const inflight = /* @__PURE__ */ new Map();
@@ -707,6 +952,34 @@ var createFakeInstance = (config, host) => {
707
952
  ]);
708
953
  return;
709
954
  }
955
+ if (script === "lane") {
956
+ const text = await runLaneTurn({
957
+ threadId,
958
+ turnId,
959
+ cwd: booked.session.cwd ?? host.workspacePath(threadId),
960
+ input,
961
+ signal,
962
+ options: laneOptions,
963
+ emit: (type, payload, extra) => emit(type, threadId, payload, { turnId, ...extra }),
964
+ nextId,
965
+ takeSteered: () => (steeredInputs.get(threadId) ?? []).splice(0).join("\n"),
966
+ approve: (request) => openShellApproval({
967
+ requestId: request.requestId,
968
+ detail: request.detail,
969
+ signal,
970
+ nextItemId: () => nextId("item"),
971
+ pending: pendingDecisions,
972
+ emit: (type, payload, extra) => emit(type, threadId, payload, { turnId, ...extra })
973
+ })
974
+ });
975
+ finishTurn(
976
+ booked,
977
+ turnId,
978
+ signal.aborted || text === null ? "interrupted" : "completed",
979
+ text === null ? [] : [{ kind: "assistant", text }]
980
+ );
981
+ return;
982
+ }
710
983
  if (script === "approval-tool") {
711
984
  const finalText = await runApprovalToolScript(threadId, turnId, signal);
712
985
  finishTurn(
@@ -767,7 +1040,17 @@ var createFakeInstance = (config, host) => {
767
1040
  sendTurn: async (input, signal) => {
768
1041
  if (disposed) throw new Error("instance disposed");
769
1042
  const booked = requireThread(input.threadId);
770
- if (inflight.has(input.threadId)) throw new Error("turn already in flight");
1043
+ const running = inflight.get(input.threadId);
1044
+ const steered = acceptLaneSteer({
1045
+ running,
1046
+ enabled: script === "lane",
1047
+ expectedTurnId: input.expectedTurnId,
1048
+ text: input.input,
1049
+ threadId: input.threadId,
1050
+ steered: steeredInputs
1051
+ });
1052
+ if (steered !== null) return steered;
1053
+ if (running !== void 0) throw new Error("turn already in flight");
771
1054
  const turnId = input.expectedTurnId ?? nextId("turn");
772
1055
  const turnAbort = new AbortController();
773
1056
  const abortTurn = () => turnAbort.abort();
@@ -787,7 +1070,7 @@ var createFakeInstance = (config, host) => {
787
1070
  signal?.removeEventListener("abort", abortTurn);
788
1071
  if (inflight.get(input.threadId)?.turnId === turnId) inflight.delete(input.threadId);
789
1072
  };
790
- if (script === "approval-tool" || script === "stream" || script === "mcp-send" || script === "visualize") {
1073
+ if (script === "approval-tool" || script === "stream" || script === "mcp-send" || script === "visualize" || script === "lane") {
791
1074
  signal?.removeEventListener("abort", abortTurn);
792
1075
  void run().catch((error) => failTurn(booked, turnId, error)).finally(settle);
793
1076
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-fake",
3
- "version": "0.2.2944",
3
+ "version": "0.2.2948",
4
4
  "description": "Minimal fake executor for plugin authors and host orchestration tests.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -21,8 +21,8 @@
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.2944",
25
- "ycodium-executor-testing": "0.0.0"
24
+ "ycodium-executor-testing": "0.0.0",
25
+ "@ycodium-ai/plugin-api": "0.2.2948"
26
26
  },
27
27
  "ycodium": {
28
28
  "plugin": "./dist/index.js",