@artemiskit/core 0.2.4 → 0.4.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +4 -0
  3. package/dist/adapters/registry.d.ts.map +1 -1
  4. package/dist/adapters/types.d.ts +50 -2
  5. package/dist/adapters/types.d.ts.map +1 -1
  6. package/dist/agent-evaluation/index.d.ts +3 -0
  7. package/dist/agent-evaluation/index.d.ts.map +1 -0
  8. package/dist/agent-evaluation/scorer.d.ts +35 -0
  9. package/dist/agent-evaluation/scorer.d.ts.map +1 -0
  10. package/dist/agent-evaluation/types.d.ts +37 -0
  11. package/dist/agent-evaluation/types.d.ts.map +1 -0
  12. package/dist/artifacts/manifest.d.ts.map +1 -1
  13. package/dist/artifacts/types.d.ts +54 -0
  14. package/dist/artifacts/types.d.ts.map +1 -1
  15. package/dist/evaluators/index.d.ts +1 -0
  16. package/dist/evaluators/index.d.ts.map +1 -1
  17. package/dist/evaluators/json-schema.d.ts +0 -1
  18. package/dist/evaluators/json-schema.d.ts.map +1 -1
  19. package/dist/evaluators/llm-grader.d.ts +2 -0
  20. package/dist/evaluators/llm-grader.d.ts.map +1 -1
  21. package/dist/evaluators/tool-trace.d.ts +7 -0
  22. package/dist/evaluators/tool-trace.d.ts.map +1 -0
  23. package/dist/evaluators/types.d.ts +20 -0
  24. package/dist/evaluators/types.d.ts.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +19964 -12482
  28. package/dist/runner/executor.d.ts.map +1 -1
  29. package/dist/runner/runner.d.ts.map +1 -1
  30. package/dist/runner/types.d.ts +4 -0
  31. package/dist/runner/types.d.ts.map +1 -1
  32. package/dist/scenario/schema.d.ts +789 -99
  33. package/dist/scenario/schema.d.ts.map +1 -1
  34. package/dist/storage/supabase.d.ts +26 -5
  35. package/dist/storage/supabase.d.ts.map +1 -1
  36. package/dist/storage/types.d.ts +167 -1
  37. package/dist/storage/types.d.ts.map +1 -1
  38. package/dist/tools/fixture-executor.d.ts +10 -0
  39. package/dist/tools/fixture-executor.d.ts.map +1 -0
  40. package/dist/tools/index.d.ts +4 -0
  41. package/dist/tools/index.d.ts.map +1 -0
  42. package/dist/tools/schema-validator.d.ts +10 -0
  43. package/dist/tools/schema-validator.d.ts.map +1 -0
  44. package/dist/tools/types.d.ts +50 -0
  45. package/dist/tools/types.d.ts.map +1 -0
  46. package/package.json +2 -1
  47. package/src/adapters/registry.ts +45 -0
  48. package/src/adapters/types.test.ts +21 -0
  49. package/src/adapters/types.ts +56 -0
  50. package/src/agent-evaluation/index.ts +2 -0
  51. package/src/agent-evaluation/scorer.test.ts +1194 -0
  52. package/src/agent-evaluation/scorer.ts +640 -0
  53. package/src/agent-evaluation/types.test.ts +27 -0
  54. package/src/agent-evaluation/types.ts +43 -0
  55. package/src/artifacts/manifest.test.ts +66 -19
  56. package/src/artifacts/manifest.ts +18 -5
  57. package/src/artifacts/types.ts +65 -0
  58. package/src/evaluators/index.ts +3 -0
  59. package/src/evaluators/json-schema.test.ts +130 -0
  60. package/src/evaluators/json-schema.ts +38 -63
  61. package/src/evaluators/llm-grader.test.ts +80 -0
  62. package/src/evaluators/llm-grader.ts +44 -6
  63. package/src/evaluators/tool-trace.test.ts +46 -0
  64. package/src/evaluators/tool-trace.ts +50 -0
  65. package/src/evaluators/types.ts +20 -0
  66. package/src/index.ts +6 -0
  67. package/src/runner/executor.test.ts +340 -0
  68. package/src/runner/executor.ts +289 -20
  69. package/src/runner/release-validation.test.ts +169 -0
  70. package/src/runner/runner.ts +5 -1
  71. package/src/runner/types.ts +4 -0
  72. package/src/scenario/schema.ts +66 -1
  73. package/src/storage/supabase.test.ts +1052 -0
  74. package/src/storage/supabase.ts +614 -5
  75. package/src/storage/types.ts +207 -1
  76. package/src/tools/fixture-executor.test.ts +88 -0
  77. package/src/tools/fixture-executor.ts +112 -0
  78. package/src/tools/index.ts +3 -0
  79. package/src/tools/schema-validator.test.ts +32 -0
  80. package/src/tools/schema-validator.ts +56 -0
  81. package/src/tools/types.ts +80 -0
