@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.
- package/README.md +1 -1
- package/dist/index.js +196 -88
- 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"`),
|
|
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: {
|
|
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
|
-
|
|
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(
|
|
320
|
-
|
|
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
|
|
636
|
+
if (inflight.has(input.threadId)) throw new Error("turn already in flight");
|
|
476
637
|
const turnId = input.expectedTurnId ?? nextId("turn");
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
492
|
-
|
|
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
|
-
|
|
557
|
-
if (inflight?.turnId === turnId) inflight = null;
|
|
665
|
+
settle();
|
|
558
666
|
}
|
|
559
667
|
},
|
|
560
668
|
interruptTurn: async (threadId, turnId) => {
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
if (turnId !== void 0 &&
|
|
564
|
-
host.log.info("interrupt turn", { threadId, turnId:
|
|
565
|
-
|
|
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
|
-
|
|
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
|
|
591
|
-
inflight
|
|
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.
|
|
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.
|
|
24
|
+
"@ycodium-ai/plugin-api": "0.2.2808",
|
|
25
25
|
"ycodium-executor-testing": "0.0.0"
|
|
26
26
|
},
|
|
27
27
|
"ycodium": {
|