@rudderjs/ai 1.4.0 → 1.6.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/README.md +484 -7
- package/boost/guidelines.md +62 -2
- package/boost/skills/ai-tools/SKILL.md +14 -5
- package/dist/agent.d.ts +66 -15
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +529 -58
- package/dist/agent.js.map +1 -1
- package/dist/budget/pricing.d.ts +124 -0
- package/dist/budget/pricing.d.ts.map +1 -0
- package/dist/budget/pricing.js +175 -0
- package/dist/budget/pricing.js.map +1 -0
- package/dist/budget/storage.d.ts +104 -0
- package/dist/budget/storage.d.ts.map +1 -0
- package/dist/budget/storage.js +0 -0
- package/dist/budget/storage.js.map +1 -0
- package/dist/budget/with-budget.d.ts +119 -0
- package/dist/budget/with-budget.d.ts.map +1 -0
- package/dist/budget/with-budget.js +175 -0
- package/dist/budget/with-budget.js.map +1 -0
- package/dist/budget-orm/index.d.ts +96 -0
- package/dist/budget-orm/index.d.ts.map +1 -0
- package/dist/budget-orm/index.js +177 -0
- package/dist/budget-orm/index.js.map +1 -0
- package/dist/commands/ai-eval.d.ts +93 -0
- package/dist/commands/ai-eval.d.ts.map +1 -0
- package/dist/commands/ai-eval.js +378 -0
- package/dist/commands/ai-eval.js.map +1 -0
- package/dist/computer-use/actions.d.ts +214 -0
- package/dist/computer-use/actions.d.ts.map +1 -0
- package/dist/computer-use/actions.js +48 -0
- package/dist/computer-use/actions.js.map +1 -0
- package/dist/computer-use/errors.d.ts +57 -0
- package/dist/computer-use/errors.d.ts.map +1 -0
- package/dist/computer-use/errors.js +76 -0
- package/dist/computer-use/errors.js.map +1 -0
- package/dist/computer-use/index.d.ts +53 -0
- package/dist/computer-use/index.d.ts.map +1 -0
- package/dist/computer-use/index.js +51 -0
- package/dist/computer-use/index.js.map +1 -0
- package/dist/computer-use/playwright.d.ts +76 -0
- package/dist/computer-use/playwright.d.ts.map +1 -0
- package/dist/computer-use/playwright.js +270 -0
- package/dist/computer-use/playwright.js.map +1 -0
- package/dist/computer-use/tool.d.ts +154 -0
- package/dist/computer-use/tool.d.ts.map +1 -0
- package/dist/computer-use/tool.js +210 -0
- package/dist/computer-use/tool.js.map +1 -0
- package/dist/eval/fixtures.d.ts +65 -0
- package/dist/eval/fixtures.d.ts.map +1 -0
- package/dist/eval/fixtures.js +110 -0
- package/dist/eval/fixtures.js.map +1 -0
- package/dist/eval/html-reporter.d.ts +25 -0
- package/dist/eval/html-reporter.d.ts.map +1 -0
- package/dist/eval/html-reporter.js +209 -0
- package/dist/eval/html-reporter.js.map +1 -0
- package/dist/eval/index.d.ts +271 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +510 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/json-reporter.d.ts +43 -0
- package/dist/eval/json-reporter.d.ts.map +1 -0
- package/dist/eval/json-reporter.js +40 -0
- package/dist/eval/json-reporter.js.map +1 -0
- package/dist/fake.d.ts +36 -1
- package/dist/fake.d.ts.map +1 -1
- package/dist/fake.js +49 -2
- package/dist/fake.js.map +1 -1
- package/dist/file-search.d.ts +168 -0
- package/dist/file-search.d.ts.map +1 -0
- package/dist/file-search.js +158 -0
- package/dist/file-search.js.map +1 -0
- package/dist/handoff.d.ts +95 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +78 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +29 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/client-tools.d.ts +39 -0
- package/dist/mcp/client-tools.d.ts.map +1 -0
- package/dist/mcp/client-tools.js +147 -0
- package/dist/mcp/client-tools.js.map +1 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server-from-agent.d.ts +24 -0
- package/dist/mcp/server-from-agent.d.ts.map +1 -0
- package/dist/mcp/server-from-agent.js +113 -0
- package/dist/mcp/server-from-agent.js.map +1 -0
- package/dist/mcp/types.d.ts +64 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +6 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory-embedding/index.d.ts +121 -0
- package/dist/memory-embedding/index.d.ts.map +1 -0
- package/dist/memory-embedding/index.js +229 -0
- package/dist/memory-embedding/index.js.map +1 -0
- package/dist/memory-extract.d.ts +60 -0
- package/dist/memory-extract.d.ts.map +1 -0
- package/dist/memory-extract.js +163 -0
- package/dist/memory-extract.js.map +1 -0
- package/dist/memory-inject.d.ts +39 -0
- package/dist/memory-inject.d.ts.map +1 -0
- package/dist/memory-inject.js +135 -0
- package/dist/memory-inject.js.map +1 -0
- package/dist/memory-orm/index.d.ts +118 -0
- package/dist/memory-orm/index.d.ts.map +1 -0
- package/dist/memory-orm/index.js +187 -0
- package/dist/memory-orm/index.js.map +1 -0
- package/dist/memory.d.ts +55 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +132 -0
- package/dist/memory.js.map +1 -0
- package/dist/observers.d.ts +22 -0
- package/dist/observers.d.ts.map +1 -1
- package/dist/observers.js.map +1 -1
- package/dist/provider-tools.d.ts +15 -1
- package/dist/provider-tools.d.ts.map +1 -1
- package/dist/provider-tools.js +21 -1
- package/dist/provider-tools.js.map +1 -1
- package/dist/providers/anthropic.d.ts +9 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +66 -11
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +60 -0
- package/dist/providers/bedrock.d.ts.map +1 -0
- package/dist/providers/bedrock.js +167 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/elevenlabs.d.ts +98 -0
- package/dist/providers/elevenlabs.d.ts.map +1 -0
- package/dist/providers/elevenlabs.js +229 -0
- package/dist/providers/elevenlabs.js.map +1 -0
- package/dist/providers/google.d.ts +83 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +491 -8
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/openai.d.ts +8 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +215 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +43 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +21 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/voyage.d.ts +91 -0
- package/dist/providers/voyage.d.ts.map +1 -0
- package/dist/providers/voyage.js +166 -0
- package/dist/providers/voyage.js.map +1 -0
- package/dist/queue-job.d.ts +69 -4
- package/dist/queue-job.d.ts.map +1 -1
- package/dist/queue-job.js +114 -11
- package/dist/queue-job.js.map +1 -1
- package/dist/registry.d.ts +3 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +10 -0
- package/dist/registry.js.map +1 -1
- package/dist/server/provider.d.ts.map +1 -1
- package/dist/server/provider.js +38 -1
- package/dist/server/provider.js.map +1 -1
- package/dist/similarity-search.d.ts +163 -0
- package/dist/similarity-search.d.ts.map +1 -0
- package/dist/similarity-search.js +147 -0
- package/dist/similarity-search.js.map +1 -0
- package/dist/sub-agent-run-store.d.ts +40 -3
- package/dist/sub-agent-run-store.d.ts.map +1 -1
- package/dist/sub-agent-run-store.js.map +1 -1
- package/dist/tool.d.ts +59 -0
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +45 -4
- package/dist/tool.js.map +1 -1
- package/dist/types.d.ts +285 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-stores/index.d.ts +96 -0
- package/dist/vector-stores/index.d.ts.map +1 -0
- package/dist/vector-stores/index.js +153 -0
- package/dist/vector-stores/index.js.map +1 -0
- package/package.json +43 -4
package/dist/agent.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { AiRegistry } from './registry.js';
|
|
3
|
-
import { isPauseForClientToolsChunk, pauseForClientTools, toolDefinition, toolToSchema } from './tool.js';
|
|
3
|
+
import { isPauseForApprovalChunk, isPauseForClientToolsChunk, pauseForApproval, pauseForClientTools, toolDefinition, toolToSchema } from './tool.js';
|
|
4
|
+
import { isHandoffTool } from './handoff.js';
|
|
4
5
|
import { attachmentsToContentParts, getMessageText } from './attachment.js';
|
|
5
6
|
import { QueuedPromptBuilder } from './queue-job.js';
|
|
6
7
|
import { resolveAutoPersistSpec, runWithPersistence, runWithPersistenceStreaming, } from './conversation-persistence.js';
|
|
8
|
+
import { resolveRemembersSpec } from './memory.js';
|
|
9
|
+
import { withMemoryInject } from './memory-inject.js';
|
|
10
|
+
import { withMemoryExtract } from './memory-extract.js';
|
|
7
11
|
import { runOnConfig, runOnChunk, runOnBeforeToolCall, runOnAfterToolCall, runSequential, runOnUsage, runOnAbort, runOnError, } from './middleware.js';
|
|
8
12
|
// ─── AI Observer (lazy accessor) ─────────────────────────
|
|
9
13
|
function _getAiObservers() {
|
|
@@ -110,6 +114,33 @@ export class Agent {
|
|
|
110
114
|
conversational() {
|
|
111
115
|
return false;
|
|
112
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Opt this agent class into per-user memory beyond conversation history
|
|
119
|
+
* (#A4). Returns a {@link RemembersSpec} naming the user whose memory
|
|
120
|
+
* the agent reads/writes, and how injection / extraction should behave.
|
|
121
|
+
* Returning `false` (the default) leaves the agent memory-stateless.
|
|
122
|
+
*
|
|
123
|
+
* Phase 1 wires the declaration + the per-call precedence chain so
|
|
124
|
+
* apps and downstream phases (auto-inject middleware in Phase 2,
|
|
125
|
+
* auto-extract middleware in Phase 3) can read a consistent spec.
|
|
126
|
+
* Calling this method directly today produces no runtime behavior
|
|
127
|
+
* unless application code reads it via `resolveRemembersSpec()`.
|
|
128
|
+
*
|
|
129
|
+
* **Precedence (high → low):**
|
|
130
|
+
* 1. Per-call `prompt(input, { memory: false | {...} })`
|
|
131
|
+
* 2. This method's return value
|
|
132
|
+
*
|
|
133
|
+
* Async returns are supported — useful when the user identity is fetched
|
|
134
|
+
* from an async DI binding.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* class SupportAgent extends Agent {
|
|
138
|
+
* remembers() { return { user: ctx.user.id, inject: 'auto', tags: ['support'] } }
|
|
139
|
+
* }
|
|
140
|
+
*/
|
|
141
|
+
remembers() {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
113
144
|
/**
|
|
114
145
|
* Default for `AgentPromptOptions.parallelTools`. When `true` (default),
|
|
115
146
|
* multiple tool calls within a single step run their `execute()` functions
|
|
@@ -119,11 +150,17 @@ export class Agent {
|
|
|
119
150
|
parallelTools() { return true; }
|
|
120
151
|
/** Run the agent with a prompt (non-streaming) */
|
|
121
152
|
async prompt(input, options) {
|
|
122
|
-
|
|
153
|
+
// Memory auto-cascade — appends inject (Phase 2) + extract (Phase 3)
|
|
154
|
+
// middlewares when `Agent.remembers()` opts in. Runs BEFORE
|
|
155
|
+
// conversation persistence so the persisted history flows in
|
|
156
|
+
// unchanged: inject only grows the system message; extract only
|
|
157
|
+
// fires onFinish.
|
|
158
|
+
const effOptions = await prepareOptionsWithMemoryAutoCascade(this, options);
|
|
159
|
+
const spec = await resolveAutoPersistSpec(() => this.conversational(), effOptions?.conversation);
|
|
123
160
|
if (spec) {
|
|
124
|
-
return runWithPersistence(spec, this.constructor.name, resolveConversationStore, input,
|
|
161
|
+
return runWithPersistence(spec, this.constructor.name, resolveConversationStore, input, effOptions, (innerOptions) => runAgentLoop(this, input, innerOptions));
|
|
125
162
|
}
|
|
126
|
-
return runAgentLoop(this, input,
|
|
163
|
+
return runAgentLoop(this, input, effOptions);
|
|
127
164
|
}
|
|
128
165
|
/** Run the agent with a prompt (streaming) */
|
|
129
166
|
stream(input, options) {
|
|
@@ -184,6 +221,7 @@ export class Agent {
|
|
|
184
221
|
pendingToolCallIds: result.pendingClientToolCalls.map((tc) => tc.id),
|
|
185
222
|
stepsSoFar: result.steps.length,
|
|
186
223
|
tokensSoFar: result.usage?.totalTokens ?? 0,
|
|
224
|
+
pauseKind: 'client_tool',
|
|
187
225
|
};
|
|
188
226
|
await suspendable.runStore.store(subRunId, snapshot);
|
|
189
227
|
yield { kind: 'subagent_paused', subRunId, pendingToolCallIds: snapshot.pendingToolCallIds };
|
|
@@ -191,6 +229,30 @@ export class Agent {
|
|
|
191
229
|
// Unreachable — the parent loop halts iteration after the pause chunk.
|
|
192
230
|
return undefined;
|
|
193
231
|
}
|
|
232
|
+
if (suspendable &&
|
|
233
|
+
result.finishReason === 'tool_approval_required' &&
|
|
234
|
+
result.pendingApprovalToolCall) {
|
|
235
|
+
const subRunId = generateSubRunId();
|
|
236
|
+
const { toolCall: pendingCall, isClientTool } = result.pendingApprovalToolCall;
|
|
237
|
+
const snapshot = {
|
|
238
|
+
messages: buildSubAgentSnapshotMessages(userPrompt, result),
|
|
239
|
+
pendingToolCallIds: [pendingCall.id],
|
|
240
|
+
stepsSoFar: result.steps.length,
|
|
241
|
+
tokensSoFar: result.usage?.totalTokens ?? 0,
|
|
242
|
+
pauseKind: 'approval',
|
|
243
|
+
pendingApprovalToolCall: { toolCall: pendingCall, isClientTool },
|
|
244
|
+
};
|
|
245
|
+
await suspendable.runStore.store(subRunId, snapshot);
|
|
246
|
+
yield {
|
|
247
|
+
kind: 'subagent_paused_approval',
|
|
248
|
+
subRunId,
|
|
249
|
+
toolCall: pendingCall,
|
|
250
|
+
isClientTool,
|
|
251
|
+
};
|
|
252
|
+
yield pauseForApproval(pendingCall, isClientTool, subRunId);
|
|
253
|
+
// Unreachable — the parent loop halts iteration after the pause chunk.
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
194
256
|
yield {
|
|
195
257
|
kind: 'agent_done',
|
|
196
258
|
steps: result.steps.length,
|
|
@@ -207,54 +269,96 @@ export class Agent {
|
|
|
207
269
|
.modelOutput(modelOutput);
|
|
208
270
|
}
|
|
209
271
|
/**
|
|
210
|
-
* Resume a sub-agent run that previously paused with
|
|
211
|
-
* `pauseForClientTools` (
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
272
|
+
* Resume a sub-agent run that previously paused with either
|
|
273
|
+
* `pauseForClientTools` (client-tool pause) or `pauseForApproval`
|
|
274
|
+
* (approval pause), typically from {@link Agent.asTool} with
|
|
275
|
+
* `suspendable: { runStore }` set. The snapshot's `pauseKind`
|
|
276
|
+
* (default `'client_tool'`) selects the resume contract:
|
|
215
277
|
*
|
|
216
|
-
*
|
|
278
|
+
* - **`client_tool`** — `clientToolResults` must carry one entry per
|
|
279
|
+
* id in the snapshot's `pendingToolCallIds`. Results are appended
|
|
280
|
+
* to the inner-agent message history and the loop re-runs.
|
|
281
|
+
* - **`approval`** — `approvedToolCallIds` and/or
|
|
282
|
+
* `rejectedToolCallIds` must reference the single pending id.
|
|
283
|
+
* `clientToolResults` must be empty; the loop re-runs with the
|
|
284
|
+
* approval decision injected via `AgentPromptOptions`.
|
|
285
|
+
*
|
|
286
|
+
* Returns either a `'completed'` result (the inner agent finished),
|
|
217
287
|
* a `'paused'` continuation pointing at a fresh `subRunId` for the
|
|
218
|
-
* next round-trip
|
|
288
|
+
* next round-trip, or stays `'paused'` if the inner loop hits another
|
|
289
|
+
* gate. The resume can pause on a different kind than it started on
|
|
290
|
+
* (e.g. an approval pause that, once approved, hits a client-tool
|
|
291
|
+
* pause on the next step).
|
|
219
292
|
*
|
|
220
|
-
* @example
|
|
293
|
+
* @example Client-tool resume
|
|
221
294
|
* const r = await Agent.resumeAsTool(subRunId, browserResults, { runStore, agent: subAgent })
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
295
|
+
*
|
|
296
|
+
* @example Approval resume
|
|
297
|
+
* const r = await Agent.resumeAsTool(subRunId, [], {
|
|
298
|
+
* runStore, agent: subAgent,
|
|
299
|
+
* approvedToolCallIds: ['inner-call-id'],
|
|
300
|
+
* })
|
|
227
301
|
*/
|
|
228
302
|
static async resumeAsTool(subRunId, clientToolResults, options) {
|
|
229
303
|
const snapshot = await options.runStore.consume(subRunId);
|
|
230
304
|
if (!snapshot) {
|
|
231
305
|
throw new Error(`[RudderJS AI] resumeAsTool: subRunId "${subRunId}" expired or never existed.`);
|
|
232
306
|
}
|
|
233
|
-
|
|
307
|
+
const pauseKind = snapshot.pauseKind ?? 'client_tool';
|
|
234
308
|
const pending = new Set(snapshot.pendingToolCallIds);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
309
|
+
let messages;
|
|
310
|
+
const promptOpts = { toolCallStreamingMode: 'stop-on-client-tool' };
|
|
311
|
+
if (pauseKind === 'client_tool') {
|
|
312
|
+
// Forgery guard — every incoming tool-result id must be in the pending set.
|
|
313
|
+
const seen = new Set();
|
|
314
|
+
for (const r of clientToolResults) {
|
|
315
|
+
if (!pending.has(r.toolCallId)) {
|
|
316
|
+
throw new Error(`[RudderJS AI] resumeAsTool: toolCallId "${r.toolCallId}" was not in the pending set.`);
|
|
317
|
+
}
|
|
318
|
+
if (seen.has(r.toolCallId)) {
|
|
319
|
+
throw new Error(`[RudderJS AI] resumeAsTool: duplicate result for toolCallId "${r.toolCallId}".`);
|
|
320
|
+
}
|
|
321
|
+
seen.add(r.toolCallId);
|
|
239
322
|
}
|
|
240
|
-
|
|
241
|
-
|
|
323
|
+
// Append client tool-result messages to the snapshot, in incoming order.
|
|
324
|
+
messages = [...snapshot.messages];
|
|
325
|
+
for (const r of clientToolResults) {
|
|
326
|
+
messages.push({
|
|
327
|
+
role: 'tool',
|
|
328
|
+
content: typeof r.result === 'string' ? r.result : JSON.stringify(r.result),
|
|
329
|
+
toolCallId: r.toolCallId,
|
|
330
|
+
});
|
|
242
331
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Approval-pause resume — clientToolResults must be empty; either an
|
|
335
|
+
// approval or a rejection must be supplied for the pending id.
|
|
336
|
+
if (clientToolResults.length > 0) {
|
|
337
|
+
throw new Error('[RudderJS AI] resumeAsTool: snapshot.pauseKind === "approval" but clientToolResults was non-empty. Pass `approvedToolCallIds` or `rejectedToolCallIds` instead.');
|
|
338
|
+
}
|
|
339
|
+
const approved = options.approvedToolCallIds ?? [];
|
|
340
|
+
const rejected = options.rejectedToolCallIds ?? [];
|
|
341
|
+
for (const id of approved) {
|
|
342
|
+
if (!pending.has(id)) {
|
|
343
|
+
throw new Error(`[RudderJS AI] resumeAsTool: approvedToolCallId "${id}" was not in the pending set.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const id of rejected) {
|
|
347
|
+
if (!pending.has(id)) {
|
|
348
|
+
throw new Error(`[RudderJS AI] resumeAsTool: rejectedToolCallId "${id}" was not in the pending set.`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (approved.length === 0 && rejected.length === 0) {
|
|
352
|
+
throw new Error('[RudderJS AI] resumeAsTool: snapshot.pauseKind === "approval" requires `approvedToolCallIds` or `rejectedToolCallIds`.');
|
|
353
|
+
}
|
|
354
|
+
messages = [...snapshot.messages];
|
|
355
|
+
if (approved.length > 0)
|
|
356
|
+
promptOpts.approvedToolCallIds = approved;
|
|
357
|
+
if (rejected.length > 0)
|
|
358
|
+
promptOpts.rejectedToolCallIds = rejected;
|
|
359
|
+
}
|
|
360
|
+
promptOpts.messages = messages;
|
|
361
|
+
const result = await options.agent.prompt('', promptOpts);
|
|
258
362
|
if (result.finishReason === 'client_tool_calls' &&
|
|
259
363
|
result.pendingClientToolCalls?.length) {
|
|
260
364
|
const newSubRunId = generateSubRunId();
|
|
@@ -263,13 +367,38 @@ export class Agent {
|
|
|
263
367
|
pendingToolCallIds: result.pendingClientToolCalls.map((tc) => tc.id),
|
|
264
368
|
stepsSoFar: snapshot.stepsSoFar + result.steps.length,
|
|
265
369
|
tokensSoFar: snapshot.tokensSoFar + (result.usage?.totalTokens ?? 0),
|
|
370
|
+
pauseKind: 'client_tool',
|
|
371
|
+
...(snapshot.meta !== undefined ? { meta: snapshot.meta } : {}),
|
|
372
|
+
};
|
|
373
|
+
await options.runStore.store(newSubRunId, newSnapshot);
|
|
374
|
+
return {
|
|
375
|
+
kind: 'paused',
|
|
376
|
+
subRunId: newSubRunId,
|
|
377
|
+
pauseKind: 'client_tool',
|
|
378
|
+
pendingToolCallIds: newSnapshot.pendingToolCallIds,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (result.finishReason === 'tool_approval_required' &&
|
|
382
|
+
result.pendingApprovalToolCall) {
|
|
383
|
+
const newSubRunId = generateSubRunId();
|
|
384
|
+
const { toolCall: pendingCall, isClientTool } = result.pendingApprovalToolCall;
|
|
385
|
+
const newSnapshot = {
|
|
386
|
+
messages: buildResumeSnapshotMessages(messages, result),
|
|
387
|
+
pendingToolCallIds: [pendingCall.id],
|
|
388
|
+
stepsSoFar: snapshot.stepsSoFar + result.steps.length,
|
|
389
|
+
tokensSoFar: snapshot.tokensSoFar + (result.usage?.totalTokens ?? 0),
|
|
390
|
+
pauseKind: 'approval',
|
|
391
|
+
pendingApprovalToolCall: { toolCall: pendingCall, isClientTool },
|
|
266
392
|
...(snapshot.meta !== undefined ? { meta: snapshot.meta } : {}),
|
|
267
393
|
};
|
|
268
394
|
await options.runStore.store(newSubRunId, newSnapshot);
|
|
269
395
|
return {
|
|
270
396
|
kind: 'paused',
|
|
271
397
|
subRunId: newSubRunId,
|
|
398
|
+
pauseKind: 'approval',
|
|
272
399
|
pendingToolCallIds: newSnapshot.pendingToolCallIds,
|
|
400
|
+
toolCall: pendingCall,
|
|
401
|
+
isClientTool,
|
|
273
402
|
};
|
|
274
403
|
}
|
|
275
404
|
return { kind: 'completed', response: result };
|
|
@@ -277,9 +406,11 @@ export class Agent {
|
|
|
277
406
|
}
|
|
278
407
|
/**
|
|
279
408
|
* Default projection from inner-agent stream chunks to {@link SubAgentUpdate}
|
|
280
|
-
* events. Emits one `tool_call` per inner `tool-call` chunk
|
|
409
|
+
* events. Emits one `tool_call` per inner `tool-call` chunk and
|
|
410
|
+
* `agent_pending_approval` per inner `pending-approval` chunk; everything
|
|
281
411
|
* else is suppressed (the wrapping execute emits the `agent_start` /
|
|
282
|
-
* `agent_done` bookends and the suspend
|
|
412
|
+
* `agent_done` bookends and the suspend paths emit `subagent_paused` /
|
|
413
|
+
* `subagent_paused_approval`).
|
|
283
414
|
*
|
|
284
415
|
* Hosts wanting different cadence (e.g. surfacing `text-delta` previews
|
|
285
416
|
* or per-step usage) pass `streaming: chunk => …` and own the discriminator.
|
|
@@ -292,6 +423,13 @@ function defaultSubAgentProjector(chunk) {
|
|
|
292
423
|
...(chunk.toolCall.arguments ? { args: chunk.toolCall.arguments } : {}),
|
|
293
424
|
};
|
|
294
425
|
}
|
|
426
|
+
if (chunk.type === 'pending-approval' && chunk.toolCall && chunk.toolCall.id && chunk.toolCall.name) {
|
|
427
|
+
return {
|
|
428
|
+
kind: 'agent_pending_approval',
|
|
429
|
+
toolCall: chunk.toolCall,
|
|
430
|
+
isClientTool: !!chunk.isClientTool,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
295
433
|
return null;
|
|
296
434
|
}
|
|
297
435
|
/**
|
|
@@ -437,6 +575,21 @@ export function setConversationStore(store) {
|
|
|
437
575
|
function resolveConversationStore() {
|
|
438
576
|
return _conversationStore;
|
|
439
577
|
}
|
|
578
|
+
// ─── User Memory Registry (#A4) ──────────────────────────
|
|
579
|
+
let _userMemory;
|
|
580
|
+
/**
|
|
581
|
+
* Set the global {@link UserMemory} (called by `AiProvider` from
|
|
582
|
+
* `AiConfig.memory`, or manually for tests / standalone setups).
|
|
583
|
+
* Phase 2/3 middleware reads it via `resolveUserMemory()` —
|
|
584
|
+
* imported by the persistence layer the same way
|
|
585
|
+
* `resolveConversationStore` is wired today.
|
|
586
|
+
*/
|
|
587
|
+
export function setUserMemory(memory) {
|
|
588
|
+
_userMemory = memory;
|
|
589
|
+
}
|
|
590
|
+
export function resolveUserMemory() {
|
|
591
|
+
return _userMemory;
|
|
592
|
+
}
|
|
440
593
|
/**
|
|
441
594
|
* Streaming counterpart of `Agent.prompt`'s auto-persist branch. The spec
|
|
442
595
|
* resolution is async (since `conversational()` may return a Promise), so
|
|
@@ -445,30 +598,38 @@ function resolveConversationStore() {
|
|
|
445
598
|
* persisted path.
|
|
446
599
|
*/
|
|
447
600
|
function runStreamWithMaybeAutoPersist(a, input, options) {
|
|
448
|
-
// Synchronous fast path — most agents
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
601
|
+
// Synchronous fast path — most agents override neither `conversational()`
|
|
602
|
+
// nor `remembers()`. Skip the async outer entirely when we can prove
|
|
603
|
+
// both are no-ops, sparing a microtask boundary per streaming call.
|
|
604
|
+
const declaredConv = a.conversational();
|
|
605
|
+
const declaredMem = a.remembers();
|
|
606
|
+
const isFast = ((options?.conversation === false ||
|
|
607
|
+
(declaredConv === false && options?.conversation === undefined))
|
|
608
|
+
&& (options?.memory === false ||
|
|
609
|
+
(declaredMem === false && options?.memory === undefined) ||
|
|
610
|
+
options?.messages !== undefined));
|
|
454
611
|
if (isFast) {
|
|
455
612
|
return runAgentLoopStreaming(a, input, options);
|
|
456
613
|
}
|
|
457
|
-
// Async path — resolve
|
|
614
|
+
// Async path — resolve memory + conversation specs, then dispatch.
|
|
458
615
|
let resolveResp;
|
|
459
616
|
let rejectResp;
|
|
460
617
|
const responsePromise = new Promise((res, rej) => { resolveResp = res; rejectResp = rej; });
|
|
461
618
|
async function* outer() {
|
|
619
|
+
let effOptions;
|
|
462
620
|
let spec;
|
|
463
621
|
try {
|
|
464
|
-
|
|
622
|
+
// Memory auto-cascade BEFORE conversation persistence — same
|
|
623
|
+
// ordering as the non-streaming `Agent.prompt` path.
|
|
624
|
+
effOptions = await prepareOptionsWithMemoryAutoCascade(a, options);
|
|
625
|
+
spec = await resolveAutoPersistSpec(() => a.conversational(), effOptions?.conversation);
|
|
465
626
|
}
|
|
466
627
|
catch (err) {
|
|
467
628
|
rejectResp(err);
|
|
468
629
|
throw err;
|
|
469
630
|
}
|
|
470
631
|
if (!spec) {
|
|
471
|
-
const inner = runAgentLoopStreaming(a, input,
|
|
632
|
+
const inner = runAgentLoopStreaming(a, input, effOptions);
|
|
472
633
|
try {
|
|
473
634
|
for await (const chunk of inner.stream)
|
|
474
635
|
yield chunk;
|
|
@@ -487,7 +648,7 @@ function runStreamWithMaybeAutoPersist(a, input, options) {
|
|
|
487
648
|
}
|
|
488
649
|
return;
|
|
489
650
|
}
|
|
490
|
-
const persisted = runWithPersistenceStreaming(spec, a.constructor.name, resolveConversationStore, input,
|
|
651
|
+
const persisted = runWithPersistenceStreaming(spec, a.constructor.name, resolveConversationStore, input, effOptions, (innerOptions) => runAgentLoopStreaming(a, input, innerOptions));
|
|
491
652
|
try {
|
|
492
653
|
for await (const chunk of persisted.stream)
|
|
493
654
|
yield chunk;
|
|
@@ -513,10 +674,53 @@ function getTools(a) {
|
|
|
513
674
|
? a.tools()
|
|
514
675
|
: [];
|
|
515
676
|
}
|
|
516
|
-
|
|
517
|
-
|
|
677
|
+
/**
|
|
678
|
+
* Internal symbol used to plumb auto-installed middlewares (today:
|
|
679
|
+
* memory-inject; future: budget-tracker, etc.) through the public
|
|
680
|
+
* `AgentPromptOptions` without polluting its surface. Resolution
|
|
681
|
+
* happens at the `Agent.prompt` / `Agent.stream` boundary; the loop
|
|
682
|
+
* just appends them to the user's `agent.middleware()` array.
|
|
683
|
+
*/
|
|
684
|
+
const EXTRA_MIDDLEWARES = Symbol.for('rudderjs.ai.extraMiddlewares');
|
|
685
|
+
function getMiddleware(a, options) {
|
|
686
|
+
const own = 'middleware' in a && typeof a.middleware === 'function'
|
|
518
687
|
? a.middleware()
|
|
519
688
|
: [];
|
|
689
|
+
const extras = options?.[EXTRA_MIDDLEWARES] ?? [];
|
|
690
|
+
return extras.length > 0 ? [...own, ...extras] : own;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Resolve the effective `remembers()` spec and append the appropriate
|
|
694
|
+
* memory middlewares (inject for Phase 2, extract for Phase 3) to the
|
|
695
|
+
* options' hidden extras list. Skips entirely on:
|
|
696
|
+
* - continuation calls (`options.messages` set) — the system message
|
|
697
|
+
* was already augmented on the original `prompt()`, re-injecting
|
|
698
|
+
* would duplicate the block on every tool round-trip; re-extracting
|
|
699
|
+
* would also double-write the same facts on every round-trip.
|
|
700
|
+
* - specs where neither `inject === 'auto'` nor `extract === 'auto'`
|
|
701
|
+
* apply.
|
|
702
|
+
*
|
|
703
|
+
* Returns options unchanged when no auto-cascade is needed so the
|
|
704
|
+
* downstream conversational/loop path sees the original reference.
|
|
705
|
+
*/
|
|
706
|
+
async function prepareOptionsWithMemoryAutoCascade(a, options) {
|
|
707
|
+
if (options?.messages)
|
|
708
|
+
return options;
|
|
709
|
+
const spec = await resolveRemembersSpec(() => a.remembers(), options?.memory);
|
|
710
|
+
if (!spec)
|
|
711
|
+
return options;
|
|
712
|
+
const installed = [];
|
|
713
|
+
if (spec.inject === 'auto')
|
|
714
|
+
installed.push(withMemoryInject(spec));
|
|
715
|
+
if (spec.extract === 'auto' && spec.extractWith)
|
|
716
|
+
installed.push(withMemoryExtract(spec));
|
|
717
|
+
if (installed.length === 0)
|
|
718
|
+
return options;
|
|
719
|
+
const current = options?.[EXTRA_MIDDLEWARES] ?? [];
|
|
720
|
+
return {
|
|
721
|
+
...options,
|
|
722
|
+
[EXTRA_MIDDLEWARES]: [...current, ...installed],
|
|
723
|
+
};
|
|
520
724
|
}
|
|
521
725
|
function createMiddlewareContext(messages, model, tools, iteration) {
|
|
522
726
|
const [provider] = AiRegistry.parseModelString(model);
|
|
@@ -753,6 +957,12 @@ function buildAgentResponse(loopCtx) {
|
|
|
753
957
|
result.pendingApprovalToolCall = loopCtx.pendingApprovalToolCall;
|
|
754
958
|
if (loopCtx.resumedToolMessages.length > 0)
|
|
755
959
|
result.resumedToolMessages = loopCtx.resumedToolMessages;
|
|
960
|
+
// Internal — consumed by the handoff-aware wrapper, then stripped before
|
|
961
|
+
// surfacing to public callers.
|
|
962
|
+
if (loopCtx.pendingHandoff) {
|
|
963
|
+
result._pendingHandoff = loopCtx.pendingHandoff;
|
|
964
|
+
result._carriedMessages = loopCtx.messages;
|
|
965
|
+
}
|
|
756
966
|
return result;
|
|
757
967
|
}
|
|
758
968
|
/**
|
|
@@ -775,7 +985,15 @@ async function* executeToolPhase(loopCtx, toolCalls, assistantMessage) {
|
|
|
775
985
|
// agent-level override which defaults to `true`. Single-tool batches
|
|
776
986
|
// route through the serial path either way (no parallelism to gain, and
|
|
777
987
|
// serial preserves live `tool-update` streaming for that one tool).
|
|
778
|
-
|
|
988
|
+
//
|
|
989
|
+
// Handoffs always force serial dispatch — the parent loop has to halt
|
|
990
|
+
// immediately on the first handoff and synthesize "skipped" results for
|
|
991
|
+
// any sibling calls. Handling that across the parallel classify/replay
|
|
992
|
+
// phases is doable but adds complexity for negligible benefit (the model
|
|
993
|
+
// rarely emits parallel siblings alongside a handoff, and even then,
|
|
994
|
+
// running them while the agent is being torn down is wasted work).
|
|
995
|
+
const hasHandoff = toolCalls.some(tc => isHandoffTool(loopCtx.toolMap.get(tc.name)));
|
|
996
|
+
const parallel = (options?.parallelTools ?? loopCtx.agent.parallelTools()) && toolCalls.length > 1 && !hasHandoff;
|
|
779
997
|
if (parallel) {
|
|
780
998
|
yield* runToolPhaseParallel(loopCtx, toolCalls, toolResults);
|
|
781
999
|
}
|
|
@@ -804,6 +1022,50 @@ async function* runToolPhaseSerial(loopCtx, toolCalls, toolResults) {
|
|
|
804
1022
|
yield { type: 'tool-result', toolCall: tc, result: unknownResult };
|
|
805
1023
|
continue;
|
|
806
1024
|
}
|
|
1025
|
+
// Handoff — detected before the no-execute (client tool) branch because
|
|
1026
|
+
// a handoff tool also has no `execute`, but it has wholly different
|
|
1027
|
+
// semantics: pivot control to a new agent instead of pausing for the
|
|
1028
|
+
// browser. The first handoff in a step wins; any subsequent tool calls
|
|
1029
|
+
// in the same step are skipped with a synthetic "skipped: handed off"
|
|
1030
|
+
// tool result so the message log stays well-formed for replay.
|
|
1031
|
+
if (loopCtx.stopForHandoff) {
|
|
1032
|
+
const skippedResult = 'Skipped: parent agent handed off to another agent.';
|
|
1033
|
+
toolResults.push({ toolCallId: tc.id, result: skippedResult });
|
|
1034
|
+
messages.push({ role: 'tool', content: skippedResult, toolCallId: tc.id });
|
|
1035
|
+
yield { type: 'tool-call', toolCall: tc };
|
|
1036
|
+
yield { type: 'tool-result', toolCall: tc, result: skippedResult };
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (isHandoffTool(tool)) {
|
|
1040
|
+
const spec = tool.__handoffSpec;
|
|
1041
|
+
const validation = validateToolArgs(tool, tc.arguments);
|
|
1042
|
+
// Handoff payload defaults to `{ message: string }`; custom schemas
|
|
1043
|
+
// are accepted but the loop only uses `args.message` (string) as the
|
|
1044
|
+
// transition prompt. Anything else surfaces in the conversation as
|
|
1045
|
+
// the args of the synthetic tool-call.
|
|
1046
|
+
const args = validation.ok ? validation.value : tc.arguments;
|
|
1047
|
+
const transitionMessage = typeof args['message'] === 'string' ? args['message'] : '';
|
|
1048
|
+
const handoffResult = `Handed off to ${spec.AgentClass.name}.`;
|
|
1049
|
+
toolResults.push({ toolCallId: tc.id, result: handoffResult });
|
|
1050
|
+
messages.push({ role: 'tool', content: handoffResult, toolCallId: tc.id });
|
|
1051
|
+
yield { type: 'tool-call', toolCall: tc };
|
|
1052
|
+
yield { type: 'tool-result', toolCall: tc, result: handoffResult };
|
|
1053
|
+
yield {
|
|
1054
|
+
type: 'handoff',
|
|
1055
|
+
handoff: {
|
|
1056
|
+
from: loopCtx.agent.constructor.name,
|
|
1057
|
+
to: spec.AgentClass.name,
|
|
1058
|
+
...(transitionMessage ? { message: transitionMessage } : {}),
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
loopCtx.pendingHandoff = { spec, transitionMessage, parentToolCallId: tc.id };
|
|
1062
|
+
loopCtx.stopForHandoff = true;
|
|
1063
|
+
// Do NOT break — keep iterating so any sibling tool calls in this
|
|
1064
|
+
// step get their synthetic "skipped" tool results before the loop
|
|
1065
|
+
// exits. This preserves message-log invariants for downstream
|
|
1066
|
+
// persistence.
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
807
1069
|
if (!tool.execute) {
|
|
808
1070
|
// Client tool — no server-side handler.
|
|
809
1071
|
if (options?.toolCallStreamingMode === 'stop-on-client-tool') {
|
|
@@ -905,6 +1167,16 @@ async function* runToolPhaseSerial(loopCtx, toolCalls, toolResults) {
|
|
|
905
1167
|
paused = true;
|
|
906
1168
|
break;
|
|
907
1169
|
}
|
|
1170
|
+
if (isPauseForApprovalChunk(step.value)) {
|
|
1171
|
+
loopCtx.pendingApprovalToolCall = {
|
|
1172
|
+
toolCall: step.value.toolCall,
|
|
1173
|
+
isClientTool: step.value.isClientTool,
|
|
1174
|
+
};
|
|
1175
|
+
loopCtx.loopFinishReason = 'tool_approval_required';
|
|
1176
|
+
loopCtx.stopForApproval = true;
|
|
1177
|
+
paused = true;
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
908
1180
|
const updateChunk = { type: 'tool-update', toolCall: tc, update: step.value };
|
|
909
1181
|
if (middlewares.length > 0) {
|
|
910
1182
|
const transformed = runOnChunk(middlewares, ctx, updateChunk);
|
|
@@ -1156,6 +1428,16 @@ async function runToolExecution(loopCtx, outcome) {
|
|
|
1156
1428
|
paused = true;
|
|
1157
1429
|
break;
|
|
1158
1430
|
}
|
|
1431
|
+
if (isPauseForApprovalChunk(step.value)) {
|
|
1432
|
+
loopCtx.pendingApprovalToolCall = {
|
|
1433
|
+
toolCall: step.value.toolCall,
|
|
1434
|
+
isClientTool: step.value.isClientTool,
|
|
1435
|
+
};
|
|
1436
|
+
loopCtx.loopFinishReason = 'tool_approval_required';
|
|
1437
|
+
loopCtx.stopForApproval = true;
|
|
1438
|
+
paused = true;
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1159
1441
|
const updateChunk = { type: 'tool-update', toolCall: outcome.tc, update: step.value };
|
|
1160
1442
|
if (middlewares.length > 0) {
|
|
1161
1443
|
const transformed = runOnChunk(middlewares, ctx, updateChunk);
|
|
@@ -1190,7 +1472,7 @@ async function initializeLoop(a, input, options) {
|
|
|
1190
1472
|
const modelString = a.model() ?? AiRegistry.getDefault();
|
|
1191
1473
|
const [providerName] = AiRegistry.parseModelString(modelString);
|
|
1192
1474
|
const tools = getTools(a);
|
|
1193
|
-
const middlewares = getMiddleware(a);
|
|
1475
|
+
const middlewares = getMiddleware(a, options);
|
|
1194
1476
|
const toolSchemas = buildToolSchemas(tools);
|
|
1195
1477
|
const toolMap = buildToolMap(tools);
|
|
1196
1478
|
const messages = options?.messages
|
|
@@ -1228,6 +1510,7 @@ async function initializeLoop(a, input, options) {
|
|
|
1228
1510
|
stopForApproval: false,
|
|
1229
1511
|
resumedToolMessages: [],
|
|
1230
1512
|
failoverAttempts: 0,
|
|
1513
|
+
stopForHandoff: false,
|
|
1231
1514
|
};
|
|
1232
1515
|
// Resume server tools left pending by a previous approval round-trip.
|
|
1233
1516
|
{
|
|
@@ -1289,7 +1572,195 @@ async function runIterationPrelude(loopCtx, iteration) {
|
|
|
1289
1572
|
return { currentModel };
|
|
1290
1573
|
}
|
|
1291
1574
|
// ─── Agent Loop (non-streaming) ──────────────────────────
|
|
1575
|
+
/**
|
|
1576
|
+
* Hard ceiling for the number of agent-to-agent handoffs in a single
|
|
1577
|
+
* `prompt()` / `stream()` call. Most workflows hop once or twice (triage →
|
|
1578
|
+
* specialist). Anything beyond this almost certainly means the agents are
|
|
1579
|
+
* cycling — surfacing a clear error beats silently looping until token
|
|
1580
|
+
* budgets explode.
|
|
1581
|
+
*/
|
|
1582
|
+
const MAX_HANDOFFS = 5;
|
|
1583
|
+
/**
|
|
1584
|
+
* Public entry point for the non-streaming agent loop. Drives
|
|
1585
|
+
* {@link runAgentLoopOnce} once, then — if the model called a {@link handoff}
|
|
1586
|
+
* tool — constructs the target agent, carries the conversation forward, and
|
|
1587
|
+
* recurses. Steps and usage from each hop are merged; the final `text` and
|
|
1588
|
+
* `finishReason` come from the agent that produced the terminal answer.
|
|
1589
|
+
* `handoffPath` records the chain of class names traversed.
|
|
1590
|
+
*/
|
|
1292
1591
|
async function runAgentLoop(a, input, options) {
|
|
1592
|
+
const onceResult = await runAgentLoopOnce(a, input, options);
|
|
1593
|
+
if (!onceResult._pendingHandoff) {
|
|
1594
|
+
return stripInternal(onceResult);
|
|
1595
|
+
}
|
|
1596
|
+
const merged = await driveHandoffs(a.constructor.name, onceResult, onceResult._pendingHandoff, onceResult._carriedMessages ?? [], options, 0);
|
|
1597
|
+
return merged;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Streaming counterpart to {@link runAgentLoop}. Iterates handoffs and
|
|
1601
|
+
* pivots the stream to the next agent each time the parent ends with a
|
|
1602
|
+
* pending handoff. Chunks from every hop flow through the same returned
|
|
1603
|
+
* `AsyncIterable`; the resolved `response` carries the merged final state.
|
|
1604
|
+
*/
|
|
1605
|
+
function runAgentLoopStreaming(a, input, options) {
|
|
1606
|
+
let resolveResponse;
|
|
1607
|
+
let rejectResponse;
|
|
1608
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
1609
|
+
resolveResponse = resolve;
|
|
1610
|
+
rejectResponse = reject;
|
|
1611
|
+
});
|
|
1612
|
+
async function* generateStream() {
|
|
1613
|
+
let currentAgent = a;
|
|
1614
|
+
let currentInput = input;
|
|
1615
|
+
let currentOpts = options;
|
|
1616
|
+
const mergedSteps = [];
|
|
1617
|
+
const mergedUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1618
|
+
const handoffPath = [];
|
|
1619
|
+
let finalResponse;
|
|
1620
|
+
for (let hop = 0; hop <= MAX_HANDOFFS; hop++) {
|
|
1621
|
+
const onceStream = runAgentLoopStreamingOnce(currentAgent, currentInput, currentOpts);
|
|
1622
|
+
// Attach a no-op handler so a rejection from the inner response
|
|
1623
|
+
// promise (e.g. caller-supplied AbortSignal firing mid-stream) is
|
|
1624
|
+
// already observed by the time the `for await` re-throws — without
|
|
1625
|
+
// this, Node logs an unhandledRejection between the stream's throw
|
|
1626
|
+
// and our outer `withRejectOnError`'s catch.
|
|
1627
|
+
onceStream.response.catch(() => { });
|
|
1628
|
+
for await (const chunk of onceStream.stream)
|
|
1629
|
+
yield chunk;
|
|
1630
|
+
const r = await onceStream.response;
|
|
1631
|
+
mergedSteps.push(...r.steps);
|
|
1632
|
+
addUsage(mergedUsage, r.usage);
|
|
1633
|
+
if (r._pendingHandoff && hop < MAX_HANDOFFS) {
|
|
1634
|
+
handoffPath.push(currentAgent.constructor.name);
|
|
1635
|
+
const ChildClass = r._pendingHandoff.spec.AgentClass;
|
|
1636
|
+
currentAgent = new ChildClass();
|
|
1637
|
+
currentInput = r._pendingHandoff.transitionMessage;
|
|
1638
|
+
currentOpts = buildHandoffChildOptions(options, r._carriedMessages ?? []);
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (r._pendingHandoff) {
|
|
1642
|
+
throw new Error(`[RudderJS AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
|
|
1643
|
+
}
|
|
1644
|
+
finalResponse = handoffPath.length === 0
|
|
1645
|
+
? stripInternal(r)
|
|
1646
|
+
: mergeFinalHandoff(stripInternal(r), mergedSteps, mergedUsage, handoffPath, currentAgent.constructor.name);
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
if (!finalResponse) {
|
|
1650
|
+
throw new Error(`[RudderJS AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
|
|
1651
|
+
}
|
|
1652
|
+
resolveResponse(finalResponse);
|
|
1653
|
+
}
|
|
1654
|
+
async function* withRejectOnError() {
|
|
1655
|
+
try {
|
|
1656
|
+
yield* generateStream();
|
|
1657
|
+
}
|
|
1658
|
+
catch (err) {
|
|
1659
|
+
rejectResponse(err);
|
|
1660
|
+
throw err;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
stream: withRejectOnError(),
|
|
1665
|
+
response: responsePromise,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Iteratively drive pending handoffs, carrying steps + usage forward.
|
|
1670
|
+
* Used by the non-streaming path. (Streaming has its own iterative driver
|
|
1671
|
+
* inline in {@link runAgentLoopStreaming} so chunks can flow as each hop's
|
|
1672
|
+
* loop runs.)
|
|
1673
|
+
*/
|
|
1674
|
+
async function driveHandoffs(rootName, rootResult, pending, carriedMessages, origOptions, startHopCount) {
|
|
1675
|
+
const mergedSteps = [...rootResult.steps];
|
|
1676
|
+
const mergedUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1677
|
+
addUsage(mergedUsage, rootResult.usage);
|
|
1678
|
+
const handoffPath = [rootName];
|
|
1679
|
+
let currentPending = pending;
|
|
1680
|
+
let currentCarried = carriedMessages;
|
|
1681
|
+
let hopCount = startHopCount;
|
|
1682
|
+
for (;;) {
|
|
1683
|
+
if (hopCount >= MAX_HANDOFFS) {
|
|
1684
|
+
throw new Error(`[RudderJS AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
|
|
1685
|
+
}
|
|
1686
|
+
const ChildClass = currentPending.spec.AgentClass;
|
|
1687
|
+
handoffPath.push(ChildClass.name);
|
|
1688
|
+
const child = new ChildClass();
|
|
1689
|
+
const childOpts = buildHandoffChildOptions(origOptions, currentCarried);
|
|
1690
|
+
const childOnce = await runAgentLoopOnce(child, currentPending.transitionMessage, childOpts);
|
|
1691
|
+
mergedSteps.push(...childOnce.steps);
|
|
1692
|
+
addUsage(mergedUsage, childOnce.usage);
|
|
1693
|
+
if (childOnce._pendingHandoff) {
|
|
1694
|
+
currentPending = childOnce._pendingHandoff;
|
|
1695
|
+
currentCarried = childOnce._carriedMessages ?? [];
|
|
1696
|
+
hopCount++;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
return {
|
|
1700
|
+
...stripInternal(childOnce),
|
|
1701
|
+
steps: mergedSteps,
|
|
1702
|
+
usage: mergedUsage,
|
|
1703
|
+
handoffPath,
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/** Merge the terminal hop's response with carried steps / usage / path. */
|
|
1708
|
+
function mergeFinalHandoff(terminal, mergedSteps, mergedUsage, pathPrefix, terminalName) {
|
|
1709
|
+
return {
|
|
1710
|
+
...terminal,
|
|
1711
|
+
steps: mergedSteps,
|
|
1712
|
+
usage: mergedUsage,
|
|
1713
|
+
handoffPath: [...pathPrefix, terminalName],
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Build the {@link AgentPromptOptions} for a child agent invoked via
|
|
1718
|
+
* handoff. The parent's carried message log replaces the child's input
|
|
1719
|
+
* (so the child sees the full conversation up to the handoff point) but
|
|
1720
|
+
* the child still prepends its own `instructions()` as the system message
|
|
1721
|
+
* during {@link initializeLoop}, so we drop the parent's leading system
|
|
1722
|
+
* message to avoid double-prefixing.
|
|
1723
|
+
*
|
|
1724
|
+
* Per-call options that make sense to carry across (signal, attachments,
|
|
1725
|
+
* tool/middleware overrides) are preserved; `messages` and `history` are
|
|
1726
|
+
* deliberately overridden.
|
|
1727
|
+
*/
|
|
1728
|
+
function buildHandoffChildOptions(parentOptions, carriedMessages) {
|
|
1729
|
+
const stripped = carriedMessages.length > 0 && carriedMessages[0]?.role === 'system'
|
|
1730
|
+
? carriedMessages.slice(1)
|
|
1731
|
+
: carriedMessages;
|
|
1732
|
+
// We append the model's transition message as the next user message so
|
|
1733
|
+
// the child has something concrete to respond to (it's also passed as
|
|
1734
|
+
// `currentInput` below — but feeding it via `messages` mode keeps the
|
|
1735
|
+
// history coherent and prevents `initializeLoop` from also prepending
|
|
1736
|
+
// an `input` user message).
|
|
1737
|
+
return {
|
|
1738
|
+
...(parentOptions ?? {}),
|
|
1739
|
+
messages: stripped,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
/** Strip the internal `_pendingHandoff` / `_carriedMessages` fields before surfacing the response to public callers. */
|
|
1743
|
+
function stripInternal(r) {
|
|
1744
|
+
const out = {
|
|
1745
|
+
text: r.text,
|
|
1746
|
+
steps: r.steps,
|
|
1747
|
+
usage: r.usage,
|
|
1748
|
+
};
|
|
1749
|
+
if (r.conversationId !== undefined)
|
|
1750
|
+
out.conversationId = r.conversationId;
|
|
1751
|
+
if (r.finishReason !== undefined)
|
|
1752
|
+
out.finishReason = r.finishReason;
|
|
1753
|
+
if (r.pendingClientToolCalls !== undefined)
|
|
1754
|
+
out.pendingClientToolCalls = r.pendingClientToolCalls;
|
|
1755
|
+
if (r.pendingApprovalToolCall !== undefined)
|
|
1756
|
+
out.pendingApprovalToolCall = r.pendingApprovalToolCall;
|
|
1757
|
+
if (r.resumedToolMessages !== undefined)
|
|
1758
|
+
out.resumedToolMessages = r.resumedToolMessages;
|
|
1759
|
+
if (r.handoffPath !== undefined)
|
|
1760
|
+
out.handoffPath = r.handoffPath;
|
|
1761
|
+
return out;
|
|
1762
|
+
}
|
|
1763
|
+
async function runAgentLoopOnce(a, input, options) {
|
|
1293
1764
|
const { loopCtx, stopConditions } = await initializeLoop(a, input, options);
|
|
1294
1765
|
const { ctx, middlewares, messages, steps, totalUsage } = loopCtx;
|
|
1295
1766
|
try {
|
|
@@ -1333,7 +1804,7 @@ async function runAgentLoop(a, input, options) {
|
|
|
1333
1804
|
};
|
|
1334
1805
|
steps.push(step);
|
|
1335
1806
|
emitObserverStepCompleted(loopCtx, iteration, false);
|
|
1336
|
-
if (loopCtx.stopForClientTools || loopCtx.stopForApproval)
|
|
1807
|
+
if (loopCtx.stopForClientTools || loopCtx.stopForApproval || loopCtx.stopForHandoff)
|
|
1337
1808
|
break;
|
|
1338
1809
|
const shouldStop = stopConditions.some(cond => cond({ steps, iteration, lastMessage: response.message }));
|
|
1339
1810
|
if (shouldStop || response.finishReason !== 'tool_calls') {
|
|
@@ -1357,7 +1828,7 @@ async function runAgentLoop(a, input, options) {
|
|
|
1357
1828
|
return result;
|
|
1358
1829
|
}
|
|
1359
1830
|
// ─── Agent Loop (streaming) ──────────────────────────────
|
|
1360
|
-
function
|
|
1831
|
+
function runAgentLoopStreamingOnce(a, input, options) {
|
|
1361
1832
|
let resolveResponse;
|
|
1362
1833
|
let rejectResponse;
|
|
1363
1834
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -1463,7 +1934,7 @@ function runAgentLoopStreaming(a, input, options) {
|
|
|
1463
1934
|
};
|
|
1464
1935
|
steps.push(step);
|
|
1465
1936
|
emitObserverStepCompleted(loopCtx, iteration, true);
|
|
1466
|
-
if (loopCtx.stopForClientTools || loopCtx.stopForApproval)
|
|
1937
|
+
if (loopCtx.stopForClientTools || loopCtx.stopForApproval || loopCtx.stopForHandoff)
|
|
1467
1938
|
break;
|
|
1468
1939
|
const shouldStop = stopConditions.some(cond => cond({ steps, iteration, lastMessage: step.message }));
|
|
1469
1940
|
if (shouldStop || finishReason !== 'tool_calls')
|