@@ -2,7 +2,13 @@
2
2
  * Storage types and interfaces
3
3
  */
4
4
 
5
- import type { AnyManifest, RedTeamManifest, RunManifest, StressManifest } from '../artifacts/types';
5
+ import type {
6
+ AnyManifest,
7
+ CaseEvaluationEvidence,
8
+ RedTeamManifest,
9
+ RunManifest,
10
+ StressManifest,
11
+ } from '../artifacts/types';
6
12
 
7
13
  /**
8
14
  * Run listing item
@@ -173,3 +179,203 @@ export interface BaselineStorageAdapter extends StorageAdapter {
173
179
  regressionThreshold: number;
174
180
  } | null>;
175
181
  }
182
+
183
+ // ============================================================================
184
+ // Case Results Types (for granular analytics)
185
+ // ============================================================================
186
+
187
+ /**
188
+ * Status of an individual case result
189
+ */
190
+ export type CaseResultStatus = 'passed' | 'failed' | 'invalid' | 'error';
191
+
192
+ /**
193
+ * Individual case result record for storage
194
+ */
195
+ export interface CaseResultRecord {
196
+ /** Unique ID (auto-generated if not provided) */
197
+ id?: string;
198
+ /** Run ID this case belongs to */
199
+ runId: string;
200
+ /** Case ID from the test */
201
+ caseId: string;
202
+ /** Optional case name */
203
+ caseName?: string;
204
+ /** Result status */
205
+ status: CaseResultStatus;
206
+ /** Number of execution attempts represented by this terminal result. */
207
+ attempts?: number;
208
+ /** Score from 0.0 to 1.0 */
209
+ score: number;
210
+ /** Type of matcher used */
211
+ matcherType: string;
212
+ /** Reason for the status */
213
+ reason?: string;
214
+ /** Model response */
215
+ response: string;
216
+ /** Latency in milliseconds */
217
+ latencyMs: number;
218
+ /** Prompt tokens used */
219
+ promptTokens: number;
220
+ /** Completion tokens used */
221
+ completionTokens: number;
222
+ /** Total tokens used */
223
+ totalTokens: number;
224
+ /** Error message if status is 'error' */
225
+ error?: string;
226
+ /** Sanitized evaluator evidence retained for review. */
227
+ evidence?: CaseEvaluationEvidence;
228
+ /** Tags for categorization */
229
+ tags?: string[];
230
+ /** ISO timestamp when created */
231
+ createdAt?: string;
232
+ }
233
+
234
+ /**
235
+ * Options for querying case results
236
+ */
237
+ export interface CaseResultQueryOptions {
238
+ /** Filter by run ID */
239
+ runId?: string;
240
+ /** Filter by case ID */
241
+ caseId?: string;
242
+ /** Filter by status */
243
+ status?: CaseResultStatus;
244
+ /** Filter by tags (any match) */
245
+ tags?: string[];
246
+ /** Maximum results to return */
247
+ limit?: number;
248
+ /** Offset for pagination */
249
+ offset?: number;
250
+ }
251
+
252
+ // ============================================================================
253
+ // Metrics History Types (for trending)
254
+ // ============================================================================
255
+
256
+ /**
257
+ * Daily metrics snapshot for a project/scenario
258
+ */
259
+ export interface MetricsSnapshot {
260
+ /** Unique ID (auto-generated if not provided) */
261
+ id?: string;
262
+ /** Date of the snapshot (YYYY-MM-DD) */
263
+ date: string;
264
+ /** Project name */
265
+ project: string;
266
+ /** Optional scenario name (null for project-wide) */
267
+ scenario?: string;
268
+ /** Total runs on this date */
269
+ totalRuns: number;
270
+ /** Total cases across all runs */
271
+ totalCases: number;
272
+ /** Total passed cases */
273
+ passedCases: number;
274
+ /** Total failed cases */
275
+ failedCases: number;
276
+ /** Average success rate */
277
+ avgSuccessRate: number;
278
+ /** Average latency in ms */
279
+ avgLatencyMs: number;
280
+ /** Average tokens per run */
281
+ avgTokensPerRun: number;
282
+ /** Minimum success rate */
283
+ minSuccessRate?: number;
284
+ /** Maximum success rate */
285
+ maxSuccessRate?: number;
286
+ /** Minimum latency in ms */
287
+ minLatencyMs?: number;
288
+ /** Maximum latency in ms */
289
+ maxLatencyMs?: number;
290
+ /** Total tokens consumed */
291
+ totalTokens: number;
292
+ /** ISO timestamp when created */
293
+ createdAt?: string;
294
+ /** ISO timestamp when last updated */
295
+ updatedAt?: string;
296
+ }
297
+
298
+ /**
299
+ * Options for querying metrics history
300
+ */
301
+ export interface MetricsTrendOptions {
302
+ /** Project to query */
303
+ project: string;
304
+ /** Optional scenario filter */
305
+ scenario?: string;
306
+ /** Start date (YYYY-MM-DD) */
307
+ startDate?: string;
308
+ /** End date (YYYY-MM-DD) */
309
+ endDate?: string;
310
+ /** Maximum results to return */
311
+ limit?: number;
312
+ }
313
+
314
+ /**
315
+ * Trend data point for visualization
316
+ */
317
+ export interface TrendDataPoint {
318
+ date: string;
319
+ successRate: number;
320
+ latencyMs: number;
321
+ totalRuns: number;
322
+ totalTokens: number;
323
+ }
324
+
325
+ // ============================================================================
326
+ // Enhanced Storage Adapter with Analytics
327
+ // ============================================================================
328
+
329
+ /**
330
+ * Extended storage adapter with analytics capabilities
331
+ */
332
+ export interface AnalyticsStorageAdapter extends BaselineStorageAdapter {
333
+ /**
334
+ * Save an individual case result
335
+ */
336
+ saveCaseResult(result: CaseResultRecord): Promise<string>;
337
+
338
+ /**
339
+ * Save multiple case results in batch
340
+ */
341
+ saveCaseResults(results: CaseResultRecord[]): Promise<string[]>;
342
+
343
+ /**
344
+ * Get case results for a run
345
+ */
346
+ getCaseResults(runId: string): Promise<CaseResultRecord[]>;
347
+
348
+ /**
349
+ * Query case results with filters
350
+ */
351
+ queryCaseResults(options: CaseResultQueryOptions): Promise<CaseResultRecord[]>;
352
+
353
+ /**
354
+ * Save a metrics snapshot
355
+ */
356
+ saveMetricsSnapshot(snapshot: MetricsSnapshot): Promise<string>;
357
+
358
+ /**
359
+ * Get metrics trend data
360
+ */
361
+ getMetricsTrend(options: MetricsTrendOptions): Promise<TrendDataPoint[]>;
362
+
363
+ /**
364
+ * Get a specific metrics snapshot
365
+ */
366
+ getMetricsSnapshot(
367
+ date: string,
368
+ project: string,
369
+ scenario?: string
370
+ ): Promise<MetricsSnapshot | null>;
371
+
372
+ /**
373
+ * Aggregate and save daily metrics from runs
374
+ * This can be called to build/update metrics_history from existing runs
375
+ */
376
+ aggregateDailyMetrics?(
377
+ date: string,
378
+ project: string,
379
+ scenario?: string
380
+ ): Promise<MetricsSnapshot>;
381
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { FixtureToolExecutor } from './fixture-executor';
3
+
4
+ const tools = [
5
+ {
6
+ type: 'function' as const,
7
+ function: {
8
+ name: 'get_weather',
9
+ parameters: {
10
+ type: 'object',
11
+ additionalProperties: false,
12
+ required: ['city'],
13
+ properties: { city: { type: 'string' } },
14
+ },
15
+ },
16
+ },
17
+ ];
18
+
19
+ const call = (arguments_: string, name = 'get_weather') => ({
20
+ id: 'call-1',
21
+ type: 'function' as const,
22
+ function: { name, arguments: arguments_ },
23
+ });
24
+
25
+ describe('FixtureToolExecutor', () => {
26
+ it('returns a deterministic matching fixture result', async () => {
27
+ const executor = new FixtureToolExecutor({
28
+ tools,
29
+ fixtures: { get_weather: [{ when: { city: 'Lagos' }, result: { temperature_c: 28 } }] },
30
+ });
31
+
32
+ await expect(executor.execute(call('{"city":"Lagos"}'))).resolves.toEqual({
33
+ status: 'success',
34
+ result: { temperature_c: 28 },
35
+ });
36
+ });
37
+
38
+ it('rejects undeclared tools, invalid JSON, and unmatched fixtures', async () => {
39
+ const executor = new FixtureToolExecutor({ tools, fixtures: { get_weather: [] } });
40
+
41
+ await expect(executor.execute(call('{}', 'delete_weather'))).resolves.toMatchObject({
42
+ status: 'error',
43
+ error: { code: 'TOOL_UNKNOWN' },
44
+ });
45
+ await expect(executor.execute(call('{city:Lagos}'))).resolves.toMatchObject({
46
+ status: 'error',
47
+ error: { code: 'TOOL_ARGUMENTS_INVALID' },
48
+ });
49
+ await expect(executor.execute(call('{"city":"Lagos"}'))).resolves.toMatchObject({
50
+ status: 'error',
51
+ error: { code: 'TOOL_FIXTURE_NOT_FOUND' },
52
+ });
53
+ });
54
+
55
+ it('returns controlled fixture errors and result-size failures', async () => {
56
+ const errorExecutor = new FixtureToolExecutor({
57
+ tools,
58
+ fixtures: { get_weather: [{ error: 'Weather service is unavailable.' }] },
59
+ });
60
+ const limitedExecutor = new FixtureToolExecutor({
61
+ tools,
62
+ fixtures: { get_weather: [{ result: { forecast: 'x'.repeat(32) } }] },
63
+ maxToolResultBytes: 16,
64
+ });
65
+
66
+ await expect(errorExecutor.execute(call('{"city":"Lagos"}'))).resolves.toMatchObject({
67
+ status: 'error',
68
+ error: { code: 'TOOL_EXECUTION_FAILED' },
69
+ });
70
+ await expect(limitedExecutor.execute(call('{"city":"Lagos"}'))).resolves.toMatchObject({
71
+ status: 'error',
72
+ error: { code: 'TOOL_RESULT_TOO_LARGE' },
73
+ });
74
+ });
75
+
76
+ it('rejects unsafe fixture configuration and fixtures for undeclared tools', () => {
77
+ expect(
78
+ () =>
79
+ new FixtureToolExecutor({
80
+ tools,
81
+ fixtures: { get_weather: [{ result: { command: 'curl example.invalid' } }] },
82
+ })
83
+ ).toThrow('TOOL_FIXTURE_UNSAFE');
84
+ expect(() => new FixtureToolExecutor({ tools, fixtures: { no_such_tool: [] } })).toThrow(
85
+ 'TOOL_UNKNOWN'
86
+ );
87
+ });
88
+ });
@@ -0,0 +1,112 @@
1
+ import type { ToolCall, ToolDefinition } from '../adapters/types';
2
+ import { validateToolArguments } from './schema-validator';
3
+ import type {
4
+ FixtureExecutorOptions,
5
+ ToolExecutionContext,
6
+ ToolExecutionResult,
7
+ ToolFixture,
8
+ ToolFixtures,
9
+ } from './types';
10
+
11
+ const DISALLOWED_FIXTURE_FIELDS = new Set(['command', 'url', 'path', 'code', 'environment', 'env']);
12
+
13
+ export class FixtureToolExecutor {
14
+ private readonly tools = new Map<string, ToolDefinition>();
15
+ private readonly fixtures: ToolFixtures;
16
+ private readonly maxToolResultBytes: number;
17
+
18
+ constructor({ tools, fixtures, maxToolResultBytes = 32_768 }: FixtureExecutorOptions) {
19
+ for (const tool of tools) this.tools.set(tool.function.name, tool);
20
+ for (const [name, entries] of Object.entries(fixtures)) {
21
+ if (!this.tools.has(name)) {
22
+ throw new Error(`TOOL_UNKNOWN: fixture declared for undeclared tool '${name}'`);
23
+ }
24
+ entries.forEach(assertSafeFixture);
25
+ }
26
+ this.fixtures = fixtures;
27
+ this.maxToolResultBytes = maxToolResultBytes;
28
+ }
29
+
30
+ async execute(call: ToolCall, _context?: ToolExecutionContext): Promise<ToolExecutionResult> {
31
+ const tool = this.tools.get(call.function.name);
32
+ if (!tool) {
33
+ return error('TOOL_UNKNOWN', `Tool '${call.function.name}' is not declared.`);
34
+ }
35
+
36
+ const validated = validateToolArguments(call.function.arguments, tool.function.parameters);
37
+ if (!validated.valid) return { status: 'error', error: validated.error };
38
+
39
+ const fixture = this.fixtures[call.function.name]?.find((candidate) =>
40
+ matches(candidate.when, validated.arguments ?? {})
41
+ );
42
+ if (!fixture) {
43
+ return error('TOOL_FIXTURE_NOT_FOUND', `No fixture matched tool '${call.function.name}'.`);
44
+ }
45
+ if (fixture.error) return error('TOOL_EXECUTION_FAILED', fixture.error);
46
+
47
+ const serialized = JSON.stringify(fixture.result ?? {});
48
+ if (
49
+ serialized === undefined ||
50
+ new TextEncoder().encode(serialized).byteLength > this.maxToolResultBytes
51
+ ) {
52
+ return error(
53
+ 'TOOL_RESULT_TOO_LARGE',
54
+ `Tool '${call.function.name}' result exceeds the configured limit.`
55
+ );
56
+ }
57
+ return { status: 'success', result: fixture.result ?? {} };
58
+ }
59
+ }
60
+
61
+ function error(
62
+ code: NonNullable<ToolExecutionResult['error']>['code'],
63
+ message: string
64
+ ): ToolExecutionResult {
65
+ return { status: 'error', error: { code, message } };
66
+ }
67
+
68
+ function matches(
69
+ when: Record<string, unknown> | undefined,
70
+ args: Record<string, unknown>
71
+ ): boolean {
72
+ return !when || Object.entries(when).every(([key, value]) => deepEqual(args[key], value));
73
+ }
74
+
75
+ function deepEqual(left: unknown, right: unknown): boolean {
76
+ if (Object.is(left, right)) return true;
77
+ if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
78
+ if (Array.isArray(left) || Array.isArray(right)) {
79
+ return (
80
+ Array.isArray(left) &&
81
+ Array.isArray(right) &&
82
+ left.length === right.length &&
83
+ left.every((value, index) => deepEqual(value, right[index]))
84
+ );
85
+ }
86
+ const leftRecord = left as Record<string, unknown>;
87
+ const rightRecord = right as Record<string, unknown>;
88
+ const keys = Object.keys(rightRecord);
89
+ return (
90
+ keys.length === Object.keys(leftRecord).length &&
91
+ keys.every((key) => deepEqual(leftRecord[key], rightRecord[key]))
92
+ );
93
+ }
94
+
95
+ function assertSafeFixture(fixture: ToolFixture): void {
96
+ assertSafeValue(fixture.when);
97
+ assertSafeValue(fixture.result);
98
+ }
99
+
100
+ function assertSafeValue(value: unknown): void {
101
+ if (!value || typeof value !== 'object') return;
102
+ if (Array.isArray(value)) {
103
+ value.forEach(assertSafeValue);
104
+ return;
105
+ }
106
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
107
+ if (DISALLOWED_FIXTURE_FIELDS.has(key.toLowerCase())) {
108
+ throw new Error(`TOOL_FIXTURE_UNSAFE: '${key}' is not permitted in fixture data.`);
109
+ }
110
+ assertSafeValue(child);
111
+ }
112
+ }
@@ -0,0 +1,3 @@
1
+ export * from './fixture-executor';
2
+ export * from './schema-validator';
3
+ export * from './types';
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { validateToolArguments } from './schema-validator';
3
+
4
+ const weatherSchema = {
5
+ type: 'object',
6
+ additionalProperties: false,
7
+ required: ['city'],
8
+ properties: { city: { type: 'string', minLength: 1 } },
9
+ };
10
+
11
+ describe('validateToolArguments', () => {
12
+ it('returns parsed arguments that satisfy the JSON schema', () => {
13
+ expect(validateToolArguments('{"city":"Lagos"}', weatherSchema)).toEqual({
14
+ valid: true,
15
+ arguments: { city: 'Lagos' },
16
+ });
17
+ });
18
+
19
+ it('rejects malformed JSON without echoing the arguments', () => {
20
+ expect(validateToolArguments('{city:Lagos}', weatherSchema)).toEqual({
21
+ valid: false,
22
+ error: { code: 'TOOL_ARGUMENTS_INVALID', message: 'Tool arguments must be valid JSON.' },
23
+ });
24
+ });
25
+
26
+ it('rejects arguments that fail the declared schema', () => {
27
+ const result = validateToolArguments('{"city":42}', weatherSchema);
28
+
29
+ expect(result.valid).toBe(false);
30
+ expect(result.error?.code).toBe('TOOL_ARGUMENTS_SCHEMA_INVALID');
31
+ });
32
+ });
@@ -0,0 +1,56 @@
1
+ import Ajv, { type ValidateFunction } from 'ajv';
2
+
3
+ export interface ToolArgumentValidation {
4
+ valid: boolean;
5
+ arguments?: Record<string, unknown>;
6
+ error?: { code: 'TOOL_ARGUMENTS_INVALID' | 'TOOL_ARGUMENTS_SCHEMA_INVALID'; message: string };
7
+ }
8
+
9
+ const ajv = new Ajv({ allErrors: true, strict: false });
10
+ const validators = new WeakMap<object, ValidateFunction>();
11
+
12
+ function getValidator(schema: Record<string, unknown>): ValidateFunction {
13
+ const cached = validators.get(schema);
14
+ if (cached) return cached;
15
+ const validator = ajv.compile(schema);
16
+ validators.set(schema, validator);
17
+ return validator;
18
+ }
19
+
20
+ export function validateToolArguments(
21
+ rawArguments: string,
22
+ schema: Record<string, unknown>
23
+ ): ToolArgumentValidation {
24
+ let args: unknown;
25
+ try {
26
+ args = JSON.parse(rawArguments);
27
+ } catch {
28
+ return {
29
+ valid: false,
30
+ error: { code: 'TOOL_ARGUMENTS_INVALID', message: 'Tool arguments must be valid JSON.' },
31
+ };
32
+ }
33
+
34
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
35
+ return {
36
+ valid: false,
37
+ error: { code: 'TOOL_ARGUMENTS_INVALID', message: 'Tool arguments must be a JSON object.' },
38
+ };
39
+ }
40
+
41
+ const validator = getValidator(schema);
42
+ if (!validator(args)) {
43
+ const details = (validator.errors ?? [])
44
+ .map((error) => `${error.instancePath || '/'} ${error.message ?? 'is invalid'}`)
45
+ .join('; ');
46
+ return {
47
+ valid: false,
48
+ error: {
49
+ code: 'TOOL_ARGUMENTS_SCHEMA_INVALID',
50
+ message: `Tool arguments do not match the schema: ${details}`,
51
+ },
52
+ };
53
+ }
54
+
55
+ return { valid: true, arguments: args as Record<string, unknown> };
56
+ }
@@ -0,0 +1,80 @@
1
+ import type { ToolCall, ToolDefinition } from '../adapters/types';
2
+
3
+ export type ToolLoopTerminationReason =
4
+ | 'completed'
5
+ | 'max_steps'
6
+ | 'timeout'
7
+ | 'duplicate_call'
8
+ | 'invalid_arguments'
9
+ | 'unknown_tool'
10
+ | 'tool_error';
11
+
12
+ export interface ToolLoopPolicy {
13
+ enabled: boolean;
14
+ maxSteps: number;
15
+ timeoutMs: number;
16
+ maxToolResultBytes: number;
17
+ rejectDuplicateCalls: boolean;
18
+ }
19
+
20
+ export const DEFAULT_TOOL_LOOP_POLICY: ToolLoopPolicy = {
21
+ enabled: false,
22
+ maxSteps: 5,
23
+ timeoutMs: 60_000,
24
+ maxToolResultBytes: 32_768,
25
+ rejectDuplicateCalls: true,
26
+ };
27
+
28
+ export interface ToolExecutionContext {
29
+ caseId: string;
30
+ step: number;
31
+ }
32
+
33
+ export interface ToolExecutionError {
34
+ code:
35
+ | 'TOOL_ARGUMENTS_INVALID'
36
+ | 'TOOL_ARGUMENTS_SCHEMA_INVALID'
37
+ | 'TOOL_UNKNOWN'
38
+ | 'TOOL_FIXTURE_NOT_FOUND'
39
+ | 'TOOL_EXECUTION_FAILED'
40
+ | 'TOOL_RESULT_TOO_LARGE';
41
+ message: string;
42
+ }
43
+
44
+ export interface ToolExecutionResult {
45
+ status: 'success' | 'error';
46
+ result?: unknown;
47
+ error?: ToolExecutionError;
48
+ }
49
+
50
+ export interface ToolExecutor {
51
+ execute(call: ToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
52
+ }
53
+
54
+ export interface ToolTraceEntry {
55
+ step: number;
56
+ toolCall: ToolCall;
57
+ result?: unknown;
58
+ error?: ToolExecutionError;
59
+ latencyMs: number;
60
+ }
61
+
62
+ export interface ToolLoopSummary {
63
+ status: 'completed' | 'error';
64
+ steps: number;
65
+ terminationReason: ToolLoopTerminationReason;
66
+ }
67
+
68
+ export interface ToolFixture {
69
+ when?: Record<string, unknown>;
70
+ result?: unknown;
71
+ error?: string;
72
+ }
73
+
74
+ export type ToolFixtures = Record<string, ToolFixture[]>;
75
+
76
+ export interface FixtureExecutorOptions {
77
+ tools: ToolDefinition[];
78
+ fixtures: ToolFixtures;
79
+ maxToolResultBytes?: number;
80
+ }