@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.
- package/README.md +1 -1
- package/dist/index.js +183 -77
- 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"`),
|
|
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",
|
|
@@ -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(
|
|
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
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
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 };
|
|
660
|
+
await run();
|
|
661
|
+
return result;
|
|
555
662
|
} finally {
|
|
556
|
-
|
|
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.
|
|
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
|
-
"
|
|
25
|
-
"ycodium-
|
|
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",
|