@ycodium-ai/agent-fake 0.2.2678 → 0.2.2694
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 +4 -1
- package/dist/index.js +171 -2
- package/locales/en.json +9 -0
- package/locales/zh-CN.json +9 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,13 +4,16 @@ 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),
|
|
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)
|
|
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`
|
|
11
|
+
- `usage` answers quota and token reads deterministically (no network, no disk): `readQuota` returns a session and a weekly window that creep up by one percent per read, `readTokenUsage` returns one record per day for the past two weeks (alternating a priced and an unpriced model), `redeemQuotaReset` spends one of two reset credits and zeroes the session window
|
|
11
12
|
|
|
12
13
|
The card also shows how a third-party executor gets its own settings form: `configSchema` marks `replyText` with `x-ycodium-form`, and the labels resolve in this package's `locales/en.json` and `locales/zh-CN.json`. `replyText` is what the plain script answers; empty means `fake turn complete`.
|
|
13
14
|
|
|
15
|
+
The instance config `usageScript` picks how quota reads answer: `available` (default), `balance` (the API-key shape: a remaining CNY balance, no windows), `rate-limited`, `custom-endpoint` or `signed-out`. It is not in the settings form; edit the instance config to change it. The window labels are locale keys (`usage.window.session`, `usage.window.weekly`, `usage.balance.account`), a worked example of how an agent plugin labels its quota rows.
|
|
16
|
+
|
|
14
17
|
Two uses:
|
|
15
18
|
|
|
16
19
|
1. **Template** for an `@ycodium-ai/agent-*` author: one card, `createInstance`, lease-scoped `host.process.spawn` (this fake never calls it), `ctx.log` instead of console.
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,14 @@ 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"] },
|
|
43
|
+
script: { type: "string", enum: ["plain", "approval-tool", "async-question", "stream"] },
|
|
44
|
+
streamChunks: { type: "integer", minimum: 1 },
|
|
45
|
+
streamIntervalMs: { type: "integer", minimum: 0 },
|
|
46
|
+
// 额度与用量怎么答(见 fakeUsage.ts);不进设置表单,开发与验收时手改实例配置。
|
|
47
|
+
usageScript: {
|
|
48
|
+
type: "string",
|
|
49
|
+
enum: ["available", "balance", "rate-limited", "custom-endpoint", "signed-out"]
|
|
50
|
+
},
|
|
44
51
|
replyText: {
|
|
45
52
|
type: "string",
|
|
46
53
|
"x-ycodium-form": {
|
|
@@ -61,6 +68,127 @@ var FAKE_CARD = {
|
|
|
61
68
|
|
|
62
69
|
// src/fakeInstance.ts
|
|
63
70
|
import * as NodeCrypto from "node:crypto";
|
|
71
|
+
|
|
72
|
+
// src/fakeUsage.ts
|
|
73
|
+
var FAKE_USAGE_SCRIPTS = [
|
|
74
|
+
"available",
|
|
75
|
+
"balance",
|
|
76
|
+
"rate-limited",
|
|
77
|
+
"custom-endpoint",
|
|
78
|
+
"signed-out"
|
|
79
|
+
];
|
|
80
|
+
var parseUsageScript = (input) => {
|
|
81
|
+
const config = input?.config;
|
|
82
|
+
return FAKE_USAGE_SCRIPTS.find((script) => script === config?.usageScript) ?? "available";
|
|
83
|
+
};
|
|
84
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
85
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
86
|
+
var INITIAL_RESET_CREDITS = 2;
|
|
87
|
+
var FAKE_DAYS = 14;
|
|
88
|
+
var createFakeUsage = (input) => {
|
|
89
|
+
const now = input.now ?? Date.now;
|
|
90
|
+
let reads = 0;
|
|
91
|
+
let resetCredits = INITIAL_RESET_CREDITS;
|
|
92
|
+
let sessionUsed = 40;
|
|
93
|
+
const readQuota = async () => {
|
|
94
|
+
switch (input.script) {
|
|
95
|
+
case "custom-endpoint":
|
|
96
|
+
return { status: "unavailable", reason: "customEndpoint" };
|
|
97
|
+
case "signed-out":
|
|
98
|
+
return { status: "unavailable", reason: "missingCredentials" };
|
|
99
|
+
case "rate-limited":
|
|
100
|
+
return { status: "unavailable", reason: "rateLimited", retryAfterMs: 3e4 };
|
|
101
|
+
case "balance":
|
|
102
|
+
reads += 1;
|
|
103
|
+
return {
|
|
104
|
+
status: "available",
|
|
105
|
+
windows: [],
|
|
106
|
+
balances: [
|
|
107
|
+
{
|
|
108
|
+
id: "account",
|
|
109
|
+
label: { key: "usage.balance.account" },
|
|
110
|
+
remaining: Math.max(0, 100 - reads),
|
|
111
|
+
used: null,
|
|
112
|
+
limit: null,
|
|
113
|
+
unit: { kind: "money", currency: "CNY" },
|
|
114
|
+
resetsAt: null
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
accountKey: `fake:${input.instanceId}`
|
|
118
|
+
};
|
|
119
|
+
case "available":
|
|
120
|
+
reads += 1;
|
|
121
|
+
sessionUsed = Math.min(100, sessionUsed + 1);
|
|
122
|
+
return {
|
|
123
|
+
status: "available",
|
|
124
|
+
plan: "Fake Pro",
|
|
125
|
+
accountKey: `fake:${input.instanceId}`,
|
|
126
|
+
windows: [
|
|
127
|
+
{
|
|
128
|
+
id: "session",
|
|
129
|
+
label: { key: "usage.window.session" },
|
|
130
|
+
// 兑换过重置额度后,会话窗口从 0 重新算。
|
|
131
|
+
usedPercent: sessionUsed,
|
|
132
|
+
resetsAt: new Date(now() + 3 * HOUR_MS).toISOString(),
|
|
133
|
+
windowMs: 5 * HOUR_MS
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "weekly",
|
|
137
|
+
label: { key: "usage.window.weekly" },
|
|
138
|
+
usedPercent: Math.min(100, 16 + reads),
|
|
139
|
+
resetsAt: new Date(now() + 4 * DAY_MS).toISOString(),
|
|
140
|
+
windowMs: 7 * DAY_MS
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
balances: [],
|
|
144
|
+
resetCredits: { availableCount: resetCredits }
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const readTokenUsage = async (request) => {
|
|
149
|
+
const since = Date.parse(request.since);
|
|
150
|
+
const until = Date.parse(request.until);
|
|
151
|
+
const todayStart = Math.floor(now() / DAY_MS) * DAY_MS;
|
|
152
|
+
const records = [];
|
|
153
|
+
for (let age = 0; age < FAKE_DAYS; age += 1) {
|
|
154
|
+
const at = todayStart - age * DAY_MS + 9 * HOUR_MS;
|
|
155
|
+
if (at < since || at >= until) continue;
|
|
156
|
+
records.push({
|
|
157
|
+
at: new Date(at).toISOString(),
|
|
158
|
+
// gpt-4o 在价格表里,fake-model 不在:两种费用来源都能在页面上看到。
|
|
159
|
+
model: age % 2 === 0 ? "gpt-4o" : "fake-model",
|
|
160
|
+
sessionId: `fake-${input.instanceId}-${age}`,
|
|
161
|
+
totals: {
|
|
162
|
+
uncachedInputTokens: 1200 + 100 * age,
|
|
163
|
+
cachedInputTokens: 8e3,
|
|
164
|
+
cacheCreationTokens: 300,
|
|
165
|
+
outputTokens: 900 + 50 * age,
|
|
166
|
+
reasoningTokens: 100
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
records,
|
|
172
|
+
source: {
|
|
173
|
+
homePath: `fake://${input.instanceId}`,
|
|
174
|
+
volumeId: "fake",
|
|
175
|
+
status: "ok",
|
|
176
|
+
scannedFiles: FAKE_DAYS,
|
|
177
|
+
skippedFiles: 0
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
const redeemQuotaReset = async () => {
|
|
182
|
+
if (input.script !== "available") return { outcome: "nothingToReset" };
|
|
183
|
+
if (resetCredits <= 0) return { outcome: "noCredit" };
|
|
184
|
+
resetCredits -= 1;
|
|
185
|
+
sessionUsed = 0;
|
|
186
|
+
return { outcome: "reset" };
|
|
187
|
+
};
|
|
188
|
+
return { readQuota, readTokenUsage, redeemQuotaReset };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/fakeInstance.ts
|
|
64
192
|
var yieldTick = () => Promise.resolve();
|
|
65
193
|
var EventBus = class {
|
|
66
194
|
#queued = [];
|
|
@@ -118,8 +246,28 @@ var FAKE_ASYNC_QUESTIONS = [
|
|
|
118
246
|
function parseScript(input) {
|
|
119
247
|
const envelope = input;
|
|
120
248
|
const script = envelope?.config?.script;
|
|
121
|
-
return script === "approval-tool" || script === "async-question" ? script : "plain";
|
|
249
|
+
return script === "approval-tool" || script === "async-question" || script === "stream" ? script : "plain";
|
|
122
250
|
}
|
|
251
|
+
var FAKE_STREAM_TEXT = [
|
|
252
|
+
"Here is the plan for the change, step by step.\n\n",
|
|
253
|
+
"1. Read the current implementation and note the hot path.\n",
|
|
254
|
+
"2. Move the measurement out of the commit phase.\n",
|
|
255
|
+
"3. Verify the numbers on a production build.\n\n",
|
|
256
|
+
"```ts\nexport function measure(root: HTMLElement) {\n return root.getBoundingClientRect().height;\n}\n```\n\n",
|
|
257
|
+
"The **important** part is that the timeline only re-renders the streaming row, ",
|
|
258
|
+
"not the whole list, so each delta stays cheap even on a long conversation. "
|
|
259
|
+
].join("");
|
|
260
|
+
var parseStreamOptions = (input) => {
|
|
261
|
+
const config = input?.config ?? {};
|
|
262
|
+
const chunks = typeof config.streamChunks === "number" && config.streamChunks >= 1 ? Math.floor(config.streamChunks) : 200;
|
|
263
|
+
const intervalMs = typeof config.streamIntervalMs === "number" && config.streamIntervalMs >= 0 ? config.streamIntervalMs : 30;
|
|
264
|
+
return { chunks, intervalMs };
|
|
265
|
+
};
|
|
266
|
+
var fakeStreamChunk = (index) => {
|
|
267
|
+
const size = 12;
|
|
268
|
+
const start = index * size % FAKE_STREAM_TEXT.length;
|
|
269
|
+
return FAKE_STREAM_TEXT.slice(start, start + size) || FAKE_STREAM_TEXT.slice(0, size);
|
|
270
|
+
};
|
|
123
271
|
var parseReplyText = (input) => {
|
|
124
272
|
const envelope = input;
|
|
125
273
|
const replyText = envelope?.config?.replyText;
|
|
@@ -134,6 +282,7 @@ var createFakeInstance = (config, host) => {
|
|
|
134
282
|
const threads = /* @__PURE__ */ new Map();
|
|
135
283
|
const script = parseScript(config);
|
|
136
284
|
const replyText = parseReplyText(config);
|
|
285
|
+
const streamOptions = parseStreamOptions(config);
|
|
137
286
|
const pendingDecisions = /* @__PURE__ */ new Map();
|
|
138
287
|
let seq = 0;
|
|
139
288
|
let inflight = null;
|
|
@@ -364,6 +513,25 @@ var createFakeInstance = (config, host) => {
|
|
|
364
513
|
]);
|
|
365
514
|
return { threadId: input.threadId, turnId };
|
|
366
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
|
+
}
|
|
367
535
|
if (script === "approval-tool") {
|
|
368
536
|
const finalText = await runApprovalToolScript(input.threadId, turnId, signal);
|
|
369
537
|
finishTurn(booked, turnId, aborted ? "interrupted" : "completed", [
|
|
@@ -416,6 +584,7 @@ var createFakeInstance = (config, host) => {
|
|
|
416
584
|
return { threadId, turns: booked.turns };
|
|
417
585
|
},
|
|
418
586
|
events,
|
|
587
|
+
usage: createFakeUsage({ instanceId: host.instanceId, script: parseUsageScript(config) }),
|
|
419
588
|
dispose: async () => {
|
|
420
589
|
disposed = true;
|
|
421
590
|
inflight?.abort();
|
package/locales/en.json
CHANGED
|
@@ -5,5 +5,14 @@
|
|
|
5
5
|
"description": "What the fake executor answers to every message. Leave empty to use the built-in reply.",
|
|
6
6
|
"placeholder": "fake turn complete"
|
|
7
7
|
}
|
|
8
|
+
},
|
|
9
|
+
"usage": {
|
|
10
|
+
"window": {
|
|
11
|
+
"session": "Session (5 hours)",
|
|
12
|
+
"weekly": "Weekly"
|
|
13
|
+
},
|
|
14
|
+
"balance": {
|
|
15
|
+
"account": "Account balance"
|
|
16
|
+
}
|
|
8
17
|
}
|
|
9
18
|
}
|
package/locales/zh-CN.json
CHANGED
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.2694",
|
|
4
4
|
"description": "Minimal fake executor for plugin authors and host orchestration tests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ycodium",
|
|
@@ -21,7 +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.
|
|
24
|
+
"@ycodium-ai/plugin-api": "0.2.2694",
|
|
25
|
+
"ycodium-executor-testing": "0.0.0"
|
|
25
26
|
},
|
|
26
27
|
"ycodium": {
|
|
27
28
|
"plugin": "./dist/index.js",
|