@standardagents/builder 0.11.12 → 0.12.1

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/test.d.ts ADDED
@@ -0,0 +1,256 @@
1
+ import { ProviderFactoryConfig, LLMProviderInterface, ProviderRequest, ProviderResponse, ProviderStreamChunk } from '@standardagents/spec';
2
+ import { l as ToolCall } from './index-DkbUJ4MM.js';
3
+ import '@cloudflare/workers-types';
4
+ import 'zod';
5
+ import './types-pLkJx8vg.js';
6
+
7
+ /**
8
+ * TestScript - Fluent builder for defining deterministic LLM response sequences.
9
+ *
10
+ * Use this to script exact responses for tests, enabling deterministic testing
11
+ * of agent behavior without calling real LLM APIs.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const script = TestScript.create()
16
+ * .addTextResponse("Hello! How can I help?")
17
+ * .addToolCallResponse([
18
+ * { name: "search", arguments: { query: "test" } }
19
+ * ])
20
+ * .addTextResponse("Based on the search, here's what I found...");
21
+ * ```
22
+ */
23
+
24
+ /**
25
+ * A single scripted response from the LLM
26
+ */
27
+ interface ScriptedResponse {
28
+ /** Text content to return (null for pure tool calls) */
29
+ content?: string | null;
30
+ /** Tool calls to return (OpenAI format) */
31
+ toolCalls?: ToolCall[];
32
+ /** Reasoning content for o1-style models */
33
+ reasoningContent?: string | null;
34
+ /** Reasoning details for OpenRouter structured reasoning */
35
+ reasoningDetails?: any[];
36
+ /** Finish reason */
37
+ finishReason?: "stop" | "tool_calls" | "length" | "content_filter";
38
+ /** Token usage to report */
39
+ usage?: {
40
+ promptTokens?: number;
41
+ completionTokens?: number;
42
+ reasoningTokens?: number;
43
+ };
44
+ /** Simulate an error instead of a successful response */
45
+ error?: {
46
+ message: string;
47
+ code?: string;
48
+ status?: number;
49
+ };
50
+ /** Optional: Validate the input matches expectations */
51
+ expectInput?: InputExpectation;
52
+ /** Optional: Delay before responding (ms) - useful for streaming tests */
53
+ delayMs?: number;
54
+ }
55
+ /**
56
+ * Input validation expectations
57
+ */
58
+ interface InputExpectation {
59
+ /** Check that a message contains this text (string or regex) */
60
+ containsMessage?: string | RegExp;
61
+ /** Check that a tool result for this tool exists */
62
+ containsToolResult?: {
63
+ toolName: string;
64
+ resultContains?: string;
65
+ };
66
+ /** Exact message count expected */
67
+ messageCount?: number;
68
+ /** Check that the system prompt contains this text */
69
+ systemPromptContains?: string | RegExp;
70
+ }
71
+ /**
72
+ * Configuration for streaming simulation
73
+ */
74
+ interface StreamingConfig {
75
+ /** Characters per chunk */
76
+ chunkSize: number;
77
+ /** Delay between chunks in milliseconds */
78
+ chunkDelayMs: number;
79
+ }
80
+ /**
81
+ * Shorthand for creating tool calls
82
+ */
83
+ interface ToolCallSpec {
84
+ name: string;
85
+ arguments: Record<string, any>;
86
+ id?: string;
87
+ }
88
+ /**
89
+ * Builder class for creating deterministic test scripts.
90
+ *
91
+ * Each call to addResponse/addTextResponse/etc adds a response to the sequence.
92
+ * When the TestProvider is used, it returns these responses in order.
93
+ */
94
+ declare class TestScript {
95
+ private responses;
96
+ private streamingConfig?;
97
+ /**
98
+ * Create a new TestScript instance
99
+ */
100
+ static create(): TestScript;
101
+ /**
102
+ * Add a response to the sequence
103
+ */
104
+ addResponse(response: ScriptedResponse): this;
105
+ /**
106
+ * Add a simple text response (stops after response)
107
+ */
108
+ addTextResponse(content: string, options?: Partial<ScriptedResponse>): this;
109
+ /**
110
+ * Add a response with tool calls
111
+ */
112
+ addToolCallResponse(toolCalls: ToolCallSpec[], content?: string, options?: Partial<ScriptedResponse>): this;
113
+ /**
114
+ * Add an error response (simulates provider error)
115
+ */
116
+ addErrorResponse(message: string, code?: string, status?: number, options?: Partial<ScriptedResponse>): this;
117
+ /**
118
+ * Add a response with reasoning content (for o1-style models)
119
+ */
120
+ addReasoningResponse(content: string, reasoningContent: string, options?: Partial<ScriptedResponse>): this;
121
+ /**
122
+ * Enable streaming simulation for all responses
123
+ */
124
+ withStreamingSimulation(config: StreamingConfig): this;
125
+ /**
126
+ * Get all scripted responses
127
+ */
128
+ getResponses(): ScriptedResponse[];
129
+ /**
130
+ * Get streaming configuration
131
+ */
132
+ getStreamingConfig(): StreamingConfig | undefined;
133
+ /**
134
+ * Get total number of responses
135
+ */
136
+ get length(): number;
137
+ /**
138
+ * Check if script is empty
139
+ */
140
+ isEmpty(): boolean;
141
+ /**
142
+ * Create a clone of this script
143
+ */
144
+ clone(): TestScript;
145
+ }
146
+
147
+ /**
148
+ * TestProvider - Deterministic LLM provider for testing.
149
+ *
150
+ * Returns scripted responses in sequence, enabling deterministic testing
151
+ * of agent execution without real API calls.
152
+ *
153
+ * Implements the new LLMProviderInterface with generate() and stream() methods.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const script = TestScript.create()
158
+ * .addTextResponse("Hello!")
159
+ * .addToolCallResponse([{ name: "search", arguments: { q: "test" } }])
160
+ * .addTextResponse("Found results.");
161
+ *
162
+ * const provider = createTestProvider(script);
163
+ * ```
164
+ */
165
+
166
+ /**
167
+ * Extended provider config for TestProvider
168
+ */
169
+ interface TestProviderConfig extends ProviderFactoryConfig {
170
+ /** The test script defining response sequence. If not provided, uses a default that echoes responses. */
171
+ script?: TestScript;
172
+ /** Whether to validate inputs match expectations (default: false) */
173
+ validateInputs?: boolean;
174
+ /** Log all requests for debugging (default: false) */
175
+ debugLog?: boolean;
176
+ }
177
+ /**
178
+ * Deterministic test provider that returns scripted LLM responses.
179
+ *
180
+ * Key features:
181
+ * - Returns responses in sequence as defined by TestScript
182
+ * - Supports streaming simulation
183
+ * - Optional input validation
184
+ * - Request logging for assertions
185
+ * - Clear error when script is exhausted
186
+ */
187
+ declare class TestProvider implements LLMProviderInterface {
188
+ readonly name = "test";
189
+ readonly specificationVersion: "1";
190
+ private script;
191
+ private responseIndex;
192
+ private validateInputs;
193
+ private debugLog;
194
+ private requestLog;
195
+ private useDefaultResponse;
196
+ constructor(config: TestProviderConfig);
197
+ /**
198
+ * Test provider supports any model ID
199
+ */
200
+ supportsModel(_modelId: string): boolean;
201
+ /**
202
+ * Non-streaming generation - returns the next scripted response
203
+ */
204
+ generate(request: ProviderRequest): Promise<ProviderResponse>;
205
+ /**
206
+ * Streaming generation - yields chunks for the next scripted response
207
+ */
208
+ stream(request: ProviderRequest): Promise<AsyncIterable<ProviderStreamChunk>>;
209
+ /**
210
+ * Validate request matches expectations
211
+ */
212
+ private validateInput;
213
+ /**
214
+ * Simple token estimation (for mock usage stats)
215
+ */
216
+ private estimateTokens;
217
+ private sleep;
218
+ /**
219
+ * Get logged requests (for test assertions)
220
+ */
221
+ getRequestLog(): ProviderRequest[];
222
+ /**
223
+ * Get the last request made
224
+ */
225
+ getLastRequest(): ProviderRequest | undefined;
226
+ /**
227
+ * Reset provider state for reuse across tests
228
+ */
229
+ reset(): void;
230
+ /**
231
+ * Replace the script with a new one and reset
232
+ */
233
+ setScript(script: TestScript): void;
234
+ /**
235
+ * Check if all scripted responses were consumed
236
+ */
237
+ isScriptComplete(): boolean;
238
+ /**
239
+ * Get remaining unconsumed response count
240
+ */
241
+ remainingResponses(): number;
242
+ /**
243
+ * Get current response index
244
+ */
245
+ getCurrentIndex(): number;
246
+ }
247
+ /**
248
+ * Factory function to create a TestProvider
249
+ */
250
+ declare function createTestProvider(script?: TestScript): TestProvider;
251
+ /**
252
+ * ProviderFactory-compatible factory for test provider
253
+ */
254
+ declare const test: (config: ProviderFactoryConfig) => LLMProviderInterface;
255
+
256
+ export { type InputExpectation, type ScriptedResponse, TestProvider, type TestProviderConfig, TestScript, createTestProvider, test };
package/dist/test.js ADDED
@@ -0,0 +1,391 @@
1
+ import { ProviderError } from '@standardagents/spec';
2
+
3
+ // src/agents/providers/TestProvider.ts
4
+
5
+ // src/agents/providers/TestScript.ts
6
+ var TestScript = class _TestScript {
7
+ responses = [];
8
+ streamingConfig;
9
+ /**
10
+ * Create a new TestScript instance
11
+ */
12
+ static create() {
13
+ return new _TestScript();
14
+ }
15
+ /**
16
+ * Add a response to the sequence
17
+ */
18
+ addResponse(response) {
19
+ this.responses.push(response);
20
+ return this;
21
+ }
22
+ /**
23
+ * Add a simple text response (stops after response)
24
+ */
25
+ addTextResponse(content, options) {
26
+ return this.addResponse({
27
+ content,
28
+ finishReason: "stop",
29
+ ...options
30
+ });
31
+ }
32
+ /**
33
+ * Add a response with tool calls
34
+ */
35
+ addToolCallResponse(toolCalls, content, options) {
36
+ return this.addResponse({
37
+ content: content ?? null,
38
+ toolCalls: toolCalls.map((tc) => ({
39
+ id: tc.id || `call_${crypto.randomUUID().substring(0, 8)}`,
40
+ type: "function",
41
+ function: {
42
+ name: tc.name,
43
+ arguments: JSON.stringify(tc.arguments)
44
+ }
45
+ })),
46
+ finishReason: "tool_calls",
47
+ ...options
48
+ });
49
+ }
50
+ /**
51
+ * Add an error response (simulates provider error)
52
+ */
53
+ addErrorResponse(message, code, status, options) {
54
+ return this.addResponse({
55
+ error: { message, code, status },
56
+ ...options
57
+ });
58
+ }
59
+ /**
60
+ * Add a response with reasoning content (for o1-style models)
61
+ */
62
+ addReasoningResponse(content, reasoningContent, options) {
63
+ return this.addResponse({
64
+ content,
65
+ reasoningContent,
66
+ finishReason: "stop",
67
+ ...options
68
+ });
69
+ }
70
+ /**
71
+ * Enable streaming simulation for all responses
72
+ */
73
+ withStreamingSimulation(config) {
74
+ this.streamingConfig = config;
75
+ return this;
76
+ }
77
+ /**
78
+ * Get all scripted responses
79
+ */
80
+ getResponses() {
81
+ return [...this.responses];
82
+ }
83
+ /**
84
+ * Get streaming configuration
85
+ */
86
+ getStreamingConfig() {
87
+ return this.streamingConfig;
88
+ }
89
+ /**
90
+ * Get total number of responses
91
+ */
92
+ get length() {
93
+ return this.responses.length;
94
+ }
95
+ /**
96
+ * Check if script is empty
97
+ */
98
+ isEmpty() {
99
+ return this.responses.length === 0;
100
+ }
101
+ /**
102
+ * Create a clone of this script
103
+ */
104
+ clone() {
105
+ const cloned = new _TestScript();
106
+ cloned.responses = [...this.responses];
107
+ cloned.streamingConfig = this.streamingConfig ? { ...this.streamingConfig } : void 0;
108
+ return cloned;
109
+ }
110
+ };
111
+
112
+ // src/agents/providers/TestProvider.ts
113
+ function contentToString(content) {
114
+ if (!content) return "";
115
+ if (typeof content === "string") return content;
116
+ return content.filter((part) => part.type === "text").map((part) => part.text).join(" ");
117
+ }
118
+ var TestProvider = class {
119
+ name = "test";
120
+ specificationVersion = "1";
121
+ script;
122
+ responseIndex = 0;
123
+ validateInputs;
124
+ debugLog;
125
+ requestLog = [];
126
+ useDefaultResponse;
127
+ constructor(config) {
128
+ this.useDefaultResponse = !config.script;
129
+ this.script = config.script ?? TestScript.create().addTextResponse("Test response received.");
130
+ this.validateInputs = config.validateInputs ?? false;
131
+ this.debugLog = config.debugLog ?? false;
132
+ }
133
+ /**
134
+ * Test provider supports any model ID
135
+ */
136
+ supportsModel(_modelId) {
137
+ return true;
138
+ }
139
+ /**
140
+ * Non-streaming generation - returns the next scripted response
141
+ */
142
+ async generate(request) {
143
+ if (this.debugLog) {
144
+ console.log(`[TestProvider] Request ${this.responseIndex + 1}:`, {
145
+ messageCount: request.messages.length,
146
+ lastMessage: request.messages.slice(-1)[0]
147
+ });
148
+ }
149
+ this.requestLog.push({ ...request });
150
+ if (request.signal?.aborted) {
151
+ throw new ProviderError("Request aborted", "timeout");
152
+ }
153
+ const responses = this.script.getResponses();
154
+ let scripted;
155
+ if (this.responseIndex >= responses.length) {
156
+ if (this.useDefaultResponse) {
157
+ scripted = responses[0];
158
+ } else {
159
+ const lastMessage = request.messages.slice(-1)[0];
160
+ const lastContent = lastMessage?.role === "user" ? contentToString(lastMessage.content) : lastMessage?.role === "assistant" ? lastMessage.content ?? "" : "";
161
+ throw new ProviderError(
162
+ `TestProvider: Script exhausted after ${this.responseIndex} responses. Expected ${responses.length} total requests. Received request with ${request.messages.length} messages. Last message role: "${lastMessage?.role}", content: "${String(lastContent).substring(0, 100)}..."`,
163
+ "invalid_request"
164
+ );
165
+ }
166
+ } else {
167
+ scripted = responses[this.responseIndex];
168
+ }
169
+ this.responseIndex++;
170
+ if (this.validateInputs && scripted.expectInput) {
171
+ this.validateInput(request, scripted.expectInput);
172
+ }
173
+ if (scripted.delayMs) {
174
+ await this.sleep(scripted.delayMs);
175
+ }
176
+ if (scripted.error) {
177
+ throw new ProviderError(
178
+ scripted.error.message,
179
+ scripted.error.code || "unknown",
180
+ scripted.error.status
181
+ );
182
+ }
183
+ const promptTokens = scripted.usage?.promptTokens ?? this.estimateTokens(request);
184
+ const completionTokens = scripted.usage?.completionTokens ?? Math.ceil((scripted.content?.length ?? 0) / 4);
185
+ const reasoningTokens = scripted.usage?.reasoningTokens ?? 0;
186
+ const toolCalls = scripted.toolCalls?.map((tc) => ({
187
+ id: tc.id,
188
+ name: tc.function.name,
189
+ arguments: JSON.parse(tc.function.arguments)
190
+ }));
191
+ const finishReason = scripted.finishReason === "tool_calls" ? "tool_calls" : "stop";
192
+ return {
193
+ content: scripted.content ?? null,
194
+ reasoning: scripted.reasoningContent ?? null,
195
+ reasoningDetails: scripted.reasoningDetails,
196
+ toolCalls,
197
+ finishReason,
198
+ usage: {
199
+ promptTokens,
200
+ completionTokens,
201
+ totalTokens: promptTokens + completionTokens,
202
+ reasoningTokens: reasoningTokens || void 0
203
+ }
204
+ };
205
+ }
206
+ /**
207
+ * Streaming generation - yields chunks for the next scripted response
208
+ */
209
+ async stream(request) {
210
+ const response = await this.generate(request);
211
+ const streamingConfig = this.script.getStreamingConfig();
212
+ const self = this;
213
+ return {
214
+ async *[Symbol.asyncIterator]() {
215
+ if (request.signal?.aborted) {
216
+ yield { type: "error", error: "Request aborted", code: "timeout" };
217
+ return;
218
+ }
219
+ if (response.content) {
220
+ if (streamingConfig) {
221
+ for (let i = 0; i < response.content.length; i += streamingConfig.chunkSize) {
222
+ const chunk = response.content.substring(i, i + streamingConfig.chunkSize);
223
+ yield { type: "content-delta", delta: chunk };
224
+ if (streamingConfig.chunkDelayMs > 0) {
225
+ await self.sleep(streamingConfig.chunkDelayMs);
226
+ }
227
+ }
228
+ } else {
229
+ yield { type: "content-delta", delta: response.content };
230
+ }
231
+ yield { type: "content-done" };
232
+ }
233
+ if (response.reasoning) {
234
+ yield { type: "reasoning-delta", delta: response.reasoning };
235
+ yield { type: "reasoning-done" };
236
+ }
237
+ if (response.toolCalls) {
238
+ for (const tc of response.toolCalls) {
239
+ yield { type: "tool-call-start", id: tc.id, name: tc.name };
240
+ yield { type: "tool-call-done", id: tc.id, arguments: tc.arguments };
241
+ }
242
+ }
243
+ yield {
244
+ type: "finish",
245
+ finishReason: response.finishReason,
246
+ usage: response.usage
247
+ };
248
+ }
249
+ };
250
+ }
251
+ /**
252
+ * Validate request matches expectations
253
+ */
254
+ validateInput(request, expectations) {
255
+ if (expectations.messageCount !== void 0) {
256
+ if (request.messages.length !== expectations.messageCount) {
257
+ throw new ProviderError(
258
+ `TestProvider: Expected ${expectations.messageCount} messages, got ${request.messages.length}`,
259
+ "invalid_request"
260
+ );
261
+ }
262
+ }
263
+ if (expectations.containsMessage) {
264
+ const pattern = expectations.containsMessage;
265
+ const found = request.messages.some((m) => {
266
+ const content = m.role === "user" ? contentToString(m.content) : m.role === "assistant" ? m.content : "";
267
+ if (!content) return false;
268
+ return typeof pattern === "string" ? content.includes(pattern) : pattern.test(content);
269
+ });
270
+ if (!found) {
271
+ throw new ProviderError(
272
+ `TestProvider: Expected message containing "${pattern}" not found`,
273
+ "invalid_request"
274
+ );
275
+ }
276
+ }
277
+ if (expectations.containsToolResult) {
278
+ const { toolName, resultContains } = expectations.containsToolResult;
279
+ const found = request.messages.some((m) => {
280
+ if (m.role !== "tool") return false;
281
+ const content = typeof m.content === "string" ? m.content : "";
282
+ return !resultContains || content.includes(resultContains);
283
+ });
284
+ if (!found) {
285
+ throw new ProviderError(
286
+ `TestProvider: Expected tool result for "${toolName}" not found`,
287
+ "invalid_request"
288
+ );
289
+ }
290
+ }
291
+ if (expectations.systemPromptContains) {
292
+ const pattern = expectations.systemPromptContains;
293
+ const systemMessage = request.messages.find((m) => m.role === "system");
294
+ const systemContent = systemMessage?.role === "system" ? systemMessage.content : "";
295
+ if (!systemContent) {
296
+ throw new ProviderError(`TestProvider: No system message found`, "invalid_request");
297
+ }
298
+ const matches = typeof pattern === "string" ? systemContent.includes(pattern) : pattern.test(systemContent);
299
+ if (!matches) {
300
+ throw new ProviderError(
301
+ `TestProvider: System prompt does not contain "${pattern}"`,
302
+ "invalid_request"
303
+ );
304
+ }
305
+ }
306
+ }
307
+ /**
308
+ * Simple token estimation (for mock usage stats)
309
+ */
310
+ estimateTokens(request) {
311
+ return request.messages.reduce((sum, m) => {
312
+ let content = "";
313
+ if (m.role === "system") {
314
+ content = m.content;
315
+ } else if (m.role === "user") {
316
+ content = contentToString(m.content);
317
+ } else if (m.role === "assistant") {
318
+ content = m.content ?? "";
319
+ } else if (m.role === "tool") {
320
+ content = typeof m.content === "string" ? m.content : "";
321
+ }
322
+ return sum + Math.ceil(content.length / 4);
323
+ }, 0);
324
+ }
325
+ sleep(ms) {
326
+ return new Promise((resolve) => setTimeout(resolve, ms));
327
+ }
328
+ // ============ Test Utility Methods ============
329
+ /**
330
+ * Get logged requests (for test assertions)
331
+ */
332
+ getRequestLog() {
333
+ return [...this.requestLog];
334
+ }
335
+ /**
336
+ * Get the last request made
337
+ */
338
+ getLastRequest() {
339
+ return this.requestLog[this.requestLog.length - 1];
340
+ }
341
+ /**
342
+ * Reset provider state for reuse across tests
343
+ */
344
+ reset() {
345
+ this.responseIndex = 0;
346
+ this.requestLog = [];
347
+ }
348
+ /**
349
+ * Replace the script with a new one and reset
350
+ */
351
+ setScript(script) {
352
+ this.script = script;
353
+ this.reset();
354
+ }
355
+ /**
356
+ * Check if all scripted responses were consumed
357
+ */
358
+ isScriptComplete() {
359
+ return this.responseIndex === this.script.length;
360
+ }
361
+ /**
362
+ * Get remaining unconsumed response count
363
+ */
364
+ remainingResponses() {
365
+ return this.script.length - this.responseIndex;
366
+ }
367
+ /**
368
+ * Get current response index
369
+ */
370
+ getCurrentIndex() {
371
+ return this.responseIndex;
372
+ }
373
+ };
374
+ function createTestProvider(script) {
375
+ return new TestProvider({
376
+ apiKey: "test",
377
+ script
378
+ });
379
+ }
380
+ var test = (config) => {
381
+ return new TestProvider({
382
+ ...config,
383
+ script: config.script,
384
+ validateInputs: config.validateInputs,
385
+ debugLog: config.debugLog
386
+ });
387
+ };
388
+
389
+ export { TestProvider, TestScript, createTestProvider, test };
390
+ //# sourceMappingURL=test.js.map
391
+ //# sourceMappingURL=test.js.map