@ycodium-ai/agent-fake 0.2.2938 → 0.2.2944

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 +145 -21
  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"`), `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)
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.”)
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
@@ -42,7 +42,7 @@ var FAKE_CARD = {
42
42
  properties: {
43
43
  script: {
44
44
  type: "string",
45
- enum: ["plain", "approval-tool", "async-question", "stream", "mcp-send"]
45
+ enum: ["plain", "approval-tool", "async-question", "stream", "mcp-send", "visualize"]
46
46
  },
47
47
  streamChunks: { type: "integer", minimum: 1 },
48
48
  streamIntervalMs: { type: "integer", minimum: 0 },
@@ -250,6 +250,124 @@ async function callMcpTool(input) {
250
250
  );
251
251
  return called.message?.result;
252
252
  }
253
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
254
+ function parseMcpSendInput(input) {
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(input ?? "");
258
+ } catch {
259
+ return null;
260
+ }
261
+ if (isRecord(parsed)) return [{ tool: "send_message", arguments: parsed }];
262
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
263
+ const calls = [];
264
+ for (const entry of parsed) {
265
+ if (!isRecord(entry) || typeof entry.tool !== "string" || entry.tool.length === 0) return null;
266
+ const args = entry.arguments ?? {};
267
+ if (!isRecord(args)) return null;
268
+ calls.push({ tool: entry.tool, arguments: args });
269
+ }
270
+ return calls;
271
+ }
272
+ async function runMcpCalls(input) {
273
+ const lines = [];
274
+ for (const call of input.calls) {
275
+ try {
276
+ const result = await callMcpTool({
277
+ fetch: input.fetch,
278
+ server: input.server,
279
+ name: call.tool,
280
+ arguments: call.arguments
281
+ });
282
+ lines.push(`mcp-send ${call.tool}: ${JSON.stringify(result)}`);
283
+ } catch (error) {
284
+ lines.push(`mcp-send ${call.tool} failed: ${String(error)}`);
285
+ }
286
+ }
287
+ return lines.join("\n");
288
+ }
289
+
290
+ // src/visualizeScript.ts
291
+ var WIRE_NAME = "mcp__ycodium__visualize_visualize";
292
+ var TITLE = "Monthly sales";
293
+ var FRAGMENT = "<section><h1>Monthly sales</h1><p>Jan 12 \xB7 Feb 18 \xB7 Mar 15</p></section>";
294
+ var REPLY = "The figure is in the card above.";
295
+ function parsePreviewHoldMs(input) {
296
+ const config = input?.config;
297
+ const raw = config?.previewHoldMs;
298
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return 0;
299
+ return Math.min(3e4, Math.floor(raw));
300
+ }
301
+ function confirmationText(result) {
302
+ const content = result?.content;
303
+ if (!Array.isArray(content)) return "";
304
+ const chunks = [];
305
+ for (const entry of content) {
306
+ const text = entry?.text;
307
+ if (typeof text === "string" && text.length > 0) chunks.push(text);
308
+ }
309
+ return chunks.join("\n");
310
+ }
311
+ function hold(ms, signal) {
312
+ if (ms === 0 || signal.aborted) return Promise.resolve();
313
+ return new Promise((resolve) => {
314
+ let timer;
315
+ const finish = () => {
316
+ if (timer !== void 0) clearTimeout(timer);
317
+ signal.removeEventListener("abort", finish);
318
+ resolve();
319
+ };
320
+ timer = setTimeout(finish, ms);
321
+ signal.addEventListener("abort", finish, { once: true });
322
+ });
323
+ }
324
+ async function runVisualizeTurn(input) {
325
+ const args = JSON.stringify({ action: "create", title: TITLE, fragment: FRAGMENT });
326
+ const tool = (status, result) => ({
327
+ itemType: "dynamic_tool_call",
328
+ status,
329
+ title: WIRE_NAME,
330
+ data: {
331
+ toolName: WIRE_NAME,
332
+ argumentText: args,
333
+ ...result === void 0 ? {} : { result }
334
+ }
335
+ });
336
+ input.emit("item.started", input.threadId, tool("inProgress"), {
337
+ turnId: input.turnId,
338
+ itemId: input.itemId
339
+ });
340
+ await hold(input.holdMs, input.signal);
341
+ if (input.signal.aborted) return null;
342
+ const server = builtinMcpServer(input.mcpServers);
343
+ if (server === null) {
344
+ const failure = "visualize: no MCP server was handed to this session";
345
+ input.emit("item.completed", input.threadId, tool("completed", failure), {
346
+ turnId: input.turnId,
347
+ itemId: input.itemId
348
+ });
349
+ return failure;
350
+ }
351
+ let resultText;
352
+ try {
353
+ resultText = confirmationText(
354
+ await callMcpTool({
355
+ fetch: input.fetch,
356
+ server,
357
+ name: "visualize_visualize",
358
+ arguments: { action: "create", title: TITLE, fragment: FRAGMENT }
359
+ })
360
+ );
361
+ } catch (error) {
362
+ resultText = `visualize failed: ${String(error)}`;
363
+ }
364
+ if (input.signal.aborted) return null;
365
+ input.emit("item.completed", input.threadId, tool("completed", resultText), {
366
+ turnId: input.turnId,
367
+ itemId: input.itemId
368
+ });
369
+ return resultText.length > 0 ? REPLY : "visualize failed: empty tool result";
370
+ }
253
371
 
