@witqq/agent-sdk 0.7.0 → 0.9.0
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/dist/{types-CqvUAYxt.d.ts → agent-C6H2CgJA.d.cts} +139 -102
- package/dist/{types-CqvUAYxt.d.cts → agent-F7oB6eKp.d.ts} +139 -102
- package/dist/auth/index.cjs +72 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +21 -154
- package/dist/auth/index.d.ts +21 -154
- package/dist/auth/index.js +72 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/backends/claude.cjs +480 -261
- package/dist/backends/claude.cjs.map +1 -1
- package/dist/backends/claude.d.cts +3 -1
- package/dist/backends/claude.d.ts +3 -1
- package/dist/backends/claude.js +480 -261
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/copilot.cjs +337 -112
- package/dist/backends/copilot.cjs.map +1 -1
- package/dist/backends/copilot.d.cts +12 -4
- package/dist/backends/copilot.d.ts +12 -4
- package/dist/backends/copilot.js +337 -112
- package/dist/backends/copilot.js.map +1 -1
- package/dist/backends/mock-llm.cjs +719 -0
- package/dist/backends/mock-llm.cjs.map +1 -0
- package/dist/backends/mock-llm.d.cts +37 -0
- package/dist/backends/mock-llm.d.ts +37 -0
- package/dist/backends/mock-llm.js +717 -0
- package/dist/backends/mock-llm.js.map +1 -0
- package/dist/backends/vercel-ai.cjs +301 -61
- package/dist/backends/vercel-ai.cjs.map +1 -1
- package/dist/backends/vercel-ai.d.cts +3 -1
- package/dist/backends/vercel-ai.d.ts +3 -1
- package/dist/backends/vercel-ai.js +301 -61
- package/dist/backends/vercel-ai.js.map +1 -1
- package/dist/backends-Cno0gZjy.d.cts +114 -0
- package/dist/backends-Cno0gZjy.d.ts +114 -0
- package/dist/chat/accumulator.cjs +1 -1
- package/dist/chat/accumulator.cjs.map +1 -1
- package/dist/chat/accumulator.d.cts +5 -2
- package/dist/chat/accumulator.d.ts +5 -2
- package/dist/chat/accumulator.js +1 -1
- package/dist/chat/accumulator.js.map +1 -1
- package/dist/chat/backends.cjs +1084 -821
- package/dist/chat/backends.cjs.map +1 -1
- package/dist/chat/backends.d.cts +10 -6
- package/dist/chat/backends.d.ts +10 -6
- package/dist/chat/backends.js +1082 -800
- package/dist/chat/backends.js.map +1 -1
- package/dist/chat/context.cjs +50 -0
- package/dist/chat/context.cjs.map +1 -1
- package/dist/chat/context.d.cts +27 -3
- package/dist/chat/context.d.ts +27 -3
- package/dist/chat/context.js +50 -0
- package/dist/chat/context.js.map +1 -1
- package/dist/chat/core.cjs +60 -27
- package/dist/chat/core.cjs.map +1 -1
- package/dist/chat/core.d.cts +41 -382
- package/dist/chat/core.d.ts +41 -382
- package/dist/chat/core.js +58 -28
- package/dist/chat/core.js.map +1 -1
- package/dist/chat/errors.cjs +48 -26
- package/dist/chat/errors.cjs.map +1 -1
- package/dist/chat/errors.d.cts +6 -31
- package/dist/chat/errors.d.ts +6 -31
- package/dist/chat/errors.js +48 -25
- package/dist/chat/errors.js.map +1 -1
- package/dist/chat/events.cjs.map +1 -1
- package/dist/chat/events.d.cts +6 -2
- package/dist/chat/events.d.ts +6 -2
- package/dist/chat/events.js.map +1 -1
- package/dist/chat/index.cjs +1612 -1125
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +35 -10
- package/dist/chat/index.d.ts +35 -10
- package/dist/chat/index.js +1600 -1097
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react/theme.css +2517 -0
- package/dist/chat/react.cjs +2212 -1158
- package/dist/chat/react.cjs.map +1 -1
- package/dist/chat/react.d.cts +665 -122
- package/dist/chat/react.d.ts +665 -122
- package/dist/chat/react.js +2191 -1156
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/runtime.cjs +405 -186
- package/dist/chat/runtime.cjs.map +1 -1
- package/dist/chat/runtime.d.cts +92 -28
- package/dist/chat/runtime.d.ts +92 -28
- package/dist/chat/runtime.js +405 -186
- package/dist/chat/runtime.js.map +1 -1
- package/dist/chat/server.cjs +2247 -212
- package/dist/chat/server.cjs.map +1 -1
- package/dist/chat/server.d.cts +451 -90
- package/dist/chat/server.d.ts +451 -90
- package/dist/chat/server.js +2234 -213
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/sessions.cjs +64 -66
- package/dist/chat/sessions.cjs.map +1 -1
- package/dist/chat/sessions.d.cts +37 -118
- package/dist/chat/sessions.d.ts +37 -118
- package/dist/chat/sessions.js +65 -67
- package/dist/chat/sessions.js.map +1 -1
- package/dist/chat/sqlite.cjs +536 -0
- package/dist/chat/sqlite.cjs.map +1 -0
- package/dist/chat/sqlite.d.cts +164 -0
- package/dist/chat/sqlite.d.ts +164 -0
- package/dist/chat/sqlite.js +527 -0
- package/dist/chat/sqlite.js.map +1 -0
- package/dist/chat/state.cjs +14 -1
- package/dist/chat/state.cjs.map +1 -1
- package/dist/chat/state.d.cts +5 -2
- package/dist/chat/state.d.ts +5 -2
- package/dist/chat/state.js +14 -1
- package/dist/chat/state.js.map +1 -1
- package/dist/chat/storage.cjs +58 -33
- package/dist/chat/storage.cjs.map +1 -1
- package/dist/chat/storage.d.cts +18 -8
- package/dist/chat/storage.d.ts +18 -8
- package/dist/chat/storage.js +59 -34
- package/dist/chat/storage.js.map +1 -1
- package/dist/errors-C-so0M4t.d.cts +33 -0
- package/dist/errors-C-so0M4t.d.ts +33 -0
- package/dist/errors-CmVvczxZ.d.cts +28 -0
- package/dist/errors-CmVvczxZ.d.ts +28 -0
- package/dist/{in-process-transport-C2oPTYs6.d.ts → in-process-transport-7EIit9Xk.d.ts} +72 -33
- package/dist/{in-process-transport-DG-w5G6k.d.cts → in-process-transport-Ct9YcX8I.d.cts} +72 -33
- package/dist/index.cjs +354 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -123
- package/dist/index.d.ts +294 -123
- package/dist/index.js +347 -60
- package/dist/index.js.map +1 -1
- package/dist/provider-types-PTSlRPNB.d.cts +39 -0
- package/dist/provider-types-PTSlRPNB.d.ts +39 -0
- package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
- package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
- package/dist/testing.cjs +1107 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +144 -0
- package/dist/testing.d.ts +144 -0
- package/dist/testing.js +1101 -0
- package/dist/testing.js.map +1 -0
- package/dist/token-store-CSUBgYwn.d.ts +48 -0
- package/dist/token-store-CuC4hB9Z.d.cts +48 -0
- package/dist/{transport-DX1Nhm4N.d.cts → transport-DLWCN18G.d.cts} +5 -4
- package/dist/{transport-D1OaUgRk.d.ts → transport-DsuS-GeM.d.ts} +5 -4
- package/dist/{types-CGF7AEX1.d.cts → types-4vbcmPTp.d.cts} +4 -2
- package/dist/{types-Bh5AhqD-.d.ts → types-BxggH0Yh.d.ts} +4 -2
- package/dist/types-DgtI1hzh.d.ts +364 -0
- package/dist/types-DkSXALKg.d.cts +364 -0
- package/package.json +41 -5
- package/LICENSE +0 -21
- package/README.md +0 -948
- package/dist/errors-BDLbNu9w.d.cts +0 -13
- package/dist/errors-BDLbNu9w.d.ts +0 -13
- package/dist/types-DLZzlJxt.d.ts +0 -39
- package/dist/types-tE0CXwBl.d.cts +0 -39
package/dist/testing.js
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
// src/types/errors.ts
|
|
2
|
+
var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
3
|
+
"TIMEOUT" /* TIMEOUT */,
|
|
4
|
+
"RATE_LIMIT" /* RATE_LIMIT */,
|
|
5
|
+
"NETWORK" /* NETWORK */,
|
|
6
|
+
"TOOL_EXECUTION" /* TOOL_EXECUTION */,
|
|
7
|
+
"MODEL_OVERLOADED" /* MODEL_OVERLOADED */,
|
|
8
|
+
"PROVIDER_ERROR" /* PROVIDER_ERROR */
|
|
9
|
+
]);
|
|
10
|
+
function isRecoverableErrorCode(code) {
|
|
11
|
+
return RECOVERABLE_CODES.has(code);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/errors.ts
|
|
15
|
+
var AgentSDKError = class extends Error {
|
|
16
|
+
/** @internal Marker for cross-bundle identity checks */
|
|
17
|
+
_agentSDKError = true;
|
|
18
|
+
/** Machine-readable error code. Prefer values from the ErrorCode enum. */
|
|
19
|
+
code;
|
|
20
|
+
/** Whether this error is safe to retry */
|
|
21
|
+
retryable;
|
|
22
|
+
/** HTTP status code hint for error classification */
|
|
23
|
+
httpStatus;
|
|
24
|
+
constructor(message, options) {
|
|
25
|
+
super(message, options);
|
|
26
|
+
this.name = "AgentSDKError";
|
|
27
|
+
this.code = options?.code;
|
|
28
|
+
this.retryable = options?.retryable ?? false;
|
|
29
|
+
this.httpStatus = options?.httpStatus;
|
|
30
|
+
}
|
|
31
|
+
/** Check if an error is an AgentSDKError (works across bundled copies) */
|
|
32
|
+
static is(error) {
|
|
33
|
+
return error instanceof Error && "_agentSDKError" in error && error._agentSDKError === true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var ReentrancyError = class extends AgentSDKError {
|
|
37
|
+
constructor() {
|
|
38
|
+
super("Agent is already running. Await the current run before starting another.", {
|
|
39
|
+
code: "REENTRANCY" /* REENTRANCY */
|
|
40
|
+
});
|
|
41
|
+
this.name = "ReentrancyError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var DisposedError = class extends AgentSDKError {
|
|
45
|
+
constructor(entity) {
|
|
46
|
+
super(`${entity} has been disposed and cannot be used.`, {
|
|
47
|
+
code: "DISPOSED" /* DISPOSED */
|
|
48
|
+
});
|
|
49
|
+
this.name = "DisposedError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var AbortError = class extends AgentSDKError {
|
|
53
|
+
constructor() {
|
|
54
|
+
super("Agent run was aborted.", { code: "ABORTED" /* ABORTED */ });
|
|
55
|
+
this.name = "AbortError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ActivityTimeoutError = class extends AgentSDKError {
|
|
59
|
+
constructor(timeoutMs) {
|
|
60
|
+
super(`Stream activity timeout: no event received within ${timeoutMs}ms.`, {
|
|
61
|
+
code: "TIMEOUT" /* TIMEOUT */,
|
|
62
|
+
retryable: true
|
|
63
|
+
});
|
|
64
|
+
this.name = "ActivityTimeoutError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/base-agent.ts
|
|
69
|
+
var BaseAgent = class {
|
|
70
|
+
state = "idle";
|
|
71
|
+
abortController = null;
|
|
72
|
+
config;
|
|
73
|
+
_cleanupExternalSignal = null;
|
|
74
|
+
_streamMiddleware = [];
|
|
75
|
+
/** CLI session ID for persistent mode. Override in backends that support it. */
|
|
76
|
+
get sessionId() {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
constructor(config) {
|
|
80
|
+
this.config = Object.freeze({ ...config });
|
|
81
|
+
}
|
|
82
|
+
// ─── Public Interface ─────────────────────────────────────────
|
|
83
|
+
async run(prompt, options) {
|
|
84
|
+
this.guardReentrancy();
|
|
85
|
+
this.guardDisposed();
|
|
86
|
+
const ac = this.createAbortController(options?.signal);
|
|
87
|
+
this.state = "running";
|
|
88
|
+
try {
|
|
89
|
+
const messages = [{ role: "user", content: prompt }];
|
|
90
|
+
const result = await this.withRetry(
|
|
91
|
+
() => this.executeRun(messages, options, ac.signal),
|
|
92
|
+
options
|
|
93
|
+
);
|
|
94
|
+
this.enrichAndNotifyUsage(result, options);
|
|
95
|
+
return result;
|
|
96
|
+
} finally {
|
|
97
|
+
this.cleanupRun();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async runWithContext(messages, options) {
|
|
101
|
+
this.guardReentrancy();
|
|
102
|
+
this.guardDisposed();
|
|
103
|
+
const ac = this.createAbortController(options?.signal);
|
|
104
|
+
this.state = "running";
|
|
105
|
+
try {
|
|
106
|
+
const result = await this.withRetry(
|
|
107
|
+
() => this.executeRun(messages, options, ac.signal),
|
|
108
|
+
options
|
|
109
|
+
);
|
|
110
|
+
this.enrichAndNotifyUsage(result, options);
|
|
111
|
+
return result;
|
|
112
|
+
} finally {
|
|
113
|
+
this.cleanupRun();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async runStructured(prompt, schema, options) {
|
|
117
|
+
this.guardReentrancy();
|
|
118
|
+
this.guardDisposed();
|
|
119
|
+
const ac = this.createAbortController(options?.signal);
|
|
120
|
+
this.state = "running";
|
|
121
|
+
try {
|
|
122
|
+
const messages = [{ role: "user", content: prompt }];
|
|
123
|
+
const result = await this.withRetry(
|
|
124
|
+
() => this.executeRunStructured(messages, schema, options, ac.signal),
|
|
125
|
+
options
|
|
126
|
+
);
|
|
127
|
+
this.enrichAndNotifyUsage(result, options);
|
|
128
|
+
return result;
|
|
129
|
+
} finally {
|
|
130
|
+
this.cleanupRun();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async *stream(prompt, options) {
|
|
134
|
+
this.guardReentrancy();
|
|
135
|
+
this.guardDisposed();
|
|
136
|
+
const ac = this.createAbortController(options?.signal);
|
|
137
|
+
this.state = "streaming";
|
|
138
|
+
try {
|
|
139
|
+
const messages = [{ role: "user", content: prompt }];
|
|
140
|
+
yield* this.streamWithRetry(
|
|
141
|
+
() => this.applyStreamPipeline(this.executeStream(messages, options, ac.signal), options, ac),
|
|
142
|
+
options
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
this.cleanupRun();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async *streamWithContext(messages, options) {
|
|
149
|
+
this.guardReentrancy();
|
|
150
|
+
this.guardDisposed();
|
|
151
|
+
const ac = this.createAbortController(options?.signal);
|
|
152
|
+
this.state = "streaming";
|
|
153
|
+
try {
|
|
154
|
+
yield* this.streamWithRetry(
|
|
155
|
+
() => this.applyStreamPipeline(this.executeStream(messages, options, ac.signal), options, ac),
|
|
156
|
+
options
|
|
157
|
+
);
|
|
158
|
+
} finally {
|
|
159
|
+
this.cleanupRun();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Register a stream middleware. Applied in registration order after built-in transforms. */
|
|
163
|
+
addStreamMiddleware(middleware) {
|
|
164
|
+
this.guardDisposed();
|
|
165
|
+
this._streamMiddleware.push(middleware);
|
|
166
|
+
}
|
|
167
|
+
/** Apply built-in transforms (enrich→timeout→heartbeat) then custom middleware */
|
|
168
|
+
async *applyStreamPipeline(source, options, ac) {
|
|
169
|
+
let stream = this.enrichStream(source, options);
|
|
170
|
+
stream = this.activityTimeoutStream(stream, options?.activityTimeoutMs, ac);
|
|
171
|
+
stream = this.heartbeatStream(stream);
|
|
172
|
+
if (this._streamMiddleware.length > 0) {
|
|
173
|
+
const ctx = {
|
|
174
|
+
model: options.model,
|
|
175
|
+
backend: this.backendName,
|
|
176
|
+
abortController: ac,
|
|
177
|
+
config: Object.freeze({ ...this.config })
|
|
178
|
+
};
|
|
179
|
+
for (const mw of this._streamMiddleware) {
|
|
180
|
+
stream = mw(stream, ctx);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
yield* stream;
|
|
184
|
+
}
|
|
185
|
+
abort() {
|
|
186
|
+
if (this.abortController) {
|
|
187
|
+
this.abortController.abort();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Default interrupt — falls back to abort(). Backends may override with graceful shutdown. */
|
|
191
|
+
async interrupt() {
|
|
192
|
+
this.abort();
|
|
193
|
+
}
|
|
194
|
+
getState() {
|
|
195
|
+
return this.state;
|
|
196
|
+
}
|
|
197
|
+
getConfig() {
|
|
198
|
+
return this.config;
|
|
199
|
+
}
|
|
200
|
+
/** Mark agent as disposed. Override to add cleanup. */
|
|
201
|
+
dispose() {
|
|
202
|
+
this._cleanupExternalSignal?.();
|
|
203
|
+
this._cleanupExternalSignal = null;
|
|
204
|
+
this.abort();
|
|
205
|
+
this.state = "disposed";
|
|
206
|
+
}
|
|
207
|
+
// ─── Retry Logic ─────────────────────────────────────────────
|
|
208
|
+
/** Check if an error should be retried given the retry configuration. */
|
|
209
|
+
isRetryableError(error, retry) {
|
|
210
|
+
if (error instanceof AbortError || error instanceof ReentrancyError || error instanceof DisposedError) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (AgentSDKError.is(error)) {
|
|
214
|
+
if (retry.retryableErrors && retry.retryableErrors.length > 0 && error.code) {
|
|
215
|
+
return retry.retryableErrors.includes(error.code);
|
|
216
|
+
}
|
|
217
|
+
if (error.retryable) return true;
|
|
218
|
+
if (error.code) return isRecoverableErrorCode(error.code);
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
/** Execute a function with retry logic per RetryConfig. */
|
|
223
|
+
async withRetry(fn, options) {
|
|
224
|
+
const retry = options?.retry;
|
|
225
|
+
if (!retry || !retry.maxRetries || retry.maxRetries <= 0) {
|
|
226
|
+
return fn();
|
|
227
|
+
}
|
|
228
|
+
const maxRetries = retry.maxRetries;
|
|
229
|
+
const initialDelay = retry.initialDelayMs ?? 1e3;
|
|
230
|
+
const multiplier = retry.backoffMultiplier ?? 2;
|
|
231
|
+
let lastError;
|
|
232
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
233
|
+
try {
|
|
234
|
+
return await fn();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
lastError = err;
|
|
237
|
+
if (attempt >= maxRetries || !this.isRetryableError(err, retry)) {
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
const delay = initialDelay * Math.pow(multiplier, attempt);
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
242
|
+
if (options?.signal?.aborted || this.abortController?.signal.aborted) {
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw lastError;
|
|
248
|
+
}
|
|
249
|
+
/** Execute a stream factory with pre-stream retry: retries until first event, then committed. */
|
|
250
|
+
async *streamWithRetry(factory, options) {
|
|
251
|
+
const retry = options?.retry;
|
|
252
|
+
if (!retry || !retry.maxRetries || retry.maxRetries <= 0) {
|
|
253
|
+
yield* factory();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const maxRetries = retry.maxRetries;
|
|
257
|
+
const initialDelay = retry.initialDelayMs ?? 1e3;
|
|
258
|
+
const multiplier = retry.backoffMultiplier ?? 2;
|
|
259
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
260
|
+
try {
|
|
261
|
+
const stream = factory();
|
|
262
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
263
|
+
const first = await iterator.next();
|
|
264
|
+
if (first.done) return;
|
|
265
|
+
yield first.value;
|
|
266
|
+
while (true) {
|
|
267
|
+
const next = await iterator.next();
|
|
268
|
+
if (next.done) break;
|
|
269
|
+
yield next.value;
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (attempt >= maxRetries || !this.isRetryableError(err, retry)) {
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
const delay = initialDelay * Math.pow(multiplier, attempt);
|
|
277
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
278
|
+
if (options?.signal?.aborted || this.abortController?.signal.aborted) {
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ─── CallOptions Resolution ──────────────────────────────────
|
|
285
|
+
/** Resolve tools to use for this call (per-call override > config default) */
|
|
286
|
+
resolveTools(options) {
|
|
287
|
+
return options?.tools ?? this.config.tools ?? [];
|
|
288
|
+
}
|
|
289
|
+
// ─── Usage Enrichment ───────────────────────────────────────────
|
|
290
|
+
/** Enrich result usage with model/backend and fire onUsage callback */
|
|
291
|
+
enrichAndNotifyUsage(result, options) {
|
|
292
|
+
if (result.usage) {
|
|
293
|
+
result.usage = {
|
|
294
|
+
...result.usage,
|
|
295
|
+
model: options.model,
|
|
296
|
+
backend: this.backendName
|
|
297
|
+
};
|
|
298
|
+
this.callOnUsage(result.usage);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Wrap a stream to enrich usage_update events and fire onUsage callback */
|
|
302
|
+
async *enrichStream(source, options) {
|
|
303
|
+
const model = options.model;
|
|
304
|
+
for await (const event of source) {
|
|
305
|
+
if (event.type === "usage_update") {
|
|
306
|
+
const usage = {
|
|
307
|
+
promptTokens: event.promptTokens,
|
|
308
|
+
completionTokens: event.completionTokens,
|
|
309
|
+
model,
|
|
310
|
+
backend: this.backendName
|
|
311
|
+
};
|
|
312
|
+
this.callOnUsage(usage);
|
|
313
|
+
yield { type: "usage_update", ...usage };
|
|
314
|
+
} else {
|
|
315
|
+
yield event;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Fire onUsage callback (fire-and-forget: errors logged, not propagated) */
|
|
320
|
+
callOnUsage(usage) {
|
|
321
|
+
if (!this.config.onUsage) return;
|
|
322
|
+
try {
|
|
323
|
+
this.config.onUsage(usage);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
console.warn(
|
|
326
|
+
"[agent-sdk] onUsage callback error:",
|
|
327
|
+
e instanceof Error ? e.message : String(e)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// ─── Heartbeat ───────────────────────────────────────────────
|
|
332
|
+
/** Wrap a stream to emit heartbeat events at configured intervals.
|
|
333
|
+
* When heartbeatInterval is not set, passes through directly. */
|
|
334
|
+
async *heartbeatStream(source) {
|
|
335
|
+
const interval = this.config.heartbeatInterval;
|
|
336
|
+
if (!interval || interval <= 0) {
|
|
337
|
+
yield* source;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
341
|
+
let pendingEvent = null;
|
|
342
|
+
let heartbeatResolve = null;
|
|
343
|
+
const timer = setInterval(() => {
|
|
344
|
+
if (heartbeatResolve) {
|
|
345
|
+
const resolve = heartbeatResolve;
|
|
346
|
+
heartbeatResolve = null;
|
|
347
|
+
resolve();
|
|
348
|
+
}
|
|
349
|
+
}, interval);
|
|
350
|
+
try {
|
|
351
|
+
while (true) {
|
|
352
|
+
if (!pendingEvent) {
|
|
353
|
+
pendingEvent = iterator.next();
|
|
354
|
+
}
|
|
355
|
+
const heartbeatPromise = new Promise((resolve) => {
|
|
356
|
+
heartbeatResolve = resolve;
|
|
357
|
+
});
|
|
358
|
+
const eventDone = pendingEvent.then(
|
|
359
|
+
(r) => ({ kind: "event", result: r })
|
|
360
|
+
);
|
|
361
|
+
const heartbeatDone = heartbeatPromise.then(
|
|
362
|
+
() => ({ kind: "heartbeat" })
|
|
363
|
+
);
|
|
364
|
+
const winner = await Promise.race([eventDone, heartbeatDone]);
|
|
365
|
+
if (winner.kind === "heartbeat") {
|
|
366
|
+
yield { type: "heartbeat" };
|
|
367
|
+
} else {
|
|
368
|
+
pendingEvent = null;
|
|
369
|
+
heartbeatResolve = null;
|
|
370
|
+
if (winner.result.done) break;
|
|
371
|
+
yield winner.result.value;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
clearInterval(timer);
|
|
376
|
+
heartbeatResolve = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// ─── Activity Timeout ────────────────────────────────────────
|
|
380
|
+
/** Wrap a stream to abort on inactivity. Resets timer on every event.
|
|
381
|
+
* When timeoutMs is not set, passes through directly. */
|
|
382
|
+
async *activityTimeoutStream(source, timeoutMs, ac) {
|
|
383
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
384
|
+
yield* source;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
388
|
+
let timerId;
|
|
389
|
+
try {
|
|
390
|
+
while (true) {
|
|
391
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
392
|
+
timerId = setTimeout(() => reject(new ActivityTimeoutError(timeoutMs)), timeoutMs);
|
|
393
|
+
});
|
|
394
|
+
const result = await Promise.race([iterator.next(), timeoutPromise]);
|
|
395
|
+
clearTimeout(timerId);
|
|
396
|
+
if (result.done) break;
|
|
397
|
+
yield result.value;
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (err instanceof ActivityTimeoutError) {
|
|
401
|
+
ac.abort(err);
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
} finally {
|
|
405
|
+
clearTimeout(timerId);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ─── Guards ───────────────────────────────────────────────────
|
|
409
|
+
guardReentrancy() {
|
|
410
|
+
if (this.state === "running" || this.state === "streaming") {
|
|
411
|
+
throw new ReentrancyError();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
guardDisposed() {
|
|
415
|
+
if (this.state === "disposed") {
|
|
416
|
+
throw new DisposedError("Agent");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** Throw AbortError if signal is already aborted */
|
|
420
|
+
checkAbort(signal) {
|
|
421
|
+
if (signal.aborted) {
|
|
422
|
+
throw new AbortError();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// ─── Internal Helpers ─────────────────────────────────────────
|
|
426
|
+
/** Clean up after a run completes (success, error, or abort). */
|
|
427
|
+
cleanupRun() {
|
|
428
|
+
this._cleanupExternalSignal?.();
|
|
429
|
+
this._cleanupExternalSignal = null;
|
|
430
|
+
this.state = "idle";
|
|
431
|
+
this.abortController = null;
|
|
432
|
+
}
|
|
433
|
+
createAbortController(externalSignal) {
|
|
434
|
+
const ac = new AbortController();
|
|
435
|
+
this.abortController = ac;
|
|
436
|
+
this._cleanupExternalSignal = null;
|
|
437
|
+
if (externalSignal) {
|
|
438
|
+
if (externalSignal.aborted) {
|
|
439
|
+
ac.abort();
|
|
440
|
+
} else {
|
|
441
|
+
const listener = () => ac.abort();
|
|
442
|
+
externalSignal.addEventListener("abort", listener, { once: true });
|
|
443
|
+
this._cleanupExternalSignal = () => externalSignal.removeEventListener("abort", listener);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return ac;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/backends/mock-llm.ts
|
|
451
|
+
function extractPrompt(messages) {
|
|
452
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
453
|
+
const msg = messages[i];
|
|
454
|
+
if (msg.role === "user") {
|
|
455
|
+
return typeof msg.content === "string" ? msg.content : msg.content.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return "";
|
|
459
|
+
}
|
|
460
|
+
function resolveResponse(mode, messages, callIndex) {
|
|
461
|
+
switch (mode.type) {
|
|
462
|
+
case "echo":
|
|
463
|
+
return extractPrompt(messages);
|
|
464
|
+
case "static":
|
|
465
|
+
return mode.response;
|
|
466
|
+
case "scripted": {
|
|
467
|
+
if (mode.loop) {
|
|
468
|
+
return mode.responses[callIndex % mode.responses.length];
|
|
469
|
+
}
|
|
470
|
+
if (callIndex < mode.responses.length) {
|
|
471
|
+
return mode.responses[callIndex];
|
|
472
|
+
}
|
|
473
|
+
return mode.responses[mode.responses.length - 1];
|
|
474
|
+
}
|
|
475
|
+
case "error":
|
|
476
|
+
throw new AgentSDKError(mode.error, {
|
|
477
|
+
code: mode.code ?? "backend_error",
|
|
478
|
+
retryable: mode.recoverable ?? false
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function applyLatency(latency, signal) {
|
|
483
|
+
if (!latency) return;
|
|
484
|
+
const ms = latency.type === "fixed" ? latency.ms : latency.minMs + Math.random() * (latency.maxMs - latency.minMs);
|
|
485
|
+
if (ms <= 0) return;
|
|
486
|
+
await new Promise((resolve, reject) => {
|
|
487
|
+
const timer = setTimeout(resolve, ms);
|
|
488
|
+
const onAbort = () => {
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
reject(new Error("aborted"));
|
|
491
|
+
};
|
|
492
|
+
if (signal.aborted) {
|
|
493
|
+
clearTimeout(timer);
|
|
494
|
+
reject(new Error("aborted"));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function chunkText(text, streaming) {
|
|
501
|
+
if (streaming?.chunkSize && streaming.chunkSize > 0) {
|
|
502
|
+
const chunks = [];
|
|
503
|
+
for (let i = 0; i < text.length; i += streaming.chunkSize) {
|
|
504
|
+
chunks.push(text.slice(i, i + streaming.chunkSize));
|
|
505
|
+
}
|
|
506
|
+
return chunks;
|
|
507
|
+
}
|
|
508
|
+
return text.split(/(\s+)/).filter(Boolean);
|
|
509
|
+
}
|
|
510
|
+
async function chunkDelay(streaming, signal) {
|
|
511
|
+
const ms = streaming?.chunkDelayMs;
|
|
512
|
+
if (!ms || ms <= 0) return;
|
|
513
|
+
await new Promise((resolve, reject) => {
|
|
514
|
+
const timer = setTimeout(resolve, ms);
|
|
515
|
+
const onAbort = () => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
reject(new Error("aborted"));
|
|
518
|
+
};
|
|
519
|
+
if (signal.aborted) {
|
|
520
|
+
clearTimeout(timer);
|
|
521
|
+
reject(new Error("aborted"));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
var MockLLMAgent = class extends BaseAgent {
|
|
528
|
+
backendName = "mock-llm";
|
|
529
|
+
mode;
|
|
530
|
+
latency;
|
|
531
|
+
streaming;
|
|
532
|
+
finishReason;
|
|
533
|
+
permissions;
|
|
534
|
+
toolCallConfigs;
|
|
535
|
+
configuredStructuredOutput;
|
|
536
|
+
callIndex = 0;
|
|
537
|
+
constructor(config, options) {
|
|
538
|
+
super(config);
|
|
539
|
+
this.mode = options.mode ?? { type: "echo" };
|
|
540
|
+
this.latency = options.latency;
|
|
541
|
+
this.streaming = options.streaming;
|
|
542
|
+
this.finishReason = options.finishReason ?? "stop";
|
|
543
|
+
this.permissions = options.permissions;
|
|
544
|
+
this.toolCallConfigs = options.toolCalls ?? [];
|
|
545
|
+
this.configuredStructuredOutput = options.structuredOutput;
|
|
546
|
+
}
|
|
547
|
+
async executeRun(messages, _options, signal) {
|
|
548
|
+
this.checkAbort(signal);
|
|
549
|
+
await applyLatency(this.latency, signal);
|
|
550
|
+
this.checkAbort(signal);
|
|
551
|
+
const idx = this.callIndex++;
|
|
552
|
+
const output = resolveResponse(this.mode, messages, idx);
|
|
553
|
+
const toolCalls = this.toolCallConfigs.map((tc) => ({
|
|
554
|
+
toolName: tc.toolName,
|
|
555
|
+
args: tc.args ?? {},
|
|
556
|
+
result: tc.result ?? null,
|
|
557
|
+
approved: true
|
|
558
|
+
}));
|
|
559
|
+
return {
|
|
560
|
+
output,
|
|
561
|
+
structuredOutput: void 0,
|
|
562
|
+
toolCalls,
|
|
563
|
+
messages: [
|
|
564
|
+
...messages,
|
|
565
|
+
{ role: "assistant", content: output }
|
|
566
|
+
],
|
|
567
|
+
usage: { promptTokens: 10, completionTokens: output.length }
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async executeRunStructured(messages, _schema, _options, signal) {
|
|
571
|
+
this.checkAbort(signal);
|
|
572
|
+
await applyLatency(this.latency, signal);
|
|
573
|
+
this.checkAbort(signal);
|
|
574
|
+
const idx = this.callIndex++;
|
|
575
|
+
const output = resolveResponse(this.mode, messages, idx);
|
|
576
|
+
let parsed;
|
|
577
|
+
if (this.configuredStructuredOutput !== void 0) {
|
|
578
|
+
parsed = this.configuredStructuredOutput;
|
|
579
|
+
} else {
|
|
580
|
+
try {
|
|
581
|
+
parsed = JSON.parse(output);
|
|
582
|
+
} catch {
|
|
583
|
+
parsed = output;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
output,
|
|
588
|
+
structuredOutput: parsed,
|
|
589
|
+
toolCalls: [],
|
|
590
|
+
messages: [
|
|
591
|
+
...messages,
|
|
592
|
+
{ role: "assistant", content: output }
|
|
593
|
+
],
|
|
594
|
+
usage: { promptTokens: 10, completionTokens: output.length }
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
async *executeStream(messages, _options, signal) {
|
|
598
|
+
this.checkAbort(signal);
|
|
599
|
+
await applyLatency(this.latency, signal);
|
|
600
|
+
this.checkAbort(signal);
|
|
601
|
+
if (this.permissions) {
|
|
602
|
+
yield* this.simulatePermissions(signal);
|
|
603
|
+
}
|
|
604
|
+
if (this.toolCallConfigs.length > 0) {
|
|
605
|
+
yield* this.simulateToolCalls(signal);
|
|
606
|
+
}
|
|
607
|
+
const idx = this.callIndex++;
|
|
608
|
+
const output = resolveResponse(this.mode, messages, idx);
|
|
609
|
+
const chunks = chunkText(output, this.streaming);
|
|
610
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
611
|
+
this.checkAbort(signal);
|
|
612
|
+
if (i > 0) {
|
|
613
|
+
await chunkDelay(this.streaming, signal);
|
|
614
|
+
}
|
|
615
|
+
yield { type: "text_delta", text: chunks[i] };
|
|
616
|
+
}
|
|
617
|
+
yield {
|
|
618
|
+
type: "usage_update",
|
|
619
|
+
promptTokens: 10,
|
|
620
|
+
completionTokens: output.length
|
|
621
|
+
};
|
|
622
|
+
yield {
|
|
623
|
+
type: "done",
|
|
624
|
+
finalOutput: output,
|
|
625
|
+
finishReason: this.finishReason
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
async *simulateToolCalls(signal) {
|
|
629
|
+
for (let i = 0; i < this.toolCallConfigs.length; i++) {
|
|
630
|
+
this.checkAbort(signal);
|
|
631
|
+
const tc = this.toolCallConfigs[i];
|
|
632
|
+
const toolCallId = tc.toolCallId ?? `mock-tc-${i}`;
|
|
633
|
+
yield {
|
|
634
|
+
type: "tool_call_start",
|
|
635
|
+
toolCallId,
|
|
636
|
+
toolName: tc.toolName,
|
|
637
|
+
args: tc.args ?? {}
|
|
638
|
+
};
|
|
639
|
+
yield {
|
|
640
|
+
type: "tool_call_end",
|
|
641
|
+
toolCallId,
|
|
642
|
+
toolName: tc.toolName,
|
|
643
|
+
result: tc.result ?? null
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async *simulatePermissions(signal) {
|
|
648
|
+
const perms = this.permissions;
|
|
649
|
+
for (const toolName of perms.toolNames) {
|
|
650
|
+
this.checkAbort(signal);
|
|
651
|
+
const request = {
|
|
652
|
+
toolName,
|
|
653
|
+
toolArgs: {}
|
|
654
|
+
};
|
|
655
|
+
yield { type: "permission_request", request };
|
|
656
|
+
if (perms.denyTools?.includes(toolName)) {
|
|
657
|
+
yield {
|
|
658
|
+
type: "permission_response",
|
|
659
|
+
toolName,
|
|
660
|
+
decision: { allowed: false, reason: "Denied by mock configuration" }
|
|
661
|
+
};
|
|
662
|
+
} else if (perms.autoApprove) {
|
|
663
|
+
yield {
|
|
664
|
+
type: "permission_response",
|
|
665
|
+
toolName,
|
|
666
|
+
decision: { allowed: true, scope: "once" }
|
|
667
|
+
};
|
|
668
|
+
} else {
|
|
669
|
+
const supervisor = this.getConfig().supervisor;
|
|
670
|
+
if (supervisor?.onPermission) {
|
|
671
|
+
const decision = await supervisor.onPermission(request, signal);
|
|
672
|
+
yield { type: "permission_response", toolName, decision };
|
|
673
|
+
} else {
|
|
674
|
+
yield {
|
|
675
|
+
type: "permission_response",
|
|
676
|
+
toolName,
|
|
677
|
+
decision: { allowed: true, scope: "once" }
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
var MockLLMService = class {
|
|
685
|
+
name = "mock-llm";
|
|
686
|
+
options;
|
|
687
|
+
models;
|
|
688
|
+
constructor(options = {}) {
|
|
689
|
+
this.options = options;
|
|
690
|
+
this.models = (options.models ?? [
|
|
691
|
+
{ id: "mock-fast", name: "Mock Fast" },
|
|
692
|
+
{ id: "mock-quality", name: "Mock Quality" }
|
|
693
|
+
]).map((m) => ({
|
|
694
|
+
id: m.id,
|
|
695
|
+
name: m.name,
|
|
696
|
+
description: m.description
|
|
697
|
+
}));
|
|
698
|
+
}
|
|
699
|
+
createAgent(config) {
|
|
700
|
+
return new MockLLMAgent(config, this.options);
|
|
701
|
+
}
|
|
702
|
+
async listModels() {
|
|
703
|
+
return this.models;
|
|
704
|
+
}
|
|
705
|
+
async validate() {
|
|
706
|
+
return { valid: true, errors: [] };
|
|
707
|
+
}
|
|
708
|
+
async dispose() {
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
function createMockLLMService(options = {}) {
|
|
712
|
+
return new MockLLMService(options);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/testing/mock-agent-service.ts
|
|
716
|
+
var MockAgent = class {
|
|
717
|
+
sessionId = void 0;
|
|
718
|
+
_state = "idle";
|
|
719
|
+
_config;
|
|
720
|
+
_onRun;
|
|
721
|
+
_onStream;
|
|
722
|
+
constructor(config, options) {
|
|
723
|
+
this._config = config;
|
|
724
|
+
this._onRun = options?.onRun;
|
|
725
|
+
this._onStream = options?.onStream;
|
|
726
|
+
}
|
|
727
|
+
async run(prompt, options) {
|
|
728
|
+
if (this._onRun) return this._onRun(prompt, options);
|
|
729
|
+
return { output: "Mock response", structuredOutput: void 0, toolCalls: [], messages: [], usage: { promptTokens: 10, completionTokens: 5 } };
|
|
730
|
+
}
|
|
731
|
+
async runWithContext(messages, options) {
|
|
732
|
+
const lastMsg = messages[messages.length - 1];
|
|
733
|
+
const text = typeof lastMsg?.content === "string" ? lastMsg.content : "context";
|
|
734
|
+
return this.run(text, options);
|
|
735
|
+
}
|
|
736
|
+
async runStructured(prompt, _schema, options) {
|
|
737
|
+
const base = await this.run(prompt, options);
|
|
738
|
+
return { ...base, structuredOutput: void 0 };
|
|
739
|
+
}
|
|
740
|
+
async *stream(prompt, options) {
|
|
741
|
+
if (this._onStream) {
|
|
742
|
+
yield* this._onStream(prompt, options);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
yield { type: "text_delta", text: "Mock " };
|
|
746
|
+
yield { type: "text_delta", text: "response" };
|
|
747
|
+
yield { type: "done", finalOutput: "Mock response" };
|
|
748
|
+
}
|
|
749
|
+
async *streamWithContext(messages, options) {
|
|
750
|
+
const lastMsg = messages[messages.length - 1];
|
|
751
|
+
const text = typeof lastMsg?.content === "string" ? lastMsg.content : "context";
|
|
752
|
+
yield* this.stream(text, options);
|
|
753
|
+
}
|
|
754
|
+
abort() {
|
|
755
|
+
this._state = "idle";
|
|
756
|
+
}
|
|
757
|
+
async interrupt() {
|
|
758
|
+
this._state = "idle";
|
|
759
|
+
}
|
|
760
|
+
getState() {
|
|
761
|
+
return this._state;
|
|
762
|
+
}
|
|
763
|
+
getConfig() {
|
|
764
|
+
return this._config;
|
|
765
|
+
}
|
|
766
|
+
dispose() {
|
|
767
|
+
this._state = "disposed";
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
function createMockAgentService(options = {}) {
|
|
771
|
+
if (options.mockLLMBackend) {
|
|
772
|
+
const llmService = createMockLLMService(options.mockLLMBackend);
|
|
773
|
+
return {
|
|
774
|
+
name: options.name ?? "mock",
|
|
775
|
+
createAgent: (config) => llmService.createAgent(config),
|
|
776
|
+
listModels: () => llmService.listModels(),
|
|
777
|
+
validate: () => llmService.validate(),
|
|
778
|
+
dispose: () => llmService.dispose()
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const name = options.name ?? "mock";
|
|
782
|
+
return {
|
|
783
|
+
name,
|
|
784
|
+
createAgent(config) {
|
|
785
|
+
return new MockAgent(config, options);
|
|
786
|
+
},
|
|
787
|
+
async listModels() {
|
|
788
|
+
return options.models ?? [
|
|
789
|
+
{ id: "mock-model-1", name: "Mock Model 1" },
|
|
790
|
+
{ id: "mock-model-2", name: "Mock Model 2" }
|
|
791
|
+
];
|
|
792
|
+
},
|
|
793
|
+
async validate() {
|
|
794
|
+
return options.validationResult ?? { valid: true, errors: [] };
|
|
795
|
+
},
|
|
796
|
+
async dispose() {
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/chat/types.ts
|
|
802
|
+
function createChatId() {
|
|
803
|
+
return crypto.randomUUID();
|
|
804
|
+
}
|
|
805
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
806
|
+
function toChatId(value) {
|
|
807
|
+
if (!UUID_RE.test(value)) {
|
|
808
|
+
throw new TypeError(`Invalid ChatId: "${value}" is not a valid UUID`);
|
|
809
|
+
}
|
|
810
|
+
return value;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/testing/mock-data.ts
|
|
814
|
+
function createMockSession(options = {}) {
|
|
815
|
+
const id = options.id ?? createChatId();
|
|
816
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
817
|
+
return {
|
|
818
|
+
id,
|
|
819
|
+
title: options.title ?? "Test Session",
|
|
820
|
+
messages: options.messages ?? [],
|
|
821
|
+
config: {
|
|
822
|
+
model: options.config?.model ?? "test-model",
|
|
823
|
+
backend: options.config?.backend ?? "test-backend",
|
|
824
|
+
systemPrompt: options.config?.systemPrompt ?? ""
|
|
825
|
+
},
|
|
826
|
+
metadata: {
|
|
827
|
+
messageCount: options.messages?.length ?? 0,
|
|
828
|
+
totalTokens: 0,
|
|
829
|
+
custom: options.metadata ?? {}
|
|
830
|
+
},
|
|
831
|
+
status: options.status ?? "active",
|
|
832
|
+
createdAt: now,
|
|
833
|
+
updatedAt: now
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function createMockMessage(options = {}) {
|
|
837
|
+
const id = createChatId();
|
|
838
|
+
const parts = options.parts ?? (options.text ? [{ type: "text", text: options.text, status: "complete" }] : [{ type: "text", text: "Test message", status: "complete" }]);
|
|
839
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
840
|
+
return {
|
|
841
|
+
id,
|
|
842
|
+
role: options.role ?? "user",
|
|
843
|
+
parts,
|
|
844
|
+
status: options.status ?? "complete",
|
|
845
|
+
createdAt: now,
|
|
846
|
+
metadata: options.metadata ?? {}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/testing/mock-runtime.ts
|
|
851
|
+
function createMockRuntime(options = {}) {
|
|
852
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
853
|
+
const tools = /* @__PURE__ */ new Map();
|
|
854
|
+
const sessionListeners = /* @__PURE__ */ new Set();
|
|
855
|
+
let currentBackend = options.defaultBackend ?? "mock";
|
|
856
|
+
let status = "idle";
|
|
857
|
+
for (const s of options.sessions ?? []) {
|
|
858
|
+
sessions.set(s.id, s);
|
|
859
|
+
}
|
|
860
|
+
function notifySessionChange() {
|
|
861
|
+
for (const cb of sessionListeners) {
|
|
862
|
+
try {
|
|
863
|
+
cb();
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const runtime = {
|
|
869
|
+
get status() {
|
|
870
|
+
return status;
|
|
871
|
+
},
|
|
872
|
+
get registeredTools() {
|
|
873
|
+
return tools;
|
|
874
|
+
},
|
|
875
|
+
async createSession(opts) {
|
|
876
|
+
const session = createMockSession({
|
|
877
|
+
title: opts.title,
|
|
878
|
+
config: {
|
|
879
|
+
model: opts.config?.model ?? "mock-model",
|
|
880
|
+
backend: opts.config?.backend ?? currentBackend,
|
|
881
|
+
systemPrompt: opts.config?.systemPrompt
|
|
882
|
+
},
|
|
883
|
+
metadata: opts.custom
|
|
884
|
+
});
|
|
885
|
+
sessions.set(session.id, session);
|
|
886
|
+
notifySessionChange();
|
|
887
|
+
return session;
|
|
888
|
+
},
|
|
889
|
+
async getSession(id) {
|
|
890
|
+
return sessions.get(toChatId(String(id))) ?? null;
|
|
891
|
+
},
|
|
892
|
+
async listSessions(_opts) {
|
|
893
|
+
return [...sessions.values()];
|
|
894
|
+
},
|
|
895
|
+
async deleteSession(id) {
|
|
896
|
+
sessions.delete(toChatId(String(id)));
|
|
897
|
+
notifySessionChange();
|
|
898
|
+
},
|
|
899
|
+
send(sessionId, message, sendOpts) {
|
|
900
|
+
if (options.onSend) return options.onSend(sessionId, message, sendOpts);
|
|
901
|
+
async function* defaultStream() {
|
|
902
|
+
const msgId = createChatId();
|
|
903
|
+
yield { type: "message:start", messageId: msgId };
|
|
904
|
+
yield { type: "message:delta", text: "Mock reply" };
|
|
905
|
+
yield { type: "message:complete", messageId: msgId };
|
|
906
|
+
yield { type: "done", finalOutput: "Mock reply" };
|
|
907
|
+
const session = sessions.get(toChatId(String(sessionId)));
|
|
908
|
+
if (session) {
|
|
909
|
+
session.messages.push(createMockMessage({ role: "assistant", text: "Mock reply" }));
|
|
910
|
+
session.metadata.messageCount = session.messages.length;
|
|
911
|
+
}
|
|
912
|
+
notifySessionChange();
|
|
913
|
+
}
|
|
914
|
+
return defaultStream();
|
|
915
|
+
},
|
|
916
|
+
abort() {
|
|
917
|
+
},
|
|
918
|
+
async listModels() {
|
|
919
|
+
return options.models ?? [{ id: "mock-model", name: "Mock Model" }];
|
|
920
|
+
},
|
|
921
|
+
async listBackends() {
|
|
922
|
+
return [{ name: currentBackend }];
|
|
923
|
+
},
|
|
924
|
+
onSessionChange(callback) {
|
|
925
|
+
sessionListeners.add(callback);
|
|
926
|
+
return () => {
|
|
927
|
+
sessionListeners.delete(callback);
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
registerTool(tool) {
|
|
931
|
+
tools.set(tool.name, tool);
|
|
932
|
+
},
|
|
933
|
+
removeTool(name) {
|
|
934
|
+
tools.delete(name);
|
|
935
|
+
},
|
|
936
|
+
use(mw) {
|
|
937
|
+
},
|
|
938
|
+
removeMiddleware(mw) {
|
|
939
|
+
},
|
|
940
|
+
async getContextStats(_sessionId) {
|
|
941
|
+
return null;
|
|
942
|
+
},
|
|
943
|
+
async dispose() {
|
|
944
|
+
status = "disposed";
|
|
945
|
+
sessions.clear();
|
|
946
|
+
tools.clear();
|
|
947
|
+
sessionListeners.clear();
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
return runtime;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/testing/mock-chat-client.ts
|
|
954
|
+
function createMockChatClient(options = {}) {
|
|
955
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
956
|
+
const providers = /* @__PURE__ */ new Map();
|
|
957
|
+
const sessionListeners = /* @__PURE__ */ new Set();
|
|
958
|
+
const selectionListeners = /* @__PURE__ */ new Set();
|
|
959
|
+
let activeSessionId = null;
|
|
960
|
+
let selectedProviderId = null;
|
|
961
|
+
let status = "idle";
|
|
962
|
+
for (const s of options.sessions ?? []) {
|
|
963
|
+
sessions.set(s.id, s);
|
|
964
|
+
}
|
|
965
|
+
for (const p of options.providers ?? []) {
|
|
966
|
+
providers.set(p.id, p);
|
|
967
|
+
}
|
|
968
|
+
function notifySessionChange() {
|
|
969
|
+
for (const cb of sessionListeners) {
|
|
970
|
+
try {
|
|
971
|
+
cb();
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const client = {
|
|
977
|
+
get status() {
|
|
978
|
+
return status;
|
|
979
|
+
},
|
|
980
|
+
get activeSessionId() {
|
|
981
|
+
return activeSessionId;
|
|
982
|
+
},
|
|
983
|
+
async createSession(opts) {
|
|
984
|
+
const session = createMockSession({
|
|
985
|
+
title: opts.title,
|
|
986
|
+
config: {
|
|
987
|
+
model: opts.config?.model ?? "mock-model",
|
|
988
|
+
backend: opts.config?.backend ?? "mock",
|
|
989
|
+
systemPrompt: opts.config?.systemPrompt
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
sessions.set(session.id, session);
|
|
993
|
+
activeSessionId = session.id;
|
|
994
|
+
notifySessionChange();
|
|
995
|
+
return session;
|
|
996
|
+
},
|
|
997
|
+
async getSession(id) {
|
|
998
|
+
return sessions.get(toChatId(String(id))) ?? null;
|
|
999
|
+
},
|
|
1000
|
+
async listSessions(_opts) {
|
|
1001
|
+
return [...sessions.values()];
|
|
1002
|
+
},
|
|
1003
|
+
async deleteSession(id) {
|
|
1004
|
+
sessions.delete(toChatId(String(id)));
|
|
1005
|
+
if (activeSessionId === String(id)) activeSessionId = null;
|
|
1006
|
+
notifySessionChange();
|
|
1007
|
+
},
|
|
1008
|
+
async switchSession(id) {
|
|
1009
|
+
const session = sessions.get(toChatId(String(id)));
|
|
1010
|
+
if (!session) throw new Error(`Session ${id} not found`);
|
|
1011
|
+
activeSessionId = session.id;
|
|
1012
|
+
return session;
|
|
1013
|
+
},
|
|
1014
|
+
send(sessionId, message, sendOpts) {
|
|
1015
|
+
if (options.onSend) return options.onSend(sessionId, message, sendOpts);
|
|
1016
|
+
async function* defaultStream() {
|
|
1017
|
+
const msgId = createChatId();
|
|
1018
|
+
yield { type: "message:start", messageId: msgId };
|
|
1019
|
+
yield { type: "message:delta", text: "Mock reply" };
|
|
1020
|
+
yield { type: "message:complete", messageId: msgId };
|
|
1021
|
+
yield { type: "done", finalOutput: "Mock reply" };
|
|
1022
|
+
const session = sessions.get(toChatId(String(sessionId)));
|
|
1023
|
+
if (session) {
|
|
1024
|
+
session.messages.push(createMockMessage({ role: "assistant", text: "Mock reply" }));
|
|
1025
|
+
session.metadata.messageCount = session.messages.length;
|
|
1026
|
+
}
|
|
1027
|
+
notifySessionChange();
|
|
1028
|
+
}
|
|
1029
|
+
return defaultStream();
|
|
1030
|
+
},
|
|
1031
|
+
abort() {
|
|
1032
|
+
},
|
|
1033
|
+
// ── Provider Selection ──
|
|
1034
|
+
get selectedProviderId() {
|
|
1035
|
+
return selectedProviderId;
|
|
1036
|
+
},
|
|
1037
|
+
selectProvider(providerId) {
|
|
1038
|
+
selectedProviderId = providerId;
|
|
1039
|
+
for (const cb of selectionListeners) {
|
|
1040
|
+
try {
|
|
1041
|
+
cb(providerId);
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
onSelectionChange(callback) {
|
|
1047
|
+
selectionListeners.add(callback);
|
|
1048
|
+
return () => {
|
|
1049
|
+
selectionListeners.delete(callback);
|
|
1050
|
+
};
|
|
1051
|
+
},
|
|
1052
|
+
async listModels() {
|
|
1053
|
+
return options.models ?? [{ id: "mock-model", name: "Mock Model" }];
|
|
1054
|
+
},
|
|
1055
|
+
async listBackends() {
|
|
1056
|
+
return [{ name: "mock" }];
|
|
1057
|
+
},
|
|
1058
|
+
onSessionChange(callback) {
|
|
1059
|
+
sessionListeners.add(callback);
|
|
1060
|
+
return () => {
|
|
1061
|
+
sessionListeners.delete(callback);
|
|
1062
|
+
};
|
|
1063
|
+
},
|
|
1064
|
+
async getContextStats(_sessionId) {
|
|
1065
|
+
return null;
|
|
1066
|
+
},
|
|
1067
|
+
// ── Provider CRUD ──
|
|
1068
|
+
async listProviders() {
|
|
1069
|
+
return [...providers.values()];
|
|
1070
|
+
},
|
|
1071
|
+
async createProvider(config) {
|
|
1072
|
+
const provider = {
|
|
1073
|
+
...config,
|
|
1074
|
+
id: createChatId(),
|
|
1075
|
+
createdAt: Date.now()
|
|
1076
|
+
};
|
|
1077
|
+
providers.set(provider.id, provider);
|
|
1078
|
+
return provider;
|
|
1079
|
+
},
|
|
1080
|
+
async updateProvider(id, changes) {
|
|
1081
|
+
const existing = providers.get(id);
|
|
1082
|
+
if (!existing) throw new Error(`Provider ${id} not found`);
|
|
1083
|
+
providers.set(id, { ...existing, ...changes });
|
|
1084
|
+
},
|
|
1085
|
+
async deleteProvider(id) {
|
|
1086
|
+
providers.delete(id);
|
|
1087
|
+
},
|
|
1088
|
+
async dispose() {
|
|
1089
|
+
status = "disposed";
|
|
1090
|
+
sessions.clear();
|
|
1091
|
+
providers.clear();
|
|
1092
|
+
sessionListeners.clear();
|
|
1093
|
+
selectionListeners.clear();
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
return client;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export { createMockAgentService, createMockChatClient, createMockMessage, createMockRuntime, createMockSession };
|
|
1100
|
+
//# sourceMappingURL=testing.js.map
|
|
1101
|
+
//# sourceMappingURL=testing.js.map
|