@ycodium-ai/agent-fake 0.2.2743 → 0.2.2808

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 +196 -88
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@ A minimal executor plugin. It starts no CLI and replays a fixed event script:
4
4
 
5
5
  - `startSession` emits `session.started`
6
6
  - `sendTurn` emits `turn.started`, one `content.delta`, then `turn.completed` (the host binds the turn by `turn.started`)
7
- - `config.script` picks the script: `plain` (default), `approval-tool` (two approvals before the reply), `async-question` (on a thread's first turn, the reply plus one non-blocking `user-input.requested` with `responseMode: "message"`), or `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)
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), or `mcp-send` (a turn whose input is a JSON object is passed as the arguments of one real `send_message` call through the built-in MCP server the host handed to `startSession`, the same path a real agent takes; the reply is the tool result, and any other input gets the plain reply)
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
@@ -40,7 +40,10 @@ var FAKE_CARD = {
40
40
  additionalProperties: false,
41
41
  "x-ycodium-form-order": ["replyText"],
42
42
  properties: {
43
- script: { type: "string", enum: ["plain", "approval-tool", "async-question", "stream"] },
43
+ script: {
44
+ type: "string",
45
+ enum: ["plain", "approval-tool", "async-question", "stream", "mcp-send"]
46
+ },
44
47
  streamChunks: { type: "integer", minimum: 1 },
45
48
  streamIntervalMs: { type: "integer", minimum: 0 },
46
49
  // 额度与用量怎么答(见 fakeUsage.ts);不进设置表单,开发与验收时手改实例配置。
@@ -188,6 +191,64 @@ var createFakeUsage = (input) => {
188
191
  return { readQuota, readTokenUsage, redeemQuotaReset };
189
192
  };
190
193
 
194
+ // src/mcpSend.ts
195
+ var PROTOCOL_VERSION = "2025-06-18";
196
+ function builtinMcpServer(servers) {
197
+ const server = servers?.find((entry) => entry.transport.type === "http");
198
+ return server === void 0 ? null : server;
199
+ }
200
+ function parseMessage(text) {
201
+ const data = text.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
202
+ const json = data.length > 0 ? data.at(-1) : text.trim();
203
+ return json.length === 0 ? null : JSON.parse(json);
204
+ }
205
+ async function callMcpTool(input) {
206
+ const post = async (headers, body) => {
207
+ const response = await input.fetch(input.server.transport.url, {
208
+ method: "POST",
209
+ headers: {
210
+ "content-type": "application/json",
211
+ accept: "application/json, text/event-stream",
212
+ ...headers
213
+ },
214
+ body: JSON.stringify({ jsonrpc: "2.0", ...body })
215
+ });
216
+ const text = await response.text();
217
+ if (!response.ok)
218
+ throw new Error(
219
+ `MCP ${String(body.method)} failed: HTTP ${response.status} ${text.slice(0, 200)}`
220
+ );
221
+ return { response, message: parseMessage(text) };
222
+ };
223
+ const base = { ...input.server.transport.headers };
224
+ const initialized = await post(base, {
225
+ id: 1,
226
+ method: "initialize",
227
+ params: {
228
+ protocolVersion: PROTOCOL_VERSION,
229
+ capabilities: {},
230
+ clientInfo: { name: "agent-fake", version: "1.0.0" }
231
+ }
232
+ });
233
+ const sessionId = initialized.response.headers.get("mcp-session-id");
234
+ const session = {
235
+ ...base,
236
+ "mcp-protocol-version": PROTOCOL_VERSION,
237
+ ...sessionId === null ? {} : { "mcp-session-id": sessionId }
238
+ };
239
+ await post(session, { method: "notifications/initialized" });
240
+ const called = await post(session, {
241
+ id: 2,
242
+ method: "tools/call",
243
+ params: { name: input.name, arguments: input.arguments }
244
+ });
245
+ if (called.message?.error !== void 0)
246
+ throw new Error(
247
+ `MCP tools/call ${input.name} failed: ${called.message.error.message ?? "error"}`
248
+ );
249
+ return called.message?.result;
250
+ }
251
+
191
252
  // src/fakeInstance.ts
192
253
  var yieldTick = () => Promise.resolve();
193
254
  var EventBus = class {
@@ -246,7 +307,7 @@ var FAKE_ASYNC_QUESTIONS = [
246
307
  function parseScript(input) {
247
308
  const envelope = input;
248
309
  const script = envelope?.config?.script;
249
- return script === "approval-tool" || script === "async-question" || script === "stream" ? script : "plain";
310
+ return script === "approval-tool" || script === "async-question" || script === "stream" || script === "mcp-send" ? script : "plain";
250
311
  }
251
312
  var FAKE_STREAM_TEXT = [
252
313
  "Here is the plan for the change, step by step.\n\n",
@@ -285,7 +346,7 @@ var createFakeInstance = (config, host) => {
285
346
  const streamOptions = parseStreamOptions(config);
286
347
  const pendingDecisions = /* @__PURE__ */ new Map();
287
348
  let seq = 0;
288
- let inflight = null;
349
+ const inflight = /* @__PURE__ */ new Map();
289
350
  let disposed = false;
290
351
  const nextId = (prefix) => `${prefix}-fake-${++seq}-${NodeCrypto.randomUUID()}`;
291
352
  const now = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -308,7 +369,7 @@ var createFakeInstance = (config, host) => {
308
369
  if (!booked) throw new Error(`unknown thread ${threadId}`);
309
370
  return booked;
310
371
  };
311
- const finishTurn = (booked, turnId, state, items) => {
372
+ const finishTurn = (booked, turnId, state, items, errorMessage) => {
312
373
  booked.turns.push({ id: turnId, items });
313
374
  const { activeTurnId: _cleared, ...idle } = booked.session;
314
375
  booked.session = {
@@ -316,8 +377,37 @@ var createFakeInstance = (config, host) => {
316
377
  status: "idle",
317
378
  updatedAt: now()
318
379
  };
319
- emit("turn.completed", booked.session.threadId, { state }, { turnId });
320
- inflight = null;
380
+ emit(
381
+ "turn.completed",
382
+ booked.session.threadId,
383
+ errorMessage === void 0 ? { state } : { state, errorMessage },
384
+ { turnId }
385
+ );
386
+ if (inflight.get(booked.session.threadId)?.turnId === turnId) {
387
+ inflight.delete(booked.session.threadId);
388
+ }
389
+ };
390
+ const runMcpSendScript = async (booked, input) => {
391
+ let args;
392
+ try {
393
+ args = JSON.parse(input ?? "");
394
+ } catch {
395
+ return replyText;
396
+ }
397
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return replyText;
398
+ const server = builtinMcpServer(booked.mcpServers);
399
+ if (server === null) return "mcp-send: no MCP server was handed to this session";
400
+ try {
401
+ const result = await callMcpTool({
402
+ fetch: host.fetch,
403
+ server,
404
+ name: "send_message",
405
+ arguments: args
406
+ });
407
+ return `mcp-send: ${JSON.stringify(result)}`;
408
+ } catch (error) {
409
+ return `mcp-send failed: ${String(error)}`;
410
+ }
321
411
  };
322
412
  const runApprovalToolScript = async (threadId, turnId, signal) => {
323
413
  const isAborted = () => signal?.aborted === true;
@@ -434,6 +524,77 @@ var createFakeInstance = (config, host) => {
434
524
  await yieldTick();
435
525
  return isAborted() ? null : fakeReply;
436
526
  };
527
+ const runScriptedTurn = async (booked, threadId, turnId, signal, input) => {
528
+ const aborted = () => signal.aborted;
529
+ await yieldTick();
530
+ if (aborted()) {
531
+ finishTurn(booked, turnId, "interrupted", []);
532
+ return;
533
+ }
534
+ if (script === "async-question" && booked.asked !== true) {
535
+ booked.asked = true;
536
+ emit(
537
+ "content.delta",
538
+ threadId,
539
+ { streamKind: "assistant_text", delta: replyText },
540
+ { turnId }
541
+ );
542
+ emit(
543
+ "user-input.requested",
544
+ threadId,
545
+ { questions: FAKE_ASYNC_QUESTIONS, responseMode: "message" },
546
+ { turnId, requestId: nextId("question") }
547
+ );
548
+ await yieldTick();
549
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
550
+ { kind: "assistant", text: replyText }
551
+ ]);
552
+ return;
553
+ }
554
+ if (script === "mcp-send") {
555
+ const text = await runMcpSendScript(booked, input);
556
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
557
+ await yieldTick();
558
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
559
+ { kind: "assistant", text }
560
+ ]);
561
+ return;
562
+ }
563
+ if (script === "stream") {
564
+ let text = "";
565
+ for (let index = 0; index < streamOptions.chunks; index++) {
566
+ if (aborted()) break;
567
+ const delta = fakeStreamChunk(index);
568
+ text += delta;
569
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta }, { turnId });
570
+ await new Promise((resolve) => setTimeout(resolve, streamOptions.intervalMs));
571
+ }
572
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
573
+ { kind: "assistant", text }
574
+ ]);
575
+ return;
576
+ }
577
+ if (script === "approval-tool") {
578
+ const finalText = await runApprovalToolScript(threadId, turnId, signal);
579
+ finishTurn(
580
+ booked,
581
+ turnId,
582
+ aborted() ? "interrupted" : "completed",
583
+ finalText === null ? [] : [{ kind: "assistant", text: finalText }]
584
+ );
585
+ return;
586
+ }
587
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta: replyText }, { turnId });
588
+ await yieldTick();
589
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
590
+ { kind: "assistant", text: replyText }
591
+ ]);
592
+ };
593
+ const failTurn = (booked, turnId, error) => {
594
+ const message = error instanceof Error ? error.message : String(error);
595
+ host.log.warn("scripted turn failed", { threadId: booked.session.threadId, message });
596
+ finishTurn(booked, turnId, "failed", [], message);
597
+ };
437
598
  const instance = {
438
599
  probe: async () => ({
439
600
  installed: true,
@@ -460,7 +621,7 @@ var createFakeInstance = (config, host) => {
460
621
  ...input.discardResumeCursor === true || input.resumeCursor === void 0 ? {} : { resumeCursor: input.resumeCursor },
461
622
  ...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
462
623
  };
463
- threads.set(input.threadId, { session, turns: [] });
624
+ threads.set(input.threadId, { session, turns: [], mcpServers: input.mcpServers });
464
625
  emit(
465
626
  "session.started",
466
627
  input.threadId,
@@ -472,14 +633,13 @@ var createFakeInstance = (config, host) => {
472
633
  sendTurn: async (input, signal) => {
473
634
  if (disposed) throw new Error("instance disposed");
474
635
  const booked = requireThread(input.threadId);
475
- if (inflight !== null) throw new Error("turn already in flight");
636
+ if (inflight.has(input.threadId)) throw new Error("turn already in flight");
476
637
  const turnId = input.expectedTurnId ?? nextId("turn");
477
- let aborted = signal?.aborted === true;
478
- const onAbort = () => {
479
- aborted = true;
480
- };
481
- signal?.addEventListener("abort", onAbort);
482
- inflight = { threadId: input.threadId, turnId, abort: onAbort };
638
+ const turnAbort = new AbortController();
639
+ const abortTurn = () => turnAbort.abort();
640
+ if (signal?.aborted === true) abortTurn();
641
+ signal?.addEventListener("abort", abortTurn);
642
+ inflight.set(input.threadId, { turnId, abort: abortTurn });
483
643
  booked.session = {
484
644
  ...booked.session,
485
645
  status: "running",
@@ -487,82 +647,30 @@ var createFakeInstance = (config, host) => {
487
647
  updatedAt: now()
488
648
  };
489
649
  emit("turn.started", input.threadId, {}, { turnId });
650
+ const result = { threadId: input.threadId, turnId };
651
+ const run = () => runScriptedTurn(booked, input.threadId, turnId, turnAbort.signal, input.input);
652
+ const settle = () => {
653
+ signal?.removeEventListener("abort", abortTurn);
654
+ if (inflight.get(input.threadId)?.turnId === turnId) inflight.delete(input.threadId);
655
+ };
656
+ if (script === "approval-tool" || script === "stream" || script === "mcp-send") {
657
+ signal?.removeEventListener("abort", abortTurn);
658
+ void run().catch((error) => failTurn(booked, turnId, error)).finally(settle);
659
+ return result;
660
+ }
490
661
  try {
491
- await yieldTick();
492
- if (aborted) {
493
- finishTurn(booked, turnId, "interrupted", []);
494
- return { threadId: input.threadId, turnId };
495
- }
496
- if (script === "async-question" && booked.asked !== true) {
497
- booked.asked = true;
498
- emit(
499
- "content.delta",
500
- input.threadId,
501
- { streamKind: "assistant_text", delta: replyText },
502
- { turnId }
503
- );
504
- emit(
505
- "user-input.requested",
506
- input.threadId,
507
- { questions: FAKE_ASYNC_QUESTIONS, responseMode: "message" },
508
- { turnId, requestId: nextId("question") }
509
- );
510
- await yieldTick();
511
- finishTurn(booked, turnId, aborted ? "interrupted" : "completed", [
512
- { kind: "assistant", text: replyText }
513
- ]);
514
- return { threadId: input.threadId, turnId };
515
- }
516
- if (script === "stream") {
517
- let text = "";
518
- for (let index = 0; index < streamOptions.chunks; index++) {
519
- if (aborted) break;
520
- const delta = fakeStreamChunk(index);
521
- text += delta;
522
- emit(
523
- "content.delta",
524
- input.threadId,
525
- { streamKind: "assistant_text", delta },
526
- { turnId }
527
- );
528
- await new Promise((resolve) => setTimeout(resolve, streamOptions.intervalMs));
529
- }
530
- finishTurn(booked, turnId, aborted ? "interrupted" : "completed", [
531
- { kind: "assistant", text }
532
- ]);
533
- return { threadId: input.threadId, turnId };
534
- }
535
- if (script === "approval-tool") {
536
- const finalText = await runApprovalToolScript(input.threadId, turnId, signal);
537
- finishTurn(booked, turnId, aborted ? "interrupted" : "completed", [
538
- ...finalText === null ? [] : [{ kind: "assistant", text: finalText }]
539
- ]);
540
- return { threadId: input.threadId, turnId };
541
- }
542
- emit(
543
- "content.delta",
544
- input.threadId,
545
- { streamKind: "assistant_text", delta: replyText },
546
- { turnId }
547
- );
548
- await yieldTick();
549
- if (aborted) {
550
- finishTurn(booked, turnId, "interrupted", [{ kind: "assistant", text: replyText }]);
551
- return { threadId: input.threadId, turnId };
552
- }
553
- finishTurn(booked, turnId, "completed", [{ kind: "assistant", text: replyText }]);
554
- return { threadId: input.threadId, turnId };
662
+ await run();
663
+ return result;
555
664
  } finally {
556
- signal?.removeEventListener("abort", onAbort);
557
- if (inflight?.turnId === turnId) inflight = null;
665
+ settle();
558
666
  }
559
667
  },
560
668
  interruptTurn: async (threadId, turnId) => {
561
- if (inflight === null) return;
562
- if (inflight.threadId !== threadId) return;
563
- if (turnId !== void 0 && inflight.turnId !== turnId) return;
564
- host.log.info("interrupt turn", { threadId, turnId: inflight.turnId });
565
- inflight.abort();
669
+ const running = inflight.get(threadId);
670
+ if (running === void 0) return;
671
+ if (turnId !== void 0 && running.turnId !== turnId) return;
672
+ host.log.info("interrupt turn", { threadId, turnId: running.turnId });
673
+ running.abort();
566
674
  },
567
675
  respondToRequest: async (threadId, requestId, decision) => {
568
676
  host.log.info("respondToRequest", { threadId, requestId, decision });
@@ -576,7 +684,7 @@ var createFakeInstance = (config, host) => {
576
684
  host.log.info("respondToUserInput ignored");
577
685
  },
578
686
  stopSession: async (threadId) => {
579
- if (inflight?.threadId === threadId) inflight.abort();
687
+ inflight.get(threadId)?.abort();
580
688
  threads.delete(threadId);
581
689
  },
582
690
  readThread: async (threadId) => {
@@ -587,8 +695,8 @@ var createFakeInstance = (config, host) => {
587
695
  usage: createFakeUsage({ instanceId: host.instanceId, script: parseUsageScript(config) }),
588
696
  dispose: async () => {
589
697
  disposed = true;
590
- inflight?.abort();
591
- inflight = null;
698
+ for (const running of inflight.values()) running.abort();
699
+ inflight.clear();
592
700
  for (const settle of pendingDecisions.values()) settle("cancel");
593
701
  pendingDecisions.clear();
594
702
  threads.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-fake",
3
- "version": "0.2.2743",
3
+ "version": "0.2.2808",
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.2743",
24
+ "@ycodium-ai/plugin-api": "0.2.2808",
25
25
  "ycodium-executor-testing": "0.0.0"
26
26
  },
27
27
  "ycodium": {