254
372
  // src/fakeInstance.ts
255
373
  var yieldTick = () => Promise.resolve();
@@ -309,7 +427,7 @@ var FAKE_ASYNC_QUESTIONS = [
309
427
  function parseScript(input) {
310
428
  const envelope = input;
311
429
  const script = envelope?.config?.script;
312
- return script === "approval-tool" || script === "async-question" || script === "stream" || script === "mcp-send" ? script : "plain";
430
+ return script === "approval-tool" || script === "async-question" || script === "stream" || script === "mcp-send" || script === "visualize" ? script : "plain";
313
431
  }
314
432
  var FAKE_STREAM_TEXT = [
315
433
  "Here is the plan for the change, step by step.\n\n",
@@ -353,6 +471,7 @@ var createFakeInstance = (config, host) => {
353
471
  const replyText = parseReplyText(config);
354
472
  const failStartSession = parseFailStartSession(config);
355
473
  const streamOptions = parseStreamOptions(config);
474
+ const previewHoldMs = parsePreviewHoldMs(config);
356
475
  const pendingDecisions = /* @__PURE__ */ new Map();
357
476
  let seq = 0;
358
477
  const inflight = /* @__PURE__ */ new Map();
@@ -397,26 +516,11 @@ var createFakeInstance = (config, host) => {
397
516
  }
398
517
  };
399
518
  const runMcpSendScript = async (booked, input) => {
400
- let args;
401
- try {
402
- args = JSON.parse(input ?? "");
403
- } catch {
404
- return replyText;
405
- }
406
- if (typeof args !== "object" || args === null || Array.isArray(args)) return replyText;
519
+ const calls = parseMcpSendInput(input);
520
+ if (calls === null) return replyText;
407
521
  const server = builtinMcpServer(booked.mcpServers);
408
522
  if (server === null) return "mcp-send: no MCP server was handed to this session";
409
- try {
410
- const result = await callMcpTool({
411
- fetch: host.fetch,
412
- server,
413
- name: "send_message",
414
- arguments: args
415
- });
416
- return `mcp-send: ${JSON.stringify(result)}`;
417
- } catch (error) {
418
- return `mcp-send failed: ${String(error)}`;
419
- }
523
+ return runMcpCalls({ fetch: host.fetch, server, calls });
420
524
  };
421
525
  const runApprovalToolScript = async (threadId, turnId, signal) => {
422
526
  const isAborted = () => signal?.aborted === true;
@@ -560,6 +664,26 @@ var createFakeInstance = (config, host) => {
560
664
  ]);
561
665
  return;
562
666
  }
667
+ if (script === "visualize") {
668
+ const text = await runVisualizeTurn({
669
+ threadId,
670
+ turnId,
671
+ itemId: nextId("item"),
672
+ holdMs: previewHoldMs,
673
+ signal,
674
+ fetch: host.fetch,
675
+ mcpServers: booked.mcpServers,
676
+ emit
677
+ });
678
+ if (text === null || aborted()) {
679
+ finishTurn(booked, turnId, "interrupted", []);
680
+ return;
681
+ }
682
+ emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
683
+ await yieldTick();
684
+ finishTurn(booked, turnId, "completed", [{ kind: "assistant", text }]);
685
+ return;
686
+ }
563
687
  if (script === "mcp-send") {
564
688
  const text = await runMcpSendScript(booked, input);
565
689
  emit("content.delta", threadId, { streamKind: "assistant_text", delta: text }, { turnId });
@@ -663,7 +787,7 @@ var createFakeInstance = (config, host) => {
663
787
  signal?.removeEventListener("abort", abortTurn);
664
788
  if (inflight.get(input.threadId)?.turnId === turnId) inflight.delete(input.threadId);
665
789
  };
666
- if (script === "approval-tool" || script === "stream" || script === "mcp-send") {
790
+ if (script === "approval-tool" || script === "stream" || script === "mcp-send" || script === "visualize") {
667
791
  signal?.removeEventListener("abort", abortTurn);
668
792
  void run().catch((error) => failTurn(booked, turnId, error)).finally(settle);
669
793
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-fake",
3
- "version": "0.2.2938",
3
+ "version": "0.2.2944",
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.2938",
24
+ "@ycodium-ai/plugin-api": "0.2.2944",
25
25
  "ycodium-executor-testing": "0.0.0"
26
26
  },
27
27
  "ycodium": {