ai-functions 2.1.1 → 2.3.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/.turbo/turbo-build.log +1 -4
- package/CHANGELOG.md +68 -1
- package/README.md +397 -157
- package/dist/ai-promise.d.ts +50 -3
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +410 -51
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai-schemas.d.ts +56 -0
- package/dist/ai-schemas.d.ts.map +1 -0
- package/dist/ai-schemas.js +53 -0
- package/dist/ai-schemas.js.map +1 -0
- package/dist/ai.d.ts +16 -242
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +54 -837
- package/dist/ai.js.map +1 -1
- package/dist/batch/anthropic.d.ts +6 -4
- package/dist/batch/anthropic.d.ts.map +1 -1
- package/dist/batch/anthropic.js +83 -145
- package/dist/batch/anthropic.js.map +1 -1
- package/dist/batch/bedrock.d.ts +8 -30
- package/dist/batch/bedrock.d.ts.map +1 -1
- package/dist/batch/bedrock.js +155 -338
- package/dist/batch/bedrock.js.map +1 -1
- package/dist/batch/cloudflare.d.ts +8 -20
- package/dist/batch/cloudflare.d.ts.map +1 -1
- package/dist/batch/cloudflare.js +68 -189
- package/dist/batch/cloudflare.js.map +1 -1
- package/dist/batch/google.d.ts +6 -20
- package/dist/batch/google.d.ts.map +1 -1
- package/dist/batch/google.js +70 -238
- package/dist/batch/google.js.map +1 -1
- package/dist/batch/index.d.ts +4 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +4 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/memory.d.ts +1 -1
- package/dist/batch/memory.d.ts.map +1 -1
- package/dist/batch/memory.js +14 -10
- package/dist/batch/memory.js.map +1 -1
- package/dist/batch/openai.d.ts +11 -14
- package/dist/batch/openai.d.ts.map +1 -1
- package/dist/batch/openai.js +52 -156
- package/dist/batch/openai.js.map +1 -1
- package/dist/batch/provider.d.ts +111 -0
- package/dist/batch/provider.d.ts.map +1 -0
- package/dist/batch/provider.js +233 -0
- package/dist/batch/provider.js.map +1 -0
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +23 -17
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +65 -0
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +169 -14
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts +272 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +513 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache.d.ts +295 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +433 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +42 -8
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -62
- package/dist/context.js.map +1 -1
- package/dist/digital-objects-registry.d.ts +229 -0
- package/dist/digital-objects-registry.d.ts.map +1 -0
- package/dist/digital-objects-registry.js +617 -0
- package/dist/digital-objects-registry.js.map +1 -0
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/eval/runner.d.ts +10 -1
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js +41 -35
- package/dist/eval/runner.js.map +1 -1
- package/dist/eval-log/in-memory.d.ts +34 -0
- package/dist/eval-log/in-memory.d.ts.map +1 -0
- package/dist/eval-log/in-memory.js +84 -0
- package/dist/eval-log/in-memory.js.map +1 -0
- package/dist/eval-log/index.d.ts +29 -0
- package/dist/eval-log/index.d.ts.map +1 -0
- package/dist/eval-log/index.js +39 -0
- package/dist/eval-log/index.js.map +1 -0
- package/dist/eval-log/types.d.ts +101 -0
- package/dist/eval-log/types.d.ts.map +1 -0
- package/dist/eval-log/types.js +16 -0
- package/dist/eval-log/types.js.map +1 -0
- package/dist/function-registry.d.ts +116 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +546 -0
- package/dist/function-registry.js.map +1 -0
- package/dist/generate.d.ts +9 -3
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +18 -22
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +35 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -42
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +118 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +187 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/budget.d.ts +84 -0
- package/dist/middleware/budget.d.ts.map +1 -0
- package/dist/middleware/budget.js +110 -0
- package/dist/middleware/budget.js.map +1 -0
- package/dist/middleware/cache.d.ts +103 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +228 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/embed-cache.d.ts +99 -0
- package/dist/middleware/embed-cache.d.ts.map +1 -0
- package/dist/middleware/embed-cache.js +128 -0
- package/dist/middleware/embed-cache.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/trace.d.ts +103 -0
- package/dist/middleware/trace.d.ts.map +1 -0
- package/dist/middleware/trace.js +176 -0
- package/dist/middleware/trace.js.map +1 -0
- package/dist/primitives.d.ts +120 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +398 -26
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +368 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +646 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -10
- package/dist/schema.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +285 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +6 -1
- package/dist/template.js.map +1 -1
- package/dist/tool-orchestration.d.ts +453 -0
- package/dist/tool-orchestration.d.ts.map +1 -0
- package/dist/tool-orchestration.js +763 -0
- package/dist/tool-orchestration.js.map +1 -0
- package/dist/type-guards.d.ts +28 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +29 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +135 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -1
- package/dist/types.js.map +1 -1
- package/dist/wrap-for-v3.d.ts +80 -0
- package/dist/wrap-for-v3.d.ts.map +1 -0
- package/dist/wrap-for-v3.js +89 -0
- package/dist/wrap-for-v3.js.map +1 -0
- package/examples/00-quickstart.ts +232 -0
- package/examples/01-rag-chatbot.ts +212 -0
- package/examples/02-multi-agent-research.ts +290 -0
- package/examples/03-email-classification.ts +379 -0
- package/examples/04-content-moderation.ts +400 -0
- package/examples/05-document-extraction.ts +455 -0
- package/examples/06-streaming-chat-nextjs.ts +437 -0
- package/examples/07-cloudflare-worker.ts +483 -0
- package/examples/08-batch-processing.ts +491 -0
- package/examples/09-budget-constrained.ts +527 -0
- package/examples/10-tool-orchestration.ts +565 -0
- package/examples/11-retry-resilience.ts +403 -0
- package/examples/12-caching-strategies.ts +422 -0
- package/examples/README.md +145 -0
- package/package.json +10 -6
- package/src/ai-promise.ts +528 -99
- package/src/ai-schemas.ts +122 -0
- package/src/ai.ts +69 -1153
- package/src/batch/anthropic.ts +96 -161
- package/src/batch/bedrock.ts +203 -454
- package/src/batch/cloudflare.ts +99 -282
- package/src/batch/google.ts +91 -297
- package/src/batch/index.ts +4 -1
- package/src/batch/memory.ts +15 -10
- package/src/batch/openai.ts +65 -193
- package/src/batch/provider.ts +336 -0
- package/src/batch-map.ts +29 -24
- package/src/batch-queue.ts +200 -11
- package/src/budget.ts +740 -0
- package/src/cache.ts +681 -0
- package/src/context.ts +122 -76
- package/src/digital-objects-registry.ts +750 -0
- package/src/errors.ts +37 -0
- package/src/eval/runner.ts +63 -38
- package/src/eval-log/in-memory.ts +90 -0
- package/src/eval-log/index.ts +46 -0
- package/src/eval-log/types.ts +110 -0
- package/src/function-registry.ts +671 -0
- package/src/generate.ts +33 -33
- package/src/index.ts +325 -49
- package/src/logger.ts +232 -0
- package/src/middleware/budget.ts +171 -0
- package/src/middleware/cache.ts +299 -0
- package/src/middleware/embed-cache.ts +195 -0
- package/src/middleware/index.ts +23 -0
- package/src/middleware/trace.ts +248 -0
- package/src/primitives.ts +589 -62
- package/src/retry.ts +902 -0
- package/src/schema.ts +8 -17
- package/src/telemetry.ts +403 -0
- package/src/template.ts +8 -4
- package/src/tool-orchestration.ts +1173 -0
- package/src/type-guards.ts +31 -0
- package/src/types.ts +164 -25
- package/src/wrap-for-v3.ts +105 -0
- package/test/ai-promise.test.ts +1080 -0
- package/test/ai-proxy.test.ts +1 -1
- package/test/backward-compat.test.ts +147 -0
- package/test/batch-autosubmit-errors.test.ts +610 -0
- package/test/batch-blog-posts.test.ts +87 -129
- package/test/budget-tracking.test.ts +800 -0
- package/test/cache.test.ts +712 -0
- package/test/context-isolation.test.ts +687 -0
- package/test/core-functions.test.ts +183 -579
- package/test/decide.test.ts +154 -322
- package/test/define.test.ts +211 -8
- package/test/digital-objects-registry.test.ts +760 -0
- package/test/embedding-cache-middleware.test.ts +140 -0
- package/test/evals/deterministic.eval.test.ts +376 -0
- package/test/generate-core.test.ts +140 -229
- package/test/implicit-batch.test.ts +22 -65
- package/test/json-parse-error-handling.test.ts +463 -0
- package/test/retry-policy-integration.test.ts +117 -0
- package/test/retry.test.ts +1016 -0
- package/test/schema.test.ts +55 -19
- package/test/streaming.test.ts +316 -0
- package/test/template.test.ts +1164 -0
- package/test/tool-orchestration.test.ts +1040 -0
- package/test/wrap-for-v3.test.ts +612 -0
- package/vitest.config.js +6 -0
- package/vitest.config.ts +20 -0
- package/dist/rpc/auth.d.ts +0 -69
- package/dist/rpc/auth.d.ts.map +0 -1
- package/dist/rpc/auth.js +0 -136
- package/dist/rpc/auth.js.map +0 -1
- package/dist/rpc/client.d.ts +0 -62
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -103
- package/dist/rpc/client.js.map +0 -1
- package/dist/rpc/deferred.d.ts +0 -60
- package/dist/rpc/deferred.d.ts.map +0 -1
- package/dist/rpc/deferred.js +0 -96
- package/dist/rpc/deferred.js.map +0 -1
- package/dist/rpc/index.d.ts +0 -22
- package/dist/rpc/index.d.ts.map +0 -1
- package/dist/rpc/index.js +0 -38
- package/dist/rpc/index.js.map +0 -1
- package/dist/rpc/local.d.ts +0 -42
- package/dist/rpc/local.d.ts.map +0 -1
- package/dist/rpc/local.js +0 -50
- package/dist/rpc/local.js.map +0 -1
- package/dist/rpc/server.d.ts +0 -165
- package/dist/rpc/server.d.ts.map +0 -1
- package/dist/rpc/server.js +0 -405
- package/dist/rpc/server.js.map +0 -1
- package/dist/rpc/session.d.ts +0 -32
- package/dist/rpc/session.d.ts.map +0 -1
- package/dist/rpc/session.js +0 -43
- package/dist/rpc/session.js.map +0 -1
- package/dist/rpc/transport.d.ts +0 -306
- package/dist/rpc/transport.d.ts.map +0 -1
- package/dist/rpc/transport.js +0 -731
- package/dist/rpc/transport.js.map +0 -1
- package/src/batch/anthropic.js +0 -256
- package/src/batch/bedrock.js +0 -584
- package/src/batch/cloudflare.js +0 -287
- package/src/batch/google.js +0 -359
- package/src/batch/index.js +0 -30
- package/src/batch/memory.js +0 -187
- package/src/batch/openai.js +0 -402
- package/src/eval/index.js +0 -7
- package/src/eval/models.js +0 -119
- package/src/eval/runner.js +0 -147
- package/test/schema.test.js +0 -96
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic Tool Orchestration
|
|
3
|
+
*
|
|
4
|
+
* Provides multi-turn model→tools→model loop orchestration for complex AI workflows.
|
|
5
|
+
*
|
|
6
|
+
* Key components:
|
|
7
|
+
* - AgenticLoop: Orchestrates multi-turn conversations with tool execution
|
|
8
|
+
* - ToolRouter: Routes tool calls to registered handlers
|
|
9
|
+
* - ToolValidator: Validates tool arguments before execution
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z, type ZodTypeAny } from 'zod'
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A tool that can be executed by the agentic loop
|
|
22
|
+
*/
|
|
23
|
+
export interface Tool<TParams extends ZodTypeAny = ZodTypeAny, TResult = unknown> {
|
|
24
|
+
/** Unique name for the tool */
|
|
25
|
+
name: string
|
|
26
|
+
/** Human-readable description */
|
|
27
|
+
description: string
|
|
28
|
+
/** Zod schema for parameters */
|
|
29
|
+
parameters: TParams
|
|
30
|
+
/** Execute the tool with validated parameters */
|
|
31
|
+
execute: (params: z.infer<TParams>) => Promise<TResult>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A tool call from the model
|
|
36
|
+
*/
|
|
37
|
+
export interface ToolCall {
|
|
38
|
+
/** Name of the tool to call */
|
|
39
|
+
name: string
|
|
40
|
+
/** Arguments for the tool */
|
|
41
|
+
arguments: Record<string, unknown>
|
|
42
|
+
/** Optional ID for tracking */
|
|
43
|
+
id?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of a tool execution
|
|
48
|
+
*/
|
|
49
|
+
export interface ToolResult<T = unknown> {
|
|
50
|
+
/** Whether execution succeeded */
|
|
51
|
+
success: boolean
|
|
52
|
+
/** The result value if successful */
|
|
53
|
+
result?: T
|
|
54
|
+
/** Error message if failed */
|
|
55
|
+
error?: string
|
|
56
|
+
/** The original tool call */
|
|
57
|
+
toolCall?: ToolCall
|
|
58
|
+
/** Number of retries attempted */
|
|
59
|
+
retryCount?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Formatted tool result for model consumption
|
|
64
|
+
*/
|
|
65
|
+
export interface FormattedToolResult {
|
|
66
|
+
/** Role is always 'tool' */
|
|
67
|
+
role: 'tool'
|
|
68
|
+
/** String content of the result */
|
|
69
|
+
content: string
|
|
70
|
+
/** Tool call ID for correlation */
|
|
71
|
+
tool_call_id?: string
|
|
72
|
+
/** Whether this is an error result */
|
|
73
|
+
isError?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validation result for tool arguments
|
|
78
|
+
*/
|
|
79
|
+
export interface ValidationResult {
|
|
80
|
+
/** Whether validation passed */
|
|
81
|
+
valid: boolean
|
|
82
|
+
/** Validation errors if any */
|
|
83
|
+
errors?: string[]
|
|
84
|
+
/** Validated and parsed arguments */
|
|
85
|
+
parsedArgs?: unknown
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Model response from a generation
|
|
90
|
+
*/
|
|
91
|
+
export interface ModelResponse {
|
|
92
|
+
/** Generated text (if no tool calls) */
|
|
93
|
+
text?: string
|
|
94
|
+
/** Tool calls requested by the model */
|
|
95
|
+
toolCalls?: ToolCall[]
|
|
96
|
+
/** Why generation stopped */
|
|
97
|
+
finishReason: 'stop' | 'tool_call' | 'length' | 'content_filter' | 'error'
|
|
98
|
+
/** Token usage */
|
|
99
|
+
usage?: {
|
|
100
|
+
promptTokens: number
|
|
101
|
+
completionTokens: number
|
|
102
|
+
totalTokens: number
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Message in the conversation
|
|
108
|
+
*/
|
|
109
|
+
export interface Message {
|
|
110
|
+
role: 'user' | 'assistant' | 'system' | 'tool'
|
|
111
|
+
content: string
|
|
112
|
+
tool_calls?: ToolCall[]
|
|
113
|
+
tool_call_id?: string
|
|
114
|
+
isError?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Step information for callbacks
|
|
119
|
+
*/
|
|
120
|
+
export interface StepInfo {
|
|
121
|
+
/** Step number (1-indexed) */
|
|
122
|
+
stepNumber: number
|
|
123
|
+
/** Tool calls in this step */
|
|
124
|
+
toolCalls: Array<ToolCall & { result?: unknown; error?: string }>
|
|
125
|
+
/** Model response */
|
|
126
|
+
response: ModelResponse
|
|
127
|
+
/** Current messages */
|
|
128
|
+
messages: Message[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Options for creating an AgenticLoop
|
|
133
|
+
*/
|
|
134
|
+
export interface LoopOptions {
|
|
135
|
+
/** Available tools */
|
|
136
|
+
tools: Tool[]
|
|
137
|
+
/** Maximum number of steps before stopping */
|
|
138
|
+
maxSteps: number
|
|
139
|
+
/** Whether to throw error when maxSteps is exceeded */
|
|
140
|
+
strictMaxSteps?: boolean
|
|
141
|
+
/** Whether to execute tool calls in parallel */
|
|
142
|
+
parallelExecution?: boolean
|
|
143
|
+
/** Maximum concurrent tool calls when parallel execution is enabled */
|
|
144
|
+
maxParallelCalls?: number
|
|
145
|
+
/** Whether to retry failed tool calls */
|
|
146
|
+
retryFailedTools?: boolean
|
|
147
|
+
/** Maximum retries per tool call */
|
|
148
|
+
maxToolRetries?: number
|
|
149
|
+
/** Whether to continue when a tool fails */
|
|
150
|
+
continueOnError?: boolean
|
|
151
|
+
/** Timeout for individual tool execution (ms) */
|
|
152
|
+
toolTimeout?: number
|
|
153
|
+
/** Track token usage across steps */
|
|
154
|
+
trackUsage?: boolean
|
|
155
|
+
/** Callback for each step */
|
|
156
|
+
onStep?: (step: StepInfo) => void
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Model generation options passed to the model
|
|
161
|
+
*/
|
|
162
|
+
export interface ModelGenerationOptions {
|
|
163
|
+
/** Messages for the conversation */
|
|
164
|
+
messages: Message[]
|
|
165
|
+
/** Tools available for use */
|
|
166
|
+
tools: Record<
|
|
167
|
+
string,
|
|
168
|
+
{ description: string; parameters: unknown; execute: (args: unknown) => Promise<unknown> }
|
|
169
|
+
>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Options for running the loop
|
|
174
|
+
*/
|
|
175
|
+
export interface RunOptions {
|
|
176
|
+
/** Model to use for generation */
|
|
177
|
+
model: {
|
|
178
|
+
generate: (options: ModelGenerationOptions) => Promise<ModelResponse>
|
|
179
|
+
}
|
|
180
|
+
/** Initial prompt */
|
|
181
|
+
prompt: string
|
|
182
|
+
/** System message */
|
|
183
|
+
system?: string
|
|
184
|
+
/** Abort signal */
|
|
185
|
+
abortSignal?: AbortSignal
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extended tool call result with metadata
|
|
190
|
+
*/
|
|
191
|
+
export interface ToolCallResult {
|
|
192
|
+
/** Tool name */
|
|
193
|
+
name: string
|
|
194
|
+
/** Arguments passed */
|
|
195
|
+
arguments: Record<string, unknown>
|
|
196
|
+
/** Result if successful */
|
|
197
|
+
result?: unknown
|
|
198
|
+
/** Error if failed */
|
|
199
|
+
error?: string
|
|
200
|
+
/** Number of retries */
|
|
201
|
+
retryCount?: number
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Tool result for SDK compatibility
|
|
206
|
+
*/
|
|
207
|
+
export interface SDKToolResult {
|
|
208
|
+
/** Tool name */
|
|
209
|
+
toolName: string
|
|
210
|
+
/** Tool call ID */
|
|
211
|
+
toolCallId?: string
|
|
212
|
+
/** Result value */
|
|
213
|
+
result: unknown
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Result of running the agentic loop
|
|
218
|
+
*/
|
|
219
|
+
export interface LoopResult {
|
|
220
|
+
/** Final text output */
|
|
221
|
+
text: string
|
|
222
|
+
/** Number of steps executed */
|
|
223
|
+
steps: number
|
|
224
|
+
/** All tool calls made */
|
|
225
|
+
toolCalls: ToolCallResult[]
|
|
226
|
+
/** Tool results in SDK format */
|
|
227
|
+
toolResults: SDKToolResult[]
|
|
228
|
+
/** Why the loop stopped */
|
|
229
|
+
stopReason: 'stop' | 'max_steps' | 'error' | 'aborted'
|
|
230
|
+
/** Token usage if tracked */
|
|
231
|
+
usage?: {
|
|
232
|
+
promptTokens: number
|
|
233
|
+
completionTokens: number
|
|
234
|
+
totalTokens: number
|
|
235
|
+
}
|
|
236
|
+
/** Conversation messages */
|
|
237
|
+
messages: Message[]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// ToolValidator
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validates tool arguments before execution
|
|
246
|
+
*/
|
|
247
|
+
export class ToolValidator {
|
|
248
|
+
private tools = new Map<string, Tool>()
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Register a tool for validation
|
|
252
|
+
*/
|
|
253
|
+
register(tool: Tool): void {
|
|
254
|
+
this.tools.set(tool.name, tool)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate arguments for a tool
|
|
259
|
+
*/
|
|
260
|
+
validate(toolName: string, args: unknown): ValidationResult {
|
|
261
|
+
const tool = this.tools.get(toolName)
|
|
262
|
+
if (!tool) {
|
|
263
|
+
return {
|
|
264
|
+
valid: false,
|
|
265
|
+
errors: [`Tool '${toolName}' not registered`],
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const parsed = tool.parameters.parse(args)
|
|
271
|
+
return {
|
|
272
|
+
valid: true,
|
|
273
|
+
parsedArgs: parsed,
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error instanceof z.ZodError) {
|
|
277
|
+
return {
|
|
278
|
+
valid: false,
|
|
279
|
+
errors: error.errors.map((e) => `${e.path.join('.')}: ${e.message}`),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
valid: false,
|
|
284
|
+
errors: [(error as Error).message],
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Validate multiple tool calls at once
|
|
291
|
+
*/
|
|
292
|
+
validateAll(calls: ToolCall[]): ValidationResult[] {
|
|
293
|
+
return calls.map((call) => this.validate(call.name, call.arguments))
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// ToolRouter
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Routes tool calls to registered handlers
|
|
303
|
+
*
|
|
304
|
+
* @deprecated Phase C Week 2 — `ToolRouter` has zero production callers in
|
|
305
|
+
* primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). Only the
|
|
306
|
+
* `ai-primitives` umbrella re-export tests reference it. AI SDK 6's native
|
|
307
|
+
* tool-routing under `generateText({ tools })` and `Agent` / `ToolLoopAgent`
|
|
308
|
+
* cover the same surface. Will be removed in the Phase C semver bump
|
|
309
|
+
* alongside `AgenticLoop` and `createAgenticLoop`.
|
|
310
|
+
*/
|
|
311
|
+
export class ToolRouter {
|
|
312
|
+
private tools = new Map<string, Tool>()
|
|
313
|
+
private validator = new ToolValidator()
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Register a tool
|
|
317
|
+
*/
|
|
318
|
+
register(tool: Tool): void {
|
|
319
|
+
this.tools.set(tool.name, tool)
|
|
320
|
+
this.validator.register(tool)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Route a single tool call
|
|
325
|
+
*/
|
|
326
|
+
async route(call: ToolCall): Promise<ToolResult> {
|
|
327
|
+
const tool = this.tools.get(call.name)
|
|
328
|
+
if (!tool) {
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
error: `Tool '${call.name}' not found`,
|
|
332
|
+
toolCall: call,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const validation = this.validator.validate(call.name, call.arguments)
|
|
337
|
+
if (!validation.valid) {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: `Validation failed: ${validation.errors?.join(', ')}`,
|
|
341
|
+
toolCall: call,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const result = await tool.execute(validation.parsedArgs)
|
|
347
|
+
return {
|
|
348
|
+
success: true,
|
|
349
|
+
result,
|
|
350
|
+
toolCall: call,
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return {
|
|
354
|
+
success: false,
|
|
355
|
+
error: (error as Error).message,
|
|
356
|
+
toolCall: call,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Route multiple tool calls sequentially
|
|
363
|
+
*/
|
|
364
|
+
async routeAll(calls: ToolCall[]): Promise<ToolResult[]> {
|
|
365
|
+
const results: ToolResult[] = []
|
|
366
|
+
for (const call of calls) {
|
|
367
|
+
results.push(await this.route(call))
|
|
368
|
+
}
|
|
369
|
+
return results
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Route multiple tool calls in parallel
|
|
374
|
+
*/
|
|
375
|
+
async routeAllParallel(calls: ToolCall[]): Promise<ToolResult[]> {
|
|
376
|
+
return Promise.all(calls.map((call) => this.route(call)))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Format a tool result for model consumption
|
|
381
|
+
*/
|
|
382
|
+
formatResult(result: ToolResult): FormattedToolResult {
|
|
383
|
+
if (result.success) {
|
|
384
|
+
return {
|
|
385
|
+
role: 'tool',
|
|
386
|
+
content: JSON.stringify(result.result),
|
|
387
|
+
...(result.toolCall?.id !== undefined && { tool_call_id: result.toolCall.id }),
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
role: 'tool',
|
|
392
|
+
content: JSON.stringify({ error: result.error }),
|
|
393
|
+
...(result.toolCall?.id !== undefined && { tool_call_id: result.toolCall.id }),
|
|
394
|
+
isError: true,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// AgenticLoop
|
|
401
|
+
// ============================================================================
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Orchestrates multi-turn model→tools→model loops
|
|
405
|
+
*
|
|
406
|
+
* @deprecated Phase C Week 2 — `AgenticLoop` has zero production callers in
|
|
407
|
+
* primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). Only the
|
|
408
|
+
* `ai-primitives` umbrella re-export tests reference it. The production
|
|
409
|
+
* cascade walker (`services-as-software/v3/invoke/cascade-walker.ts:178`)
|
|
410
|
+
* already uses AI SDK 6's `generateText({ tools, maxSteps: 10 })` directly
|
|
411
|
+
* for agentic steps — no consumer code paths through this class. AI SDK 6's
|
|
412
|
+
* `Agent` / `ToolLoopAgent` (`stopWhen: stepCountIs(N)`) are the going-
|
|
413
|
+
* forward primitives. Will be removed in the Phase C semver bump.
|
|
414
|
+
*/
|
|
415
|
+
export class AgenticLoop {
|
|
416
|
+
private options: LoopOptions
|
|
417
|
+
private router: ToolRouter
|
|
418
|
+
private validator: ToolValidator
|
|
419
|
+
|
|
420
|
+
constructor(options: LoopOptions) {
|
|
421
|
+
this.options = {
|
|
422
|
+
strictMaxSteps: false,
|
|
423
|
+
parallelExecution: false,
|
|
424
|
+
maxParallelCalls: 10,
|
|
425
|
+
retryFailedTools: false,
|
|
426
|
+
maxToolRetries: 3,
|
|
427
|
+
continueOnError: false,
|
|
428
|
+
trackUsage: false,
|
|
429
|
+
...options,
|
|
430
|
+
}
|
|
431
|
+
this.router = new ToolRouter()
|
|
432
|
+
this.validator = new ToolValidator()
|
|
433
|
+
|
|
434
|
+
// Register all tools
|
|
435
|
+
for (const tool of options.tools) {
|
|
436
|
+
this.router.register(tool)
|
|
437
|
+
this.validator.register(tool)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get tools in AI SDK format
|
|
443
|
+
*/
|
|
444
|
+
getToolsForSDK(): Record<
|
|
445
|
+
string,
|
|
446
|
+
{ description: string; parameters: unknown; execute: (args: unknown) => Promise<unknown> }
|
|
447
|
+
> {
|
|
448
|
+
const tools: Record<
|
|
449
|
+
string,
|
|
450
|
+
{ description: string; parameters: unknown; execute: (args: unknown) => Promise<unknown> }
|
|
451
|
+
> = {}
|
|
452
|
+
for (const tool of this.options.tools) {
|
|
453
|
+
tools[tool.name] = {
|
|
454
|
+
description: tool.description,
|
|
455
|
+
parameters: tool.parameters,
|
|
456
|
+
execute: tool.execute,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return tools
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Execute a tool call with timeout and retry support
|
|
464
|
+
*/
|
|
465
|
+
private async executeToolCall(
|
|
466
|
+
call: ToolCall,
|
|
467
|
+
abortSignal?: AbortSignal
|
|
468
|
+
): Promise<ToolCallResult> {
|
|
469
|
+
const { toolTimeout, retryFailedTools, maxToolRetries = 3 } = this.options
|
|
470
|
+
let lastError: string | undefined
|
|
471
|
+
let retryCount = 0
|
|
472
|
+
|
|
473
|
+
const maxAttempts = retryFailedTools ? maxToolRetries : 1
|
|
474
|
+
|
|
475
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
476
|
+
// Check abort signal
|
|
477
|
+
if (abortSignal?.aborted) {
|
|
478
|
+
throw new Error('Aborted')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
// Create a promise for the tool execution
|
|
483
|
+
const executePromise = this.router.route(call)
|
|
484
|
+
|
|
485
|
+
// Apply timeout if configured
|
|
486
|
+
let result: ToolResult
|
|
487
|
+
if (toolTimeout) {
|
|
488
|
+
let timeoutId: NodeJS.Timeout
|
|
489
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
490
|
+
timeoutId = setTimeout(() => reject(new Error('Tool execution timeout')), toolTimeout)
|
|
491
|
+
})
|
|
492
|
+
try {
|
|
493
|
+
result = await Promise.race([executePromise, timeoutPromise])
|
|
494
|
+
} finally {
|
|
495
|
+
clearTimeout(timeoutId!)
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
result = await executePromise
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (result.success) {
|
|
502
|
+
return {
|
|
503
|
+
name: call.name,
|
|
504
|
+
arguments: call.arguments,
|
|
505
|
+
result: result.result,
|
|
506
|
+
retryCount,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
lastError = result.error
|
|
511
|
+
retryCount = attempt + 1
|
|
512
|
+
} catch (error) {
|
|
513
|
+
lastError = (error as Error).message
|
|
514
|
+
if (lastError === 'Aborted') throw error
|
|
515
|
+
retryCount = attempt + 1
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
name: call.name,
|
|
521
|
+
arguments: call.arguments,
|
|
522
|
+
...(lastError !== undefined && { error: lastError }),
|
|
523
|
+
retryCount: retryCount > 0 ? retryCount - 1 : 0,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Execute multiple tool calls
|
|
529
|
+
*/
|
|
530
|
+
private async executeToolCalls(
|
|
531
|
+
calls: ToolCall[],
|
|
532
|
+
abortSignal?: AbortSignal
|
|
533
|
+
): Promise<ToolCallResult[]> {
|
|
534
|
+
const { parallelExecution, maxParallelCalls = 10 } = this.options
|
|
535
|
+
|
|
536
|
+
if (!parallelExecution) {
|
|
537
|
+
// Sequential execution
|
|
538
|
+
const results: ToolCallResult[] = []
|
|
539
|
+
for (const call of calls) {
|
|
540
|
+
results.push(await this.executeToolCall(call, abortSignal))
|
|
541
|
+
}
|
|
542
|
+
return results
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Parallel execution with concurrency limit
|
|
546
|
+
const results: ToolCallResult[] = []
|
|
547
|
+
const chunks: ToolCall[][] = []
|
|
548
|
+
|
|
549
|
+
for (let i = 0; i < calls.length; i += maxParallelCalls) {
|
|
550
|
+
chunks.push(calls.slice(i, i + maxParallelCalls))
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const chunk of chunks) {
|
|
554
|
+
const chunkResults = await Promise.all(
|
|
555
|
+
chunk.map((call) => this.executeToolCall(call, abortSignal))
|
|
556
|
+
)
|
|
557
|
+
results.push(...chunkResults)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return results
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Build messages for the next model call
|
|
565
|
+
*/
|
|
566
|
+
private buildMessages(
|
|
567
|
+
prompt: string,
|
|
568
|
+
system: string | undefined,
|
|
569
|
+
conversationMessages: Message[],
|
|
570
|
+
toolResults: ToolCallResult[]
|
|
571
|
+
): Message[] {
|
|
572
|
+
const messages: Message[] = []
|
|
573
|
+
|
|
574
|
+
// Add system message if provided
|
|
575
|
+
if (system) {
|
|
576
|
+
messages.push({ role: 'system', content: system })
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Add conversation history
|
|
580
|
+
messages.push(...conversationMessages)
|
|
581
|
+
|
|
582
|
+
// Add tool results as tool messages
|
|
583
|
+
for (const result of toolResults) {
|
|
584
|
+
if (result.error) {
|
|
585
|
+
messages.push({
|
|
586
|
+
role: 'tool',
|
|
587
|
+
content: JSON.stringify({ error: result.error }),
|
|
588
|
+
isError: true,
|
|
589
|
+
})
|
|
590
|
+
} else {
|
|
591
|
+
messages.push({
|
|
592
|
+
role: 'tool',
|
|
593
|
+
content: JSON.stringify(result.result),
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return messages
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Run the agentic loop
|
|
603
|
+
*/
|
|
604
|
+
async run(runOptions: RunOptions): Promise<LoopResult> {
|
|
605
|
+
const { model, prompt, system, abortSignal } = runOptions
|
|
606
|
+
const { maxSteps, strictMaxSteps, continueOnError, trackUsage, onStep } = this.options
|
|
607
|
+
|
|
608
|
+
const allToolCalls: ToolCallResult[] = []
|
|
609
|
+
const allToolResults: SDKToolResult[] = []
|
|
610
|
+
const messages: Message[] = [{ role: 'user', content: prompt }]
|
|
611
|
+
let steps = 0
|
|
612
|
+
let stopReason: LoopResult['stopReason'] = 'stop'
|
|
613
|
+
let finalText = ''
|
|
614
|
+
let totalUsage = trackUsage
|
|
615
|
+
? { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
616
|
+
: undefined
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
while (steps < maxSteps) {
|
|
620
|
+
// Check abort signal
|
|
621
|
+
if (abortSignal?.aborted) {
|
|
622
|
+
stopReason = 'aborted'
|
|
623
|
+
throw new Error('Aborted')
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
steps++
|
|
627
|
+
|
|
628
|
+
// Call the model
|
|
629
|
+
const response = await model.generate({
|
|
630
|
+
messages: this.buildMessages(prompt, system, messages.slice(1), []),
|
|
631
|
+
tools: this.getToolsForSDK(),
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
// Track usage
|
|
635
|
+
if (trackUsage && response.usage) {
|
|
636
|
+
totalUsage!.promptTokens += response.usage.promptTokens
|
|
637
|
+
totalUsage!.completionTokens += response.usage.completionTokens
|
|
638
|
+
totalUsage!.totalTokens += response.usage.totalTokens
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// If no tool calls, we're done
|
|
642
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
643
|
+
finalText = response.text || ''
|
|
644
|
+
messages.push({ role: 'assistant', content: finalText })
|
|
645
|
+
stopReason = 'stop'
|
|
646
|
+
|
|
647
|
+
if (onStep) {
|
|
648
|
+
onStep({
|
|
649
|
+
stepNumber: steps,
|
|
650
|
+
toolCalls: [],
|
|
651
|
+
response,
|
|
652
|
+
messages: [...messages],
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
break
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Execute tool calls
|
|
659
|
+
const toolResults = await this.executeToolCalls(response.toolCalls, abortSignal)
|
|
660
|
+
|
|
661
|
+
// Check for errors
|
|
662
|
+
const hasErrors = toolResults.some((r) => r.error)
|
|
663
|
+
if (hasErrors && !continueOnError) {
|
|
664
|
+
// Still record the results but note the errors
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Record tool calls and results
|
|
668
|
+
for (const result of toolResults) {
|
|
669
|
+
allToolCalls.push(result)
|
|
670
|
+
allToolResults.push({
|
|
671
|
+
toolName: result.name,
|
|
672
|
+
result: result.result,
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
// Add tool result to messages
|
|
676
|
+
if (result.error) {
|
|
677
|
+
messages.push({
|
|
678
|
+
role: 'tool',
|
|
679
|
+
content: JSON.stringify({ error: result.error }),
|
|
680
|
+
isError: true,
|
|
681
|
+
})
|
|
682
|
+
} else {
|
|
683
|
+
messages.push({
|
|
684
|
+
role: 'tool',
|
|
685
|
+
content: JSON.stringify(result.result),
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Call onStep callback
|
|
691
|
+
if (onStep) {
|
|
692
|
+
onStep({
|
|
693
|
+
stepNumber: steps,
|
|
694
|
+
toolCalls: response.toolCalls.map((tc, i) => ({
|
|
695
|
+
...tc,
|
|
696
|
+
...(toolResults[i]?.result !== undefined && { result: toolResults[i]?.result }),
|
|
697
|
+
...(toolResults[i]?.error !== undefined && { error: toolResults[i]?.error }),
|
|
698
|
+
})),
|
|
699
|
+
response,
|
|
700
|
+
messages: [...messages],
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Add assistant message with tool calls
|
|
705
|
+
messages.push({
|
|
706
|
+
role: 'assistant',
|
|
707
|
+
content: '',
|
|
708
|
+
tool_calls: response.toolCalls,
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Check if we hit max steps
|
|
713
|
+
if (steps >= maxSteps && stopReason === 'stop') {
|
|
714
|
+
stopReason = 'max_steps'
|
|
715
|
+
if (strictMaxSteps) {
|
|
716
|
+
throw new Error('Max steps exceeded')
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if ((error as Error).message === 'Aborted') {
|
|
721
|
+
stopReason = 'aborted'
|
|
722
|
+
throw error
|
|
723
|
+
}
|
|
724
|
+
if ((error as Error).message === 'Max steps exceeded') {
|
|
725
|
+
throw error
|
|
726
|
+
}
|
|
727
|
+
stopReason = 'error'
|
|
728
|
+
throw error
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
text: finalText,
|
|
733
|
+
steps,
|
|
734
|
+
toolCalls: allToolCalls,
|
|
735
|
+
toolResults: allToolResults,
|
|
736
|
+
stopReason,
|
|
737
|
+
...(totalUsage !== undefined && { usage: totalUsage }),
|
|
738
|
+
messages,
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Run the agentic loop with streaming support
|
|
744
|
+
*
|
|
745
|
+
* Returns an async generator that yields step events as they occur.
|
|
746
|
+
*/
|
|
747
|
+
async *stream(runOptions: RunOptions): AsyncGenerator<LoopStreamEvent, LoopResult> {
|
|
748
|
+
const { model, prompt, system, abortSignal } = runOptions
|
|
749
|
+
const { maxSteps, strictMaxSteps, continueOnError, trackUsage } = this.options
|
|
750
|
+
|
|
751
|
+
const allToolCalls: ToolCallResult[] = []
|
|
752
|
+
const allToolResults: SDKToolResult[] = []
|
|
753
|
+
const messages: Message[] = [{ role: 'user', content: prompt }]
|
|
754
|
+
let steps = 0
|
|
755
|
+
let stopReason: LoopResult['stopReason'] = 'stop'
|
|
756
|
+
let finalText = ''
|
|
757
|
+
let totalUsage = trackUsage
|
|
758
|
+
? { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
759
|
+
: undefined
|
|
760
|
+
|
|
761
|
+
yield { type: 'start', prompt, timestamp: Date.now() }
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
while (steps < maxSteps) {
|
|
765
|
+
if (abortSignal?.aborted) {
|
|
766
|
+
yield { type: 'aborted', steps, timestamp: Date.now() }
|
|
767
|
+
throw new Error('Aborted')
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
steps++
|
|
771
|
+
yield { type: 'step_start', stepNumber: steps, timestamp: Date.now() }
|
|
772
|
+
|
|
773
|
+
const response = await model.generate({
|
|
774
|
+
messages: this.buildMessages(prompt, system, messages.slice(1), []),
|
|
775
|
+
tools: this.getToolsForSDK(),
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
if (trackUsage && response.usage) {
|
|
779
|
+
totalUsage!.promptTokens += response.usage.promptTokens
|
|
780
|
+
totalUsage!.completionTokens += response.usage.completionTokens
|
|
781
|
+
totalUsage!.totalTokens += response.usage.totalTokens
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
785
|
+
finalText = response.text || ''
|
|
786
|
+
messages.push({ role: 'assistant', content: finalText })
|
|
787
|
+
yield { type: 'text', text: finalText, stepNumber: steps, timestamp: Date.now() }
|
|
788
|
+
yield { type: 'step_end', stepNumber: steps, hasToolCalls: false, timestamp: Date.now() }
|
|
789
|
+
break
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
yield {
|
|
793
|
+
type: 'tool_calls',
|
|
794
|
+
toolCalls: response.toolCalls,
|
|
795
|
+
stepNumber: steps,
|
|
796
|
+
timestamp: Date.now(),
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const toolResults = await this.executeToolCalls(response.toolCalls, abortSignal)
|
|
800
|
+
|
|
801
|
+
for (const result of toolResults) {
|
|
802
|
+
allToolCalls.push(result)
|
|
803
|
+
allToolResults.push({ toolName: result.name, result: result.result })
|
|
804
|
+
|
|
805
|
+
yield {
|
|
806
|
+
type: 'tool_result',
|
|
807
|
+
toolName: result.name,
|
|
808
|
+
...(result.result !== undefined && { result: result.result }),
|
|
809
|
+
...(result.error !== undefined && { error: result.error }),
|
|
810
|
+
stepNumber: steps,
|
|
811
|
+
timestamp: Date.now(),
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (result.error) {
|
|
815
|
+
messages.push({
|
|
816
|
+
role: 'tool',
|
|
817
|
+
content: JSON.stringify({ error: result.error }),
|
|
818
|
+
isError: true,
|
|
819
|
+
})
|
|
820
|
+
} else {
|
|
821
|
+
messages.push({
|
|
822
|
+
role: 'tool',
|
|
823
|
+
content: JSON.stringify(result.result),
|
|
824
|
+
})
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
yield { type: 'step_end', stepNumber: steps, hasToolCalls: true, timestamp: Date.now() }
|
|
829
|
+
|
|
830
|
+
messages.push({
|
|
831
|
+
role: 'assistant',
|
|
832
|
+
content: '',
|
|
833
|
+
tool_calls: response.toolCalls,
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (steps >= maxSteps && stopReason === 'stop') {
|
|
838
|
+
stopReason = 'max_steps'
|
|
839
|
+
yield { type: 'max_steps', steps, timestamp: Date.now() }
|
|
840
|
+
if (strictMaxSteps) throw new Error('Max steps exceeded')
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if ((error as Error).message === 'Aborted') {
|
|
844
|
+
stopReason = 'aborted'
|
|
845
|
+
throw error
|
|
846
|
+
}
|
|
847
|
+
if ((error as Error).message === 'Max steps exceeded') {
|
|
848
|
+
throw error
|
|
849
|
+
}
|
|
850
|
+
yield { type: 'error', error: (error as Error).message, timestamp: Date.now() }
|
|
851
|
+
stopReason = 'error'
|
|
852
|
+
throw error
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
yield { type: 'end', steps, stopReason, timestamp: Date.now() }
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
text: finalText,
|
|
859
|
+
steps,
|
|
860
|
+
toolCalls: allToolCalls,
|
|
861
|
+
toolResults: allToolResults,
|
|
862
|
+
stopReason,
|
|
863
|
+
...(totalUsage !== undefined && { usage: totalUsage }),
|
|
864
|
+
messages,
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ============================================================================
|
|
870
|
+
// Streaming Types
|
|
871
|
+
// ============================================================================
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Events emitted during streaming loop execution
|
|
875
|
+
*/
|
|
876
|
+
export type LoopStreamEvent =
|
|
877
|
+
| { type: 'start'; prompt: string; timestamp: number }
|
|
878
|
+
| { type: 'step_start'; stepNumber: number; timestamp: number }
|
|
879
|
+
| { type: 'step_end'; stepNumber: number; hasToolCalls: boolean; timestamp: number }
|
|
880
|
+
| { type: 'text'; text: string; stepNumber: number; timestamp: number }
|
|
881
|
+
| { type: 'tool_calls'; toolCalls: ToolCall[]; stepNumber: number; timestamp: number }
|
|
882
|
+
| {
|
|
883
|
+
type: 'tool_result'
|
|
884
|
+
toolName: string
|
|
885
|
+
result?: unknown
|
|
886
|
+
error?: string
|
|
887
|
+
stepNumber: number
|
|
888
|
+
timestamp: number
|
|
889
|
+
}
|
|
890
|
+
| { type: 'max_steps'; steps: number; timestamp: number }
|
|
891
|
+
| { type: 'aborted'; steps: number; timestamp: number }
|
|
892
|
+
| { type: 'error'; error: string; timestamp: number }
|
|
893
|
+
| { type: 'end'; steps: number; stopReason: LoopResult['stopReason']; timestamp: number }
|
|
894
|
+
|
|
895
|
+
// ============================================================================
|
|
896
|
+
// Tool Composition Patterns
|
|
897
|
+
// ============================================================================
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Create a tool from a simple function
|
|
901
|
+
*/
|
|
902
|
+
export function createTool<TParams extends z.ZodRawShape, TResult>(config: {
|
|
903
|
+
name: string
|
|
904
|
+
description: string
|
|
905
|
+
parameters: TParams
|
|
906
|
+
execute: (params: z.infer<z.ZodObject<TParams>>) => Promise<TResult>
|
|
907
|
+
}): Tool<z.ZodObject<TParams>, TResult> {
|
|
908
|
+
return {
|
|
909
|
+
name: config.name,
|
|
910
|
+
description: config.description,
|
|
911
|
+
parameters: z.object(config.parameters),
|
|
912
|
+
execute: config.execute,
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Compose multiple tools into a single toolset
|
|
918
|
+
*/
|
|
919
|
+
export function createToolset(...tools: Tool[]): Tool[] {
|
|
920
|
+
return tools
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Create a tool that wraps another tool with middleware
|
|
925
|
+
*/
|
|
926
|
+
export function wrapTool<T extends Tool>(
|
|
927
|
+
tool: T,
|
|
928
|
+
middleware: {
|
|
929
|
+
before?: (params: unknown) => Promise<unknown> | unknown
|
|
930
|
+
after?: (result: unknown) => Promise<unknown> | unknown
|
|
931
|
+
onError?: (error: Error) => Promise<unknown> | unknown
|
|
932
|
+
}
|
|
933
|
+
): Tool {
|
|
934
|
+
return {
|
|
935
|
+
...tool,
|
|
936
|
+
execute: async (params: unknown) => {
|
|
937
|
+
try {
|
|
938
|
+
const modifiedParams = middleware.before ? await middleware.before(params) : params
|
|
939
|
+
const result = await tool.execute(modifiedParams)
|
|
940
|
+
return middleware.after ? await middleware.after(result) : result
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (middleware.onError) {
|
|
943
|
+
return middleware.onError(error as Error)
|
|
944
|
+
}
|
|
945
|
+
throw error
|
|
946
|
+
}
|
|
947
|
+
},
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Options for cachedTool
|
|
953
|
+
*/
|
|
954
|
+
export interface CachedToolOptions {
|
|
955
|
+
/** Time-to-live in milliseconds (default: 60000) */
|
|
956
|
+
ttl?: number
|
|
957
|
+
/** Function to generate cache key from params (default: JSON.stringify) */
|
|
958
|
+
keyFn?: (params: unknown) => string
|
|
959
|
+
/** Interval in ms for automatic cleanup of expired entries (default: 0 = disabled) */
|
|
960
|
+
cleanupIntervalMs?: number
|
|
961
|
+
/** Maximum cache size before LRU eviction kicks in (default: 0 = unlimited) */
|
|
962
|
+
maxSize?: number
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Extended tool interface with cache management methods
|
|
967
|
+
*/
|
|
968
|
+
export interface CachedTool extends Tool {
|
|
969
|
+
/** Get the current number of entries in the cache */
|
|
970
|
+
cacheSize(): number
|
|
971
|
+
/** Clear all cache entries */
|
|
972
|
+
clearCache(): void
|
|
973
|
+
/** Stop cleanup timer and clear cache */
|
|
974
|
+
destroy(): void
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Create a tool with caching support
|
|
979
|
+
*
|
|
980
|
+
* Features:
|
|
981
|
+
* - TTL-based expiration
|
|
982
|
+
* - Optional periodic cleanup of expired entries (prevents memory leaks)
|
|
983
|
+
* - Optional max size with LRU eviction
|
|
984
|
+
* - Manual cache control (clear, destroy)
|
|
985
|
+
*/
|
|
986
|
+
export function cachedTool<T extends Tool>(tool: T, options: CachedToolOptions = {}): CachedTool {
|
|
987
|
+
const { ttl = 60000, keyFn = JSON.stringify, cleanupIntervalMs = 0, maxSize = 0 } = options
|
|
988
|
+
|
|
989
|
+
interface CacheEntry {
|
|
990
|
+
value: unknown
|
|
991
|
+
expires: number
|
|
992
|
+
lastAccessed: number
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const cache = new Map<string, CacheEntry>()
|
|
996
|
+
let cleanupTimer: ReturnType<typeof setInterval> | null = null
|
|
997
|
+
let destroyed = false
|
|
998
|
+
|
|
999
|
+
// Cleanup function to remove expired entries
|
|
1000
|
+
const cleanupExpired = () => {
|
|
1001
|
+
const now = Date.now()
|
|
1002
|
+
for (const [key, entry] of cache) {
|
|
1003
|
+
if (entry.expires <= now) {
|
|
1004
|
+
cache.delete(key)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Start periodic cleanup if configured
|
|
1010
|
+
if (cleanupIntervalMs > 0) {
|
|
1011
|
+
cleanupTimer = setInterval(cleanupExpired, cleanupIntervalMs)
|
|
1012
|
+
// Unref the timer so it doesn't keep the process alive (Node.js)
|
|
1013
|
+
if (typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {
|
|
1014
|
+
cleanupTimer.unref()
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Evict oldest entries based on lastAccessed (LRU)
|
|
1019
|
+
const evictOldest = () => {
|
|
1020
|
+
if (maxSize <= 0 || cache.size < maxSize) return
|
|
1021
|
+
|
|
1022
|
+
// Find the entry with oldest lastAccessed
|
|
1023
|
+
let oldestKey: string | null = null
|
|
1024
|
+
let oldestTime = Infinity
|
|
1025
|
+
|
|
1026
|
+
for (const [key, entry] of cache) {
|
|
1027
|
+
if (entry.lastAccessed < oldestTime) {
|
|
1028
|
+
oldestTime = entry.lastAccessed
|
|
1029
|
+
oldestKey = key
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (oldestKey) {
|
|
1034
|
+
cache.delete(oldestKey)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const cachedToolInstance: CachedTool = {
|
|
1039
|
+
...tool,
|
|
1040
|
+
execute: async (params: unknown) => {
|
|
1041
|
+
if (destroyed) {
|
|
1042
|
+
// If destroyed, just execute without caching
|
|
1043
|
+
return tool.execute(params)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const key = keyFn(params)
|
|
1047
|
+
const cached = cache.get(key)
|
|
1048
|
+
const now = Date.now()
|
|
1049
|
+
|
|
1050
|
+
if (cached && cached.expires > now) {
|
|
1051
|
+
// Cache hit - update last accessed time for LRU
|
|
1052
|
+
cached.lastAccessed = now
|
|
1053
|
+
return cached.value
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Cache miss or expired - remove expired entry if present
|
|
1057
|
+
if (cached) {
|
|
1058
|
+
cache.delete(key)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const result = await tool.execute(params)
|
|
1062
|
+
|
|
1063
|
+
// Evict oldest if we're at max size
|
|
1064
|
+
if (maxSize > 0 && cache.size >= maxSize) {
|
|
1065
|
+
evictOldest()
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
cache.set(key, {
|
|
1069
|
+
value: result,
|
|
1070
|
+
expires: now + ttl,
|
|
1071
|
+
lastAccessed: now,
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
return result
|
|
1075
|
+
},
|
|
1076
|
+
|
|
1077
|
+
cacheSize(): number {
|
|
1078
|
+
return cache.size
|
|
1079
|
+
},
|
|
1080
|
+
|
|
1081
|
+
clearCache(): void {
|
|
1082
|
+
cache.clear()
|
|
1083
|
+
},
|
|
1084
|
+
|
|
1085
|
+
destroy(): void {
|
|
1086
|
+
destroyed = true
|
|
1087
|
+
if (cleanupTimer !== null) {
|
|
1088
|
+
clearInterval(cleanupTimer)
|
|
1089
|
+
cleanupTimer = null
|
|
1090
|
+
}
|
|
1091
|
+
cache.clear()
|
|
1092
|
+
},
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return cachedToolInstance
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Create a tool with rate limiting
|
|
1100
|
+
*/
|
|
1101
|
+
export function rateLimitedTool<T extends Tool>(
|
|
1102
|
+
tool: T,
|
|
1103
|
+
options: {
|
|
1104
|
+
maxCalls: number
|
|
1105
|
+
windowMs: number
|
|
1106
|
+
}
|
|
1107
|
+
): Tool {
|
|
1108
|
+
const calls: number[] = []
|
|
1109
|
+
const { maxCalls, windowMs } = options
|
|
1110
|
+
|
|
1111
|
+
return {
|
|
1112
|
+
...tool,
|
|
1113
|
+
execute: async (params: unknown) => {
|
|
1114
|
+
const now = Date.now()
|
|
1115
|
+
// Remove expired calls
|
|
1116
|
+
while (calls.length > 0 && calls[0]! < now - windowMs) {
|
|
1117
|
+
calls.shift()
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (calls.length >= maxCalls) {
|
|
1121
|
+
throw new Error(`Rate limit exceeded: max ${maxCalls} calls per ${windowMs}ms`)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
calls.push(now)
|
|
1125
|
+
return tool.execute(params)
|
|
1126
|
+
},
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Create a tool that times out after a specified duration
|
|
1132
|
+
*/
|
|
1133
|
+
export function timeoutTool<T extends Tool>(tool: T, timeoutMs: number): Tool {
|
|
1134
|
+
return {
|
|
1135
|
+
...tool,
|
|
1136
|
+
execute: async (params: unknown) => {
|
|
1137
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
1138
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
1139
|
+
timeoutId = setTimeout(
|
|
1140
|
+
() => reject(new Error(`Tool '${tool.name}' timed out after ${timeoutMs}ms`)),
|
|
1141
|
+
timeoutMs
|
|
1142
|
+
)
|
|
1143
|
+
})
|
|
1144
|
+
try {
|
|
1145
|
+
return await Promise.race([tool.execute(params), timeoutPromise])
|
|
1146
|
+
} finally {
|
|
1147
|
+
if (timeoutId !== undefined) {
|
|
1148
|
+
clearTimeout(timeoutId)
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Create an agentic loop with sensible defaults
|
|
1157
|
+
*
|
|
1158
|
+
* @deprecated Phase C Week 2 — `createAgenticLoop` has zero production
|
|
1159
|
+
* callers in primitives.org.ai (only `ai-primitives` umbrella re-export
|
|
1160
|
+
* tests). Use AI SDK 6's `Agent` / `ToolLoopAgent` with
|
|
1161
|
+
* `stopWhen: stepCountIs(N)` instead. Will be removed alongside
|
|
1162
|
+
* `AgenticLoop` in the Phase C semver bump. See `bd show aip-ibid`.
|
|
1163
|
+
*/
|
|
1164
|
+
export function createAgenticLoop(options: Partial<LoopOptions> & { tools: Tool[] }): AgenticLoop {
|
|
1165
|
+
return new AgenticLoop({
|
|
1166
|
+
maxSteps: 10,
|
|
1167
|
+
parallelExecution: true,
|
|
1168
|
+
maxParallelCalls: 5,
|
|
1169
|
+
continueOnError: true,
|
|
1170
|
+
trackUsage: true,
|
|
1171
|
+
...options,
|
|
1172
|
+
})
|
|
1173
|
+
}
|