@ycodium-ai/agent-fake 0.2.2729 → 0.2.2800

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 +183 -77
  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"`), 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",
@@ -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,9 +377,36 @@ var createFakeInstance = (config, host) => {
316
377
  status: "idle",
317
378
  updatedAt: now()
318
379
  };
319
- emit("turn.completed", booked.session.threadId, { state }, { turnId });
380
+ emit(
381
+ "turn.completed",
382
+ booked.session.threadId,
383
+ errorMessage === void 0 ? { state } : { state, errorMessage },
384
+ { turnId }
385
+ );
320
386
  inflight = null;
321
387
  };
388
+ const runMcpSendScript = async (booked, input) => {
389
+ let args;
390
+ try {
391
+ args = JSON.parse(input ?? "");
392
+ } catch {
393
+ return replyText;
394
+ }
395
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return replyText;
396
+ const server = builtinMcpServer(booked.mcpServers);
397
+ if (server === null) return "mcp-send: no MCP server was handed to this session";
398
+ try {
399
+ const result = await callMcpTool({
400
+ fetch: host.fetch,
401
+ server,
402
+ name: "send_message",
403
+ arguments: args
404
+ });
405
+ return `mcp-send: ${JSON.stringify(result)}`;
406
+ } catch (error) {
407
+ return `mcp-send failed: ${String(error)}`;
408
+ }
409
+ };
322
410
  const runApprovalToolScript = async (threadId, turnId, signal) => {
323
411
  const isAborted = () => signal?.aborted === true;
324
412
  const fakeReply = "\u597D";
@@ -434,6 +522,77 @@ var createFakeInstance = (config, host) => {
434
522
  await yieldTick();
435
523
  return isAborted() ? null : fakeReply;
436
524
  };
525
+ const runScriptedTurn = async (booked, threadId, turnId, signal, input) => {
526
+ const aborted = () => signal.aborted;
527
+ await yieldTick();
528
+ if (aborted()) {
529
+ finishTurn(booked, turnId, "interrupted", []);
530
+ return;
531
+ }
532
+ if (script === "async-question" && booked.asked !== true) {
533
+ booked.asked = true;
534
+ emit(
535
+ "content.delta",
536
+ threadId,
537
+ { streamKind: "assistant_text", delta: replyText },
538
+ { turnId }
539
+ );
540
+ emit(
541
+ "user-input.requested",
542
+ threadId,
543
+ { questions: FAKE_ASYNC_QUESTIONS, responseMode: "message" },
544
+ { turnId, requestId: nextId("question") }
545
+ );
546
+ await yieldTick();
547
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
548
+ { kind: "assistant", text: replyText }
549
+ ]);
550
+ return;
551
+ }
552
+ if (script === "mcp-send") {
553
+ const text = await runMcpSendScript(booked, input);
554
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
555
+ await yieldTick();
556
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
557
+ { kind: "assistant", text }
558
+ ]);
559
+ return;
560
+ }
561
+ if (script === "stream") {
562
+ let text = "";
563
+ for (let index = 0; index < streamOptions.chunks; index++) {
564
+ if (aborted()) break;
565
+ const delta = fakeStreamChunk(index);
566
+ text += delta;
567
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta }, { turnId });
568
+ await new Promise((resolve) => setTimeout(resolve, streamOptions.intervalMs));
569
+ }
570
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
571
+ { kind: "assistant", text }
572
+ ]);
573
+ return;
574
+ }
575
+ if (script === "approval-tool") {
576
+ const finalText = await runApprovalToolScript(threadId, turnId, signal);
577
+ finishTurn(
578
+ booked,
579
+ turnId,
580
+ aborted() ? "interrupted" : "completed",
581
+ finalText === null ? [] : [{ kind: "assistant", text: finalText }]
582
+ );
583
+ return;
584
+ }
585
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta: replyText }, { turnId });
586
+ await yieldTick();
587
+ finishTurn(booked, turnId, aborted() ? "interrupted" : "completed", [
588
+ { kind: "assistant", text: replyText }
589
+ ]);
590
+ };
591
+ const failTurn = (booked, turnId, error) => {
592
+ const message = error instanceof Error ? error.message : String(error);
593
+ host.log.warn("scripted turn failed", { threadId: booked.session.threadId, message });
594
+ finishTurn(booked, turnId, "failed", [], message);
595
+ };
437
596
  const instance = {
438
597
  probe: async () => ({
439
598
  installed: true,
@@ -460,7 +619,7 @@ var createFakeInstance = (config, host) => {
460
619
  ...input.discardResumeCursor === true || input.resumeCursor === void 0 ? {} : { resumeCursor: input.resumeCursor },
461
620
  ...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
462
621
  };
463
- threads.set(input.threadId, { session, turns: [] });
622
+ threads.set(input.threadId, { session, turns: [], mcpServers: input.mcpServers });
464
623
  emit(
465
624
  "session.started",
466
625
  input.threadId,
@@ -474,12 +633,11 @@ var createFakeInstance = (config, host) => {
474
633
  const booked = requireThread(input.threadId);
475
634
  if (inflight !== null) throw new Error("turn already in flight");
476
635
  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 };
636
+ const turnAbort = new AbortController();
637
+ const abortTurn = () => turnAbort.abort();
638
+ if (signal?.aborted === true) abortTurn();
639
+ signal?.addEventListener("abort", abortTurn);
640
+ inflight = { threadId: input.threadId, turnId, abort: abortTurn };
483
641
  booked.session = {
484
642
  ...booked.session,
485
643
  status: "running",
@@ -487,74 +645,22 @@ var createFakeInstance = (config, host) => {
487
645
  updatedAt: now()
488
646
  };
489
647
  emit("turn.started", input.threadId, {}, { turnId });
648
+ const result = { threadId: input.threadId, turnId };
649
+ const run = () => runScriptedTurn(booked, input.threadId, turnId, turnAbort.signal, input.input);
650
+ const settle = () => {
651
+ signal?.removeEventListener("abort", abortTurn);
652
+ if (inflight?.turnId === turnId) inflight = null;
653
+ };
654
+ if (script === "approval-tool" || script === "stream" || script === "mcp-send") {
655
+ signal?.removeEventListener("abort", abortTurn);
656
+ void run().catch((error) => failTurn(booked, turnId, error)).finally(settle);
657
+ return result;
658
+ }
490
659
  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 };
660
+ await run();
661
+ return result;
555
662
  } finally {
556
- signal?.removeEventListener("abort", onAbort);
557
- if (inflight?.turnId === turnId) inflight = null;
663
+ settle();
558
664
  }
559
665
  },
560
666
  interruptTurn: async (threadId, turnId) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-fake",
3
- "version": "0.2.2729",
3
+ "version": "0.2.2800",
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.2729",
25
- "ycodium-executor-testing": "0.0.0"
24
+ "ycodium-executor-testing": "0.0.0",
25
+ "@ycodium-ai/plugin-api": "0.2.2800"
26
26
  },
27
27
  "ycodium": {
28
28
  "plugin": "./dist/index.js",