@rudderjs/ai 1.17.3 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1274
- package/dist/budget-orm/index.d.ts +1 -95
- package/dist/budget-orm/index.d.ts.map +1 -1
- package/dist/budget-orm/index.js +4 -176
- package/dist/budget-orm/index.js.map +1 -1
- package/dist/chat-mentions.d.ts +1 -58
- package/dist/chat-mentions.d.ts.map +1 -1
- package/dist/chat-mentions.js +4 -80
- package/dist/chat-mentions.js.map +1 -1
- package/dist/commands/ai-eval.d.ts +1 -92
- package/dist/commands/ai-eval.d.ts.map +1 -1
- package/dist/commands/ai-eval.js +4 -377
- package/dist/commands/ai-eval.js.map +1 -1
- package/dist/commands/make-agent.d.ts +1 -2
- package/dist/commands/make-agent.d.ts.map +1 -1
- package/dist/commands/make-agent.js +4 -22
- package/dist/commands/make-agent.js.map +1 -1
- package/dist/computer-use/index.d.ts +1 -52
- package/dist/computer-use/index.d.ts.map +1 -1
- package/dist/computer-use/index.js +4 -50
- package/dist/computer-use/index.js.map +1 -1
- package/dist/conversation-orm/index.d.ts +1 -108
- package/dist/conversation-orm/index.d.ts.map +1 -1
- package/dist/conversation-orm/index.js +4 -214
- package/dist/conversation-orm/index.js.map +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -65
- package/dist/doctor.js.map +1 -1
- package/dist/eval/index.d.ts +1 -270
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +4 -509
- package/dist/eval/index.js.map +1 -1
- package/dist/gateway/index.d.ts +1 -10
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +4 -10
- package/dist/gateway/index.js.map +1 -1
- package/dist/index.d.ts +1 -66
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -78
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -15
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +4 -14
- package/dist/mcp/index.js.map +1 -1
- package/dist/memory-embedding/index.d.ts +1 -120
- package/dist/memory-embedding/index.d.ts.map +1 -1
- package/dist/memory-embedding/index.js +4 -228
- package/dist/memory-embedding/index.js.map +1 -1
- package/dist/memory-orm/index.d.ts +1 -117
- package/dist/memory-orm/index.d.ts.map +1 -1
- package/dist/memory-orm/index.js +4 -186
- package/dist/memory-orm/index.js.map +1 -1
- package/dist/node/index.d.ts +1 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +4 -2
- package/dist/node/index.js.map +1 -1
- package/dist/observers.d.ts +1 -129
- package/dist/observers.d.ts.map +1 -1
- package/dist/observers.js +4 -39
- package/dist/observers.js.map +1 -1
- package/dist/react/index.d.ts +1 -15
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +4 -15
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +9 -13
- package/boost/guidelines.md +0 -260
- package/boost/skills/ai-agents/SKILL.md +0 -240
- package/boost/skills/ai-tools/SKILL.md +0 -260
- package/dist/agent-run-store.d.ts +0 -161
- package/dist/agent-run-store.d.ts.map +0 -1
- package/dist/agent-run-store.js +0 -98
- package/dist/agent-run-store.js.map +0 -1
- package/dist/agent-sse.d.ts +0 -153
- package/dist/agent-sse.d.ts.map +0 -1
- package/dist/agent-sse.js +0 -282
- package/dist/agent-sse.js.map +0 -1
- package/dist/agent.d.ts +0 -508
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -1538
- package/dist/agent.js.map +0 -1
- package/dist/attachment.d.ts +0 -31
- package/dist/attachment.d.ts.map +0 -1
- package/dist/attachment.js +0 -89
- package/dist/attachment.js.map +0 -1
- package/dist/audio.d.ts +0 -45
- package/dist/audio.d.ts.map +0 -1
- package/dist/audio.js +0 -93
- package/dist/audio.js.map +0 -1
- package/dist/base64.d.ts +0 -7
- package/dist/base64.d.ts.map +0 -1
- package/dist/base64.js +0 -39
- package/dist/base64.js.map +0 -1
- package/dist/budget/pricing.d.ts +0 -124
- package/dist/budget/pricing.d.ts.map +0 -1
- package/dist/budget/pricing.js +0 -175
- package/dist/budget/pricing.js.map +0 -1
- package/dist/budget/storage.d.ts +0 -104
- package/dist/budget/storage.d.ts.map +0 -1
- package/dist/budget/storage.js +0 -0
- package/dist/budget/storage.js.map +0 -1
- package/dist/budget/with-budget.d.ts +0 -119
- package/dist/budget/with-budget.d.ts.map +0 -1
- package/dist/budget/with-budget.js +0 -175
- package/dist/budget/with-budget.js.map +0 -1
- package/dist/cached-embedding.d.ts +0 -14
- package/dist/cached-embedding.d.ts.map +0 -1
- package/dist/cached-embedding.js +0 -44
- package/dist/cached-embedding.js.map +0 -1
- package/dist/computer-use/actions.d.ts +0 -214
- package/dist/computer-use/actions.d.ts.map +0 -1
- package/dist/computer-use/actions.js +0 -48
- package/dist/computer-use/actions.js.map +0 -1
- package/dist/computer-use/errors.d.ts +0 -57
- package/dist/computer-use/errors.d.ts.map +0 -1
- package/dist/computer-use/errors.js +0 -76
- package/dist/computer-use/errors.js.map +0 -1
- package/dist/computer-use/playwright.d.ts +0 -76
- package/dist/computer-use/playwright.d.ts.map +0 -1
- package/dist/computer-use/playwright.js +0 -270
- package/dist/computer-use/playwright.js.map +0 -1
- package/dist/computer-use/tool.d.ts +0 -154
- package/dist/computer-use/tool.d.ts.map +0 -1
- package/dist/computer-use/tool.js +0 -210
- package/dist/computer-use/tool.js.map +0 -1
- package/dist/continuation-validation.d.ts +0 -85
- package/dist/continuation-validation.d.ts.map +0 -1
- package/dist/continuation-validation.js +0 -166
- package/dist/continuation-validation.js.map +0 -1
- package/dist/conversation-persistence.d.ts +0 -46
- package/dist/conversation-persistence.d.ts.map +0 -1
- package/dist/conversation-persistence.js +0 -176
- package/dist/conversation-persistence.js.map +0 -1
- package/dist/conversation.d.ts +0 -11
- package/dist/conversation.d.ts.map +0 -1
- package/dist/conversation.js +0 -55
- package/dist/conversation.js.map +0 -1
- package/dist/eval/fixtures.d.ts +0 -65
- package/dist/eval/fixtures.d.ts.map +0 -1
- package/dist/eval/fixtures.js +0 -110
- package/dist/eval/fixtures.js.map +0 -1
- package/dist/eval/html-reporter.d.ts +0 -25
- package/dist/eval/html-reporter.d.ts.map +0 -1
- package/dist/eval/html-reporter.js +0 -209
- package/dist/eval/html-reporter.js.map +0 -1
- package/dist/eval/json-reporter.d.ts +0 -43
- package/dist/eval/json-reporter.d.ts.map +0 -1
- package/dist/eval/json-reporter.js +0 -40
- package/dist/eval/json-reporter.js.map +0 -1
- package/dist/facade.d.ts +0 -96
- package/dist/facade.d.ts.map +0 -1
- package/dist/facade.js +0 -146
- package/dist/facade.js.map +0 -1
- package/dist/fake.d.ts +0 -201
- package/dist/fake.d.ts.map +0 -1
- package/dist/fake.js +0 -428
- package/dist/fake.js.map +0 -1
- package/dist/file-search.d.ts +0 -168
- package/dist/file-search.d.ts.map +0 -1
- package/dist/file-search.js +0 -158
- package/dist/file-search.js.map +0 -1
- package/dist/files.d.ts +0 -27
- package/dist/files.d.ts.map +0 -1
- package/dist/files.js +0 -44
- package/dist/files.js.map +0 -1
- package/dist/gateway/http-gateway-adapter.d.ts +0 -94
- package/dist/gateway/http-gateway-adapter.d.ts.map +0 -1
- package/dist/gateway/http-gateway-adapter.js +0 -106
- package/dist/gateway/http-gateway-adapter.js.map +0 -1
- package/dist/gateway/sse.d.ts +0 -28
- package/dist/gateway/sse.d.ts.map +0 -1
- package/dist/gateway/sse.js +0 -78
- package/dist/gateway/sse.js.map +0 -1
- package/dist/handoff.d.ts +0 -95
- package/dist/handoff.d.ts.map +0 -1
- package/dist/handoff.js +0 -78
- package/dist/handoff.js.map +0 -1
- package/dist/handoffs-driver.d.ts +0 -58
- package/dist/handoffs-driver.d.ts.map +0 -1
- package/dist/handoffs-driver.js +0 -103
- package/dist/handoffs-driver.js.map +0 -1
- package/dist/image.d.ts +0 -40
- package/dist/image.d.ts.map +0 -1
- package/dist/image.js +0 -109
- package/dist/image.js.map +0 -1
- package/dist/mcp/client-tools.d.ts +0 -39
- package/dist/mcp/client-tools.d.ts.map +0 -1
- package/dist/mcp/client-tools.js +0 -147
- package/dist/mcp/client-tools.js.map +0 -1
- package/dist/mcp/server-from-agent.d.ts +0 -24
- package/dist/mcp/server-from-agent.d.ts.map +0 -1
- package/dist/mcp/server-from-agent.js +0 -113
- package/dist/mcp/server-from-agent.js.map +0 -1
- package/dist/mcp/types.d.ts +0 -64
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/mcp/types.js +0 -6
- package/dist/mcp/types.js.map +0 -1
- package/dist/memory-extract.d.ts +0 -60
- package/dist/memory-extract.d.ts.map +0 -1
- package/dist/memory-extract.js +0 -163
- package/dist/memory-extract.js.map +0 -1
- package/dist/memory-inject.d.ts +0 -39
- package/dist/memory-inject.d.ts.map +0 -1
- package/dist/memory-inject.js +0 -135
- package/dist/memory-inject.js.map +0 -1
- package/dist/memory.d.ts +0 -55
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -132
- package/dist/memory.js.map +0 -1
- package/dist/middleware.d.ts +0 -18
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -72
- package/dist/middleware.js.map +0 -1
- package/dist/node/attachment.d.ts +0 -6
- package/dist/node/attachment.d.ts.map +0 -1
- package/dist/node/attachment.js +0 -35
- package/dist/node/attachment.js.map +0 -1
- package/dist/node/transcription.d.ts +0 -4
- package/dist/node/transcription.d.ts.map +0 -1
- package/dist/node/transcription.js +0 -8
- package/dist/node/transcription.js.map +0 -1
- package/dist/output.d.ts +0 -22
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -60
- package/dist/output.js.map +0 -1
- package/dist/provider-tools.d.ts +0 -87
- package/dist/provider-tools.d.ts.map +0 -1
- package/dist/provider-tools.js +0 -189
- package/dist/provider-tools.js.map +0 -1
- package/dist/providers/anthropic.d.ts +0 -24
- package/dist/providers/anthropic.d.ts.map +0 -1
- package/dist/providers/anthropic.js +0 -405
- package/dist/providers/anthropic.js.map +0 -1
- package/dist/providers/azure.d.ts +0 -13
- package/dist/providers/azure.d.ts.map +0 -1
- package/dist/providers/azure.js +0 -15
- package/dist/providers/azure.js.map +0 -1
- package/dist/providers/bedrock.d.ts +0 -75
- package/dist/providers/bedrock.d.ts.map +0 -1
- package/dist/providers/bedrock.js +0 -181
- package/dist/providers/bedrock.js.map +0 -1
- package/dist/providers/cohere.d.ts +0 -13
- package/dist/providers/cohere.d.ts.map +0 -1
- package/dist/providers/cohere.js +0 -87
- package/dist/providers/cohere.js.map +0 -1
- package/dist/providers/deepseek.d.ts +0 -12
- package/dist/providers/deepseek.d.ts.map +0 -1
- package/dist/providers/deepseek.js +0 -15
- package/dist/providers/deepseek.js.map +0 -1
- package/dist/providers/elevenlabs.d.ts +0 -98
- package/dist/providers/elevenlabs.d.ts.map +0 -1
- package/dist/providers/elevenlabs.js +0 -229
- package/dist/providers/elevenlabs.js.map +0 -1
- package/dist/providers/google-cache-registry.d.ts +0 -132
- package/dist/providers/google-cache-registry.d.ts.map +0 -1
- package/dist/providers/google-cache-registry.js +0 -209
- package/dist/providers/google-cache-registry.js.map +0 -1
- package/dist/providers/google.d.ts +0 -38
- package/dist/providers/google.d.ts.map +0 -1
- package/dist/providers/google.js +0 -903
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/groq.d.ts +0 -12
- package/dist/providers/groq.d.ts.map +0 -1
- package/dist/providers/groq.js +0 -15
- package/dist/providers/groq.js.map +0 -1
- package/dist/providers/jina.d.ts +0 -13
- package/dist/providers/jina.d.ts.map +0 -1
- package/dist/providers/jina.js +0 -90
- package/dist/providers/jina.js.map +0 -1
- package/dist/providers/mistral.d.ts +0 -13
- package/dist/providers/mistral.d.ts.map +0 -1
- package/dist/providers/mistral.js +0 -46
- package/dist/providers/mistral.js.map +0 -1
- package/dist/providers/ollama.d.ts +0 -11
- package/dist/providers/ollama.d.ts.map +0 -1
- package/dist/providers/ollama.js +0 -15
- package/dist/providers/ollama.js.map +0 -1
- package/dist/providers/openai.d.ts +0 -79
- package/dist/providers/openai.d.ts.map +0 -1
- package/dist/providers/openai.js +0 -792
- package/dist/providers/openai.js.map +0 -1
- package/dist/providers/openrouter.d.ts +0 -43
- package/dist/providers/openrouter.d.ts.map +0 -1
- package/dist/providers/openrouter.js +0 -21
- package/dist/providers/openrouter.js.map +0 -1
- package/dist/providers/voyage.d.ts +0 -91
- package/dist/providers/voyage.d.ts.map +0 -1
- package/dist/providers/voyage.js +0 -166
- package/dist/providers/voyage.js.map +0 -1
- package/dist/providers/xai.d.ts +0 -12
- package/dist/providers/xai.d.ts.map +0 -1
- package/dist/providers/xai.js +0 -15
- package/dist/providers/xai.js.map +0 -1
- package/dist/queue-job.d.ts +0 -100
- package/dist/queue-job.d.ts.map +0 -1
- package/dist/queue-job.js +0 -185
- package/dist/queue-job.js.map +0 -1
- package/dist/react/agent-run.d.ts +0 -111
- package/dist/react/agent-run.d.ts.map +0 -1
- package/dist/react/agent-run.js +0 -107
- package/dist/react/agent-run.js.map +0 -1
- package/dist/react/useAgentRun.d.ts +0 -68
- package/dist/react/useAgentRun.d.ts.map +0 -1
- package/dist/react/useAgentRun.js +0 -125
- package/dist/react/useAgentRun.js.map +0 -1
- package/dist/registry.d.ts +0 -45
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -131
- package/dist/registry.js.map +0 -1
- package/dist/rerank.d.ts +0 -20
- package/dist/rerank.d.ts.map +0 -1
- package/dist/rerank.js +0 -40
- package/dist/rerank.js.map +0 -1
- package/dist/resume-approval.d.ts +0 -30
- package/dist/resume-approval.d.ts.map +0 -1
- package/dist/resume-approval.js +0 -147
- package/dist/resume-approval.js.map +0 -1
- package/dist/sanitize-conversation.d.ts +0 -43
- package/dist/sanitize-conversation.d.ts.map +0 -1
- package/dist/sanitize-conversation.js +0 -85
- package/dist/sanitize-conversation.js.map +0 -1
- package/dist/scoped-tool.d.ts +0 -98
- package/dist/scoped-tool.d.ts.map +0 -1
- package/dist/scoped-tool.js +0 -174
- package/dist/scoped-tool.js.map +0 -1
- package/dist/server/provider.d.ts +0 -22
- package/dist/server/provider.d.ts.map +0 -1
- package/dist/server/provider.js +0 -194
- package/dist/server/provider.js.map +0 -1
- package/dist/similarity-search.d.ts +0 -163
- package/dist/similarity-search.d.ts.map +0 -1
- package/dist/similarity-search.js +0 -147
- package/dist/similarity-search.js.map +0 -1
- package/dist/sub-agent-run-store.d.ts +0 -157
- package/dist/sub-agent-run-store.d.ts.map +0 -1
- package/dist/sub-agent-run-store.js +0 -87
- package/dist/sub-agent-run-store.js.map +0 -1
- package/dist/tool-execution.d.ts +0 -16
- package/dist/tool-execution.d.ts.map +0 -1
- package/dist/tool-execution.js +0 -498
- package/dist/tool-execution.js.map +0 -1
- package/dist/tool-helpers.d.ts +0 -77
- package/dist/tool-helpers.d.ts.map +0 -1
- package/dist/tool-helpers.js +0 -117
- package/dist/tool-helpers.js.map +0 -1
- package/dist/tool.d.ts +0 -216
- package/dist/tool.d.ts.map +0 -1
- package/dist/tool.js +0 -175
- package/dist/tool.js.map +0 -1
- package/dist/transcription.d.ts +0 -42
- package/dist/transcription.d.ts.map +0 -1
- package/dist/transcription.js +0 -77
- package/dist/transcription.js.map +0 -1
- package/dist/types.d.ts +0 -1020
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/util/hash.d.ts +0 -11
- package/dist/util/hash.d.ts.map +0 -1
- package/dist/util/hash.js +0 -23
- package/dist/util/hash.js.map +0 -1
- package/dist/vector-stores/index.d.ts +0 -96
- package/dist/vector-stores/index.d.ts.map +0 -1
- package/dist/vector-stores/index.js +0 -153
- package/dist/vector-stores/index.js.map +0 -1
- package/dist/vercel-protocol.d.ts +0 -18
- package/dist/vercel-protocol.d.ts.map +0 -1
- package/dist/vercel-protocol.js +0 -75
- package/dist/vercel-protocol.js.map +0 -1
- package/dist/zod-to-json-schema.d.ts +0 -16
- package/dist/zod-to-json-schema.d.ts.map +0 -1
- package/dist/zod-to-json-schema.js +0 -17
- package/dist/zod-to-json-schema.js.map +0 -1
package/dist/agent.js
DELETED
|
@@ -1,1538 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { AiRegistry } from './registry.js';
|
|
3
|
-
import { pauseForApproval, pauseForClientTools, toolDefinition, toolToSchema } from './tool.js';
|
|
4
|
-
import { attachmentsToContentParts, getMessageText } from './attachment.js';
|
|
5
|
-
import { QueuedPromptBuilder } from './queue-job.js';
|
|
6
|
-
import { resolveAutoPersistSpec, runWithPersistence, runWithPersistenceStreaming, } from './conversation-persistence.js';
|
|
7
|
-
import { resolveRemembersSpec } from './memory.js';
|
|
8
|
-
import { withMemoryInject } from './memory-inject.js';
|
|
9
|
-
import { withMemoryExtract } from './memory-extract.js';
|
|
10
|
-
import { runOnConfig, runOnChunk, runSequential, runOnUsage, runOnAbort, runOnError, } from './middleware.js';
|
|
11
|
-
import { executeToolPhase } from './tool-execution.js';
|
|
12
|
-
import { resumePendingToolCalls } from './resume-approval.js';
|
|
13
|
-
import { buildHandoffChildOptions, driveHandoffs, MAX_HANDOFFS, mergeFinalHandoff, stripInternal, } from './handoffs-driver.js';
|
|
14
|
-
// ─── AI Observer (lazy accessor) ─────────────────────────
|
|
15
|
-
function _getAiObservers() {
|
|
16
|
-
return globalThis['__rudderjs_ai_observers__'] ?? null;
|
|
17
|
-
}
|
|
18
|
-
function _buildObserverSteps(steps, modelString) {
|
|
19
|
-
return steps.map((step, i) => ({
|
|
20
|
-
iteration: i + 1,
|
|
21
|
-
model: modelString,
|
|
22
|
-
tokens: { prompt: step.usage.promptTokens, completion: step.usage.completionTokens, total: step.usage.totalTokens },
|
|
23
|
-
finishReason: step.finishReason,
|
|
24
|
-
toolCalls: step.toolCalls.map(tc => {
|
|
25
|
-
const tr = step.toolResults.find(r => r.toolCallId === tc.id);
|
|
26
|
-
return {
|
|
27
|
-
id: tc.id,
|
|
28
|
-
name: tc.name,
|
|
29
|
-
args: tc.arguments,
|
|
30
|
-
result: tr?.result,
|
|
31
|
-
duration: tr?.duration ?? 0,
|
|
32
|
-
needsApproval: false,
|
|
33
|
-
};
|
|
34
|
-
}),
|
|
35
|
-
}));
|
|
36
|
-
}
|
|
37
|
-
// ─── Stop Condition Combinators ──────────────────────────
|
|
38
|
-
/** Stop after N steps */
|
|
39
|
-
export function stepCountIs(n) {
|
|
40
|
-
return ({ steps }) => steps.length >= n;
|
|
41
|
-
}
|
|
42
|
-
/** Stop when a specific tool is called in the latest step */
|
|
43
|
-
export function hasToolCall(toolName) {
|
|
44
|
-
return ({ steps }) => {
|
|
45
|
-
const last = steps[steps.length - 1];
|
|
46
|
-
return last?.toolCalls.some(tc => tc.name === toolName) ?? false;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
// ─── Agent Base Class ────────────────────────────────────
|
|
50
|
-
export class Agent {
|
|
51
|
-
/** Model string (e.g. 'anthropic/claude-sonnet-4-5'). Defaults to registry default. */
|
|
52
|
-
model() { return undefined; }
|
|
53
|
-
/** Failover provider/model strings */
|
|
54
|
-
failover() { return []; }
|
|
55
|
-
/** Maximum iterations for the tool loop (default: 20) */
|
|
56
|
-
maxSteps() { return 20; }
|
|
57
|
-
/** Stop conditions — combine with array (OR logic) */
|
|
58
|
-
stopWhen() {
|
|
59
|
-
return stepCountIs(this.maxSteps());
|
|
60
|
-
}
|
|
61
|
-
/** Temperature (0-1) */
|
|
62
|
-
temperature() { return undefined; }
|
|
63
|
-
/** Max tokens for response */
|
|
64
|
-
maxTokens() { return undefined; }
|
|
65
|
-
/**
|
|
66
|
-
* Declarative prompt-cache configuration.
|
|
67
|
-
*
|
|
68
|
-
* Override on a subclass to mark stable parts of the prompt as cacheable
|
|
69
|
-
* — provider adapters translate to native primitives (Anthropic
|
|
70
|
-
* `cache_control`, OpenAI `prompt_cache_key`, Google `cachedContent`)
|
|
71
|
-
* so cache hits can save 50–90% on input tokens for long system prompts,
|
|
72
|
-
* tool definitions, or stable conversation context.
|
|
73
|
-
*
|
|
74
|
-
* Returning `undefined` (the default) means no caching. Per-call override
|
|
75
|
-
* via `agent.prompt(input, { cache: false })` disables caching for that
|
|
76
|
-
* call; passing a {@link CacheableConfig} for `cache` replaces the agent
|
|
77
|
-
* default for that call.
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* class SupportAgent extends Agent {
|
|
81
|
-
* instructions() { return LONG_SYSTEM_PROMPT }
|
|
82
|
-
* tools() { return [tool1, tool2, tool3] }
|
|
83
|
-
* cacheable() {
|
|
84
|
-
* return { instructions: true, tools: true }
|
|
85
|
-
* }
|
|
86
|
-
* }
|
|
87
|
-
*/
|
|
88
|
-
cacheable() { return undefined; }
|
|
89
|
-
/**
|
|
90
|
-
* Opt into auto-persisted conversation behavior. Override on a subclass
|
|
91
|
-
* to declare *which* user owns the thread and (optionally) which
|
|
92
|
-
* specific thread, and the framework will load history before each
|
|
93
|
-
* `prompt()`/`stream()` call and append the new turn after it — without
|
|
94
|
-
* any caller having to remember `forUser()` / `continue()`.
|
|
95
|
-
*
|
|
96
|
-
* Returning `false` (the default) disables auto-persist; the agent runs
|
|
97
|
-
* stateless. Returning a {@link ConversationalSpec} opts in:
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* class ChatAgent extends Agent {
|
|
101
|
-
* conversational() {
|
|
102
|
-
* return { user: Auth.user()?.id } // null user → falsy → opt-out
|
|
103
|
-
* }
|
|
104
|
-
* }
|
|
105
|
-
*
|
|
106
|
-
* await new ChatAgent().prompt('Hi') // auto-loads + auto-saves
|
|
107
|
-
*
|
|
108
|
-
* **Precedence (high → low):**
|
|
109
|
-
* 1. Explicit `agent.forUser(id).prompt()` / `agent.continue(id).prompt()`
|
|
110
|
-
* 2. Per-call `prompt(input, { conversation: false | {...} })`
|
|
111
|
-
* 3. This method's return value
|
|
112
|
-
*
|
|
113
|
-
* Async returns are supported — useful when the user identity is fetched
|
|
114
|
-
* from an async DI binding.
|
|
115
|
-
*/
|
|
116
|
-
conversational() {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Opt this agent class into per-user memory beyond conversation history
|
|
121
|
-
* (#A4). Returns a {@link RemembersSpec} naming the user whose memory
|
|
122
|
-
* the agent reads/writes, and how injection / extraction should behave.
|
|
123
|
-
* Returning `false` (the default) leaves the agent memory-stateless.
|
|
124
|
-
*
|
|
125
|
-
* Phase 1 wires the declaration + the per-call precedence chain so
|
|
126
|
-
* apps and downstream phases (auto-inject middleware in Phase 2,
|
|
127
|
-
* auto-extract middleware in Phase 3) can read a consistent spec.
|
|
128
|
-
* Calling this method directly today produces no runtime behavior
|
|
129
|
-
* unless application code reads it via `resolveRemembersSpec()`.
|
|
130
|
-
*
|
|
131
|
-
* **Precedence (high → low):**
|
|
132
|
-
* 1. Per-call `prompt(input, { memory: false | {...} })`
|
|
133
|
-
* 2. This method's return value
|
|
134
|
-
*
|
|
135
|
-
* Async returns are supported — useful when the user identity is fetched
|
|
136
|
-
* from an async DI binding.
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* class SupportAgent extends Agent {
|
|
140
|
-
* remembers() { return { user: ctx.user.id, inject: 'auto', tags: ['support'] } }
|
|
141
|
-
* }
|
|
142
|
-
*/
|
|
143
|
-
remembers() {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Default for `AgentPromptOptions.parallelTools`. When `true` (default),
|
|
148
|
-
* multiple tool calls within a single step run their `execute()` functions
|
|
149
|
-
* concurrently. Override on a subclass to flip the default for an agent
|
|
150
|
-
* whose tools share non-idempotent state. Per-call options still win.
|
|
151
|
-
*/
|
|
152
|
-
parallelTools() { return true; }
|
|
153
|
-
/** Run the agent with a prompt (non-streaming) */
|
|
154
|
-
async prompt(input, options) {
|
|
155
|
-
// Memory auto-cascade — appends inject (Phase 2) + extract (Phase 3)
|
|
156
|
-
// middlewares when `Agent.remembers()` opts in. Runs BEFORE
|
|
157
|
-
// conversation persistence so the persisted history flows in
|
|
158
|
-
// unchanged: inject only grows the system message; extract only
|
|
159
|
-
// fires onFinish.
|
|
160
|
-
const effOptions = await prepareOptionsWithMemoryAutoCascade(this, options);
|
|
161
|
-
const spec = await resolveAutoPersistSpec(() => this.conversational(), effOptions?.conversation);
|
|
162
|
-
if (spec) {
|
|
163
|
-
return runWithPersistence(spec, this.constructor.name, resolveConversationStore, input, effOptions, (innerOptions) => runAgentLoop(this, input, innerOptions));
|
|
164
|
-
}
|
|
165
|
-
return runAgentLoop(this, input, effOptions);
|
|
166
|
-
}
|
|
167
|
-
/** Run the agent with a prompt (streaming) */
|
|
168
|
-
stream(input, options) {
|
|
169
|
-
return runStreamWithMaybeAutoPersist(this, input, options);
|
|
170
|
-
}
|
|
171
|
-
/** Queue the prompt for background execution */
|
|
172
|
-
queue(input, options) {
|
|
173
|
-
return new QueuedPromptBuilder(this, input, options);
|
|
174
|
-
}
|
|
175
|
-
/** Set the user scope for conversation persistence */
|
|
176
|
-
forUser(userId) {
|
|
177
|
-
return new ConversableAgent(this).forUser(userId);
|
|
178
|
-
}
|
|
179
|
-
/** Continue an existing conversation */
|
|
180
|
-
continue(conversationId) {
|
|
181
|
-
return new ConversableAgent(this).continue(conversationId);
|
|
182
|
-
}
|
|
183
|
-
asTool(options) {
|
|
184
|
-
if (options.suspendable && !options.streaming) {
|
|
185
|
-
throw new Error('[Rudder AI] asTool: `suspendable` requires `streaming: true` (or a projector). Silent suspend would leave the parent UI with no progress signal between sub-agent invocations.');
|
|
186
|
-
}
|
|
187
|
-
const schema = options.inputSchema ?? z.object({ prompt: z.string() });
|
|
188
|
-
const promptOf = options.prompt ?? ((input) => input.prompt);
|
|
189
|
-
const modelOutput = options.modelOutput ?? ((response) => response.text);
|
|
190
|
-
if (!options.streaming) {
|
|
191
|
-
// 1.2.0 zero-config path — single prompt() call, single AgentResponse out.
|
|
192
|
-
return toolDefinition({
|
|
193
|
-
name: options.name,
|
|
194
|
-
description: options.description,
|
|
195
|
-
inputSchema: schema,
|
|
196
|
-
})
|
|
197
|
-
.server((input) => this.prompt(promptOf(input)))
|
|
198
|
-
.modelOutput(modelOutput);
|
|
199
|
-
}
|
|
200
|
-
const project = options.streaming === true ? defaultSubAgentProjector : options.streaming;
|
|
201
|
-
const innerAgent = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
|
202
|
-
const agentName = options.name;
|
|
203
|
-
const suspendable = options.suspendable;
|
|
204
|
-
const generatorExecute = async function* (input) {
|
|
205
|
-
const userPrompt = promptOf(input);
|
|
206
|
-
yield { kind: 'agent_start', agentName };
|
|
207
|
-
const streamOpts = suspendable
|
|
208
|
-
? { toolCallStreamingMode: 'stop-on-client-tool' }
|
|
209
|
-
: undefined;
|
|
210
|
-
const { stream, response } = innerAgent.stream(userPrompt, streamOpts);
|
|
211
|
-
for await (const chunk of stream) {
|
|
212
|
-
const update = project(chunk);
|
|
213
|
-
if (update)
|
|
214
|
-
yield update;
|
|
215
|
-
}
|
|
216
|
-
const result = await response;
|
|
217
|
-
if (suspendable &&
|
|
218
|
-
result.finishReason === 'client_tool_calls' &&
|
|
219
|
-
result.pendingClientToolCalls?.length) {
|
|
220
|
-
const subRunId = generateSubRunId();
|
|
221
|
-
const snapshot = {
|
|
222
|
-
messages: buildSubAgentSnapshotMessages(userPrompt, result),
|
|
223
|
-
pendingToolCallIds: result.pendingClientToolCalls.map((tc) => tc.id),
|
|
224
|
-
stepsSoFar: result.steps.length,
|
|
225
|
-
tokensSoFar: result.usage?.totalTokens ?? 0,
|
|
226
|
-
pauseKind: 'client_tool',
|
|
227
|
-
};
|
|
228
|
-
await suspendable.runStore.store(subRunId, snapshot);
|
|
229
|
-
yield { kind: 'subagent_paused', subRunId, pendingToolCallIds: snapshot.pendingToolCallIds };
|
|
230
|
-
yield pauseForClientTools(result.pendingClientToolCalls, subRunId);
|
|
231
|
-
// Unreachable — the parent loop halts iteration after the pause chunk.
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
if (suspendable &&
|
|
235
|
-
result.finishReason === 'tool_approval_required' &&
|
|
236
|
-
result.pendingApprovalToolCall) {
|
|
237
|
-
const subRunId = generateSubRunId();
|
|
238
|
-
const { toolCall: pendingCall, isClientTool } = result.pendingApprovalToolCall;
|
|
239
|
-
const snapshot = {
|
|
240
|
-
messages: buildSubAgentSnapshotMessages(userPrompt, result),
|
|
241
|
-
pendingToolCallIds: [pendingCall.id],
|
|
242
|
-
stepsSoFar: result.steps.length,
|
|
243
|
-
tokensSoFar: result.usage?.totalTokens ?? 0,
|
|
244
|
-
pauseKind: 'approval',
|
|
245
|
-
pendingApprovalToolCall: { toolCall: pendingCall, isClientTool },
|
|
246
|
-
};
|
|
247
|
-
await suspendable.runStore.store(subRunId, snapshot);
|
|
248
|
-
yield {
|
|
249
|
-
kind: 'subagent_paused_approval',
|
|
250
|
-
subRunId,
|
|
251
|
-
toolCall: pendingCall,
|
|
252
|
-
isClientTool,
|
|
253
|
-
};
|
|
254
|
-
yield pauseForApproval(pendingCall, isClientTool, subRunId);
|
|
255
|
-
// Unreachable — the parent loop halts iteration after the pause chunk.
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
yield {
|
|
259
|
-
kind: 'agent_done',
|
|
260
|
-
steps: result.steps.length,
|
|
261
|
-
tokens: result.usage?.totalTokens ?? 0,
|
|
262
|
-
};
|
|
263
|
-
return result;
|
|
264
|
-
};
|
|
265
|
-
return toolDefinition({
|
|
266
|
-
name: options.name,
|
|
267
|
-
description: options.description,
|
|
268
|
-
inputSchema: schema,
|
|
269
|
-
})
|
|
270
|
-
.server(generatorExecute)
|
|
271
|
-
.modelOutput(modelOutput);
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Resume a sub-agent run that previously paused with either
|
|
275
|
-
* `pauseForClientTools` (client-tool pause) or `pauseForApproval`
|
|
276
|
-
* (approval pause), typically from {@link Agent.asTool} with
|
|
277
|
-
* `suspendable: { runStore }` set. The snapshot's `pauseKind`
|
|
278
|
-
* (default `'client_tool'`) selects the resume contract:
|
|
279
|
-
*
|
|
280
|
-
* - **`client_tool`** — `clientToolResults` must carry one entry per
|
|
281
|
-
* id in the snapshot's `pendingToolCallIds`. Results are appended
|
|
282
|
-
* to the inner-agent message history and the loop re-runs.
|
|
283
|
-
* - **`approval`** — `approvedToolCallIds` and/or
|
|
284
|
-
* `rejectedToolCallIds` must reference the single pending id.
|
|
285
|
-
* `clientToolResults` must be empty; the loop re-runs with the
|
|
286
|
-
* approval decision injected via `AgentPromptOptions`.
|
|
287
|
-
*
|
|
288
|
-
* Returns either a `'completed'` result (the inner agent finished),
|
|
289
|
-
* a `'paused'` continuation pointing at a fresh `subRunId` for the
|
|
290
|
-
* next round-trip, or stays `'paused'` if the inner loop hits another
|
|
291
|
-
* gate. The resume can pause on a different kind than it started on
|
|
292
|
-
* (e.g. an approval pause that, once approved, hits a client-tool
|
|
293
|
-
* pause on the next step).
|
|
294
|
-
*
|
|
295
|
-
* @example Client-tool resume
|
|
296
|
-
* const r = await Agent.resumeAsTool(subRunId, browserResults, { runStore, agent: subAgent })
|
|
297
|
-
*
|
|
298
|
-
* @example Approval resume
|
|
299
|
-
* const r = await Agent.resumeAsTool(subRunId, [], {
|
|
300
|
-
* runStore, agent: subAgent,
|
|
301
|
-
* approvedToolCallIds: ['inner-call-id'],
|
|
302
|
-
* })
|
|
303
|
-
*/
|
|
304
|
-
static async resumeAsTool(subRunId, clientToolResults, options) {
|
|
305
|
-
const snapshot = await options.runStore.consume(subRunId);
|
|
306
|
-
if (!snapshot) {
|
|
307
|
-
throw new Error(`[Rudder AI] resumeAsTool: subRunId "${subRunId}" expired or never existed.`);
|
|
308
|
-
}
|
|
309
|
-
const pauseKind = snapshot.pauseKind ?? 'client_tool';
|
|
310
|
-
const pending = new Set(snapshot.pendingToolCallIds);
|
|
311
|
-
let messages;
|
|
312
|
-
const promptOpts = { toolCallStreamingMode: 'stop-on-client-tool' };
|
|
313
|
-
if (pauseKind === 'client_tool') {
|
|
314
|
-
// Forgery guard — every incoming tool-result id must be in the pending set.
|
|
315
|
-
const seen = new Set();
|
|
316
|
-
for (const r of clientToolResults) {
|
|
317
|
-
if (!pending.has(r.toolCallId)) {
|
|
318
|
-
throw new Error(`[Rudder AI] resumeAsTool: toolCallId "${r.toolCallId}" was not in the pending set.`);
|
|
319
|
-
}
|
|
320
|
-
if (seen.has(r.toolCallId)) {
|
|
321
|
-
throw new Error(`[Rudder AI] resumeAsTool: duplicate result for toolCallId "${r.toolCallId}".`);
|
|
322
|
-
}
|
|
323
|
-
seen.add(r.toolCallId);
|
|
324
|
-
}
|
|
325
|
-
// Append client tool-result messages to the snapshot, in incoming order.
|
|
326
|
-
messages = [...snapshot.messages];
|
|
327
|
-
for (const r of clientToolResults) {
|
|
328
|
-
messages.push({
|
|
329
|
-
role: 'tool',
|
|
330
|
-
content: typeof r.result === 'string' ? r.result : JSON.stringify(r.result),
|
|
331
|
-
toolCallId: r.toolCallId,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
// Approval-pause resume — clientToolResults must be empty; either an
|
|
337
|
-
// approval or a rejection must be supplied for the pending id.
|
|
338
|
-
if (clientToolResults.length > 0) {
|
|
339
|
-
throw new Error('[Rudder AI] resumeAsTool: snapshot.pauseKind === "approval" but clientToolResults was non-empty. Pass `approvedToolCallIds` or `rejectedToolCallIds` instead.');
|
|
340
|
-
}
|
|
341
|
-
const approved = options.approvedToolCallIds ?? [];
|
|
342
|
-
const rejected = options.rejectedToolCallIds ?? [];
|
|
343
|
-
for (const id of approved) {
|
|
344
|
-
if (!pending.has(id)) {
|
|
345
|
-
throw new Error(`[Rudder AI] resumeAsTool: approvedToolCallId "${id}" was not in the pending set.`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
for (const id of rejected) {
|
|
349
|
-
if (!pending.has(id)) {
|
|
350
|
-
throw new Error(`[Rudder AI] resumeAsTool: rejectedToolCallId "${id}" was not in the pending set.`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (approved.length === 0 && rejected.length === 0) {
|
|
354
|
-
throw new Error('[Rudder AI] resumeAsTool: snapshot.pauseKind === "approval" requires `approvedToolCallIds` or `rejectedToolCallIds`.');
|
|
355
|
-
}
|
|
356
|
-
messages = [...snapshot.messages];
|
|
357
|
-
if (approved.length > 0)
|
|
358
|
-
promptOpts.approvedToolCallIds = approved;
|
|
359
|
-
if (rejected.length > 0)
|
|
360
|
-
promptOpts.rejectedToolCallIds = rejected;
|
|
361
|
-
}
|
|
362
|
-
promptOpts.messages = messages;
|
|
363
|
-
// Default path: non-streaming resume — one prompt() call, one response.
|
|
364
|
-
// Opt-in streaming path: run the inner loop via stream() and forward each
|
|
365
|
-
// projected chunk to onUpdate, so a host can keep a resumed sub-agent's
|
|
366
|
-
// progress live across the round-trip. Either way `result` is the same
|
|
367
|
-
// AgentResponse and the pause/completion partition below is unchanged.
|
|
368
|
-
let result;
|
|
369
|
-
if (options.streaming) {
|
|
370
|
-
const project = options.streaming === true ? defaultSubAgentProjector : options.streaming;
|
|
371
|
-
const { stream, response } = options.agent.stream('', promptOpts);
|
|
372
|
-
const ctx = { originalSubRunId: subRunId, ...(options.key !== undefined ? { key: options.key } : {}) };
|
|
373
|
-
for await (const chunk of stream) {
|
|
374
|
-
const update = project(chunk, ctx);
|
|
375
|
-
if (update && options.onUpdate)
|
|
376
|
-
await options.onUpdate(update);
|
|
377
|
-
}
|
|
378
|
-
result = await response;
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
result = await options.agent.prompt('', promptOpts);
|
|
382
|
-
}
|
|
383
|
-
if (result.finishReason === 'client_tool_calls' &&
|
|
384
|
-
result.pendingClientToolCalls?.length) {
|
|
385
|
-
const newSubRunId = generateSubRunId();
|
|
386
|
-
const newSnapshot = {
|
|
387
|
-
messages: buildResumeSnapshotMessages(messages, result),
|
|
388
|
-
pendingToolCallIds: result.pendingClientToolCalls.map((tc) => tc.id),
|
|
389
|
-
stepsSoFar: snapshot.stepsSoFar + result.steps.length,
|
|
390
|
-
tokensSoFar: snapshot.tokensSoFar + (result.usage?.totalTokens ?? 0),
|
|
391
|
-
pauseKind: 'client_tool',
|
|
392
|
-
...(snapshot.meta !== undefined ? { meta: snapshot.meta } : {}),
|
|
393
|
-
};
|
|
394
|
-
await options.runStore.store(newSubRunId, newSnapshot);
|
|
395
|
-
return {
|
|
396
|
-
kind: 'paused',
|
|
397
|
-
subRunId: newSubRunId,
|
|
398
|
-
pauseKind: 'client_tool',
|
|
399
|
-
pendingToolCallIds: newSnapshot.pendingToolCallIds,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
if (result.finishReason === 'tool_approval_required' &&
|
|
403
|
-
result.pendingApprovalToolCall) {
|
|
404
|
-
const newSubRunId = generateSubRunId();
|
|
405
|
-
const { toolCall: pendingCall, isClientTool } = result.pendingApprovalToolCall;
|
|
406
|
-
const newSnapshot = {
|
|
407
|
-
messages: buildResumeSnapshotMessages(messages, result),
|
|
408
|
-
pendingToolCallIds: [pendingCall.id],
|
|
409
|
-
stepsSoFar: snapshot.stepsSoFar + result.steps.length,
|
|
410
|
-
tokensSoFar: snapshot.tokensSoFar + (result.usage?.totalTokens ?? 0),
|
|
411
|
-
pauseKind: 'approval',
|
|
412
|
-
pendingApprovalToolCall: { toolCall: pendingCall, isClientTool },
|
|
413
|
-
...(snapshot.meta !== undefined ? { meta: snapshot.meta } : {}),
|
|
414
|
-
};
|
|
415
|
-
await options.runStore.store(newSubRunId, newSnapshot);
|
|
416
|
-
return {
|
|
417
|
-
kind: 'paused',
|
|
418
|
-
subRunId: newSubRunId,
|
|
419
|
-
pauseKind: 'approval',
|
|
420
|
-
pendingToolCallIds: newSnapshot.pendingToolCallIds,
|
|
421
|
-
toolCall: pendingCall,
|
|
422
|
-
isClientTool,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
return { kind: 'completed', response: result };
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Resume MANY paused sub-agents in one call and aggregate their pending
|
|
429
|
-
* tool calls into a single client round-trip.
|
|
430
|
-
*
|
|
431
|
-
* When an orchestrator dispatches several sub-agents in one parent turn
|
|
432
|
-
* and more than one pauses on a client tool (or approval gate), the host
|
|
433
|
-
* would otherwise loop over {@link Agent.resumeAsTool} by hand and stitch
|
|
434
|
-
* the pending sets back together. This does that: each request resumes its
|
|
435
|
-
* own `(subRunId, agent)` snapshot, and the result carries the combined
|
|
436
|
-
* `completed` / `paused` / `errors` partition plus the flattened
|
|
437
|
-
* `pendingToolCallIds` the host collects the next batch of results for.
|
|
438
|
-
*
|
|
439
|
-
* Re-entrant: feed the next round of `clientToolResults` / approvals back
|
|
440
|
-
* in as a fresh batch keyed off each paused item's NEW `subRunId` until
|
|
441
|
-
* `allCompleted` is `true`.
|
|
442
|
-
*
|
|
443
|
-
* @example
|
|
444
|
-
* let batch = await Agent.resumeManyAsTool(
|
|
445
|
-
* paused.map(p => ({ subRunId: p.subRunId, agent: p.agent, clientToolResults: results[p.subRunId] })),
|
|
446
|
-
* { runStore },
|
|
447
|
-
* )
|
|
448
|
-
* // batch.pendingToolCallIds → gather the next round from the browser, repeat.
|
|
449
|
-
*/
|
|
450
|
-
static async resumeManyAsTool(requests, options) {
|
|
451
|
-
const onError = options.onError ?? 'capture';
|
|
452
|
-
const concurrency = options.concurrency ?? 'parallel';
|
|
453
|
-
const runOne = async (req) => {
|
|
454
|
-
const base = { originalSubRunId: req.subRunId, ...(req.key !== undefined ? { key: req.key } : {}) };
|
|
455
|
-
try {
|
|
456
|
-
const opts = { runStore: options.runStore, agent: req.agent };
|
|
457
|
-
if (req.approvedToolCallIds)
|
|
458
|
-
opts.approvedToolCallIds = req.approvedToolCallIds;
|
|
459
|
-
if (req.rejectedToolCallIds)
|
|
460
|
-
opts.rejectedToolCallIds = req.rejectedToolCallIds;
|
|
461
|
-
// Thread the shared projector + correlate each update back to this item.
|
|
462
|
-
if (options.streaming !== undefined)
|
|
463
|
-
opts.streaming = options.streaming;
|
|
464
|
-
if (req.key !== undefined)
|
|
465
|
-
opts.key = req.key;
|
|
466
|
-
if (options.onUpdate) {
|
|
467
|
-
const batchOnUpdate = options.onUpdate;
|
|
468
|
-
opts.onUpdate = (update) => batchOnUpdate(update, base);
|
|
469
|
-
}
|
|
470
|
-
const r = await Agent.resumeAsTool(req.subRunId, req.clientToolResults ?? [], opts);
|
|
471
|
-
if (r.kind === 'completed')
|
|
472
|
-
return { ...base, kind: 'completed', response: r.response };
|
|
473
|
-
return {
|
|
474
|
-
...base,
|
|
475
|
-
kind: 'paused',
|
|
476
|
-
subRunId: r.subRunId,
|
|
477
|
-
pauseKind: r.pauseKind,
|
|
478
|
-
pendingToolCallIds: r.pendingToolCallIds,
|
|
479
|
-
...(r.toolCall !== undefined ? { toolCall: r.toolCall } : {}),
|
|
480
|
-
...(r.isClientTool !== undefined ? { isClientTool: r.isClientTool } : {}),
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
if (onError === 'throw')
|
|
485
|
-
throw err;
|
|
486
|
-
return { ...base, kind: 'error', error: err instanceof Error ? err : new Error(String(err)) };
|
|
487
|
-
}
|
|
488
|
-
};
|
|
489
|
-
let results;
|
|
490
|
-
if (concurrency === 'serial') {
|
|
491
|
-
results = [];
|
|
492
|
-
for (const req of requests)
|
|
493
|
-
results.push(await runOne(req));
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
results = await Promise.all(requests.map(runOne));
|
|
497
|
-
}
|
|
498
|
-
const paused = results.filter((o) => o.kind === 'paused');
|
|
499
|
-
const completed = results.filter((o) => o.kind === 'completed');
|
|
500
|
-
const errors = results.filter((o) => o.kind === 'error');
|
|
501
|
-
return {
|
|
502
|
-
results,
|
|
503
|
-
paused,
|
|
504
|
-
completed,
|
|
505
|
-
errors,
|
|
506
|
-
pendingToolCallIds: paused.flatMap((p) => p.pendingToolCallIds),
|
|
507
|
-
allCompleted: paused.length === 0 && errors.length === 0,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Default projection from inner-agent stream chunks to {@link SubAgentUpdate}
|
|
513
|
-
* events. Emits one `tool_call` per inner `tool-call` chunk and
|
|
514
|
-
* `agent_pending_approval` per inner `pending-approval` chunk; everything
|
|
515
|
-
* else is suppressed (the wrapping execute emits the `agent_start` /
|
|
516
|
-
* `agent_done` bookends and the suspend paths emit `subagent_paused` /
|
|
517
|
-
* `subagent_paused_approval`).
|
|
518
|
-
*
|
|
519
|
-
* Hosts wanting different cadence (e.g. surfacing `text-delta` previews
|
|
520
|
-
* or per-step usage) pass `streaming: chunk => …` and own the discriminator.
|
|
521
|
-
*/
|
|
522
|
-
function defaultSubAgentProjector(chunk) {
|
|
523
|
-
if (chunk.type === 'tool-call' && chunk.toolCall?.name) {
|
|
524
|
-
return {
|
|
525
|
-
kind: 'tool_call',
|
|
526
|
-
tool: chunk.toolCall.name,
|
|
527
|
-
...(chunk.toolCall.arguments ? { args: chunk.toolCall.arguments } : {}),
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
if (chunk.type === 'pending-approval' && chunk.toolCall && chunk.toolCall.id && chunk.toolCall.name) {
|
|
531
|
-
return {
|
|
532
|
-
kind: 'agent_pending_approval',
|
|
533
|
-
toolCall: chunk.toolCall,
|
|
534
|
-
isClientTool: !!chunk.isClientTool,
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Reconstruct the inner-agent message history at the point the loop
|
|
541
|
-
* paused, so a subsequent {@link Agent.resumeAsTool} can rerun the loop
|
|
542
|
-
* with the appended client tool results. The shape is `[user, …(message
|
|
543
|
-
* + serverToolResults)*]` — system messages are omitted because the
|
|
544
|
-
* `messages` mode of the agent loop prepends `system` itself.
|
|
545
|
-
*
|
|
546
|
-
* Each step's `message` includes ALL `toolCalls` (server + client).
|
|
547
|
-
* Server-side `toolResults` are interleaved; client-side calls remain
|
|
548
|
-
* unfulfilled until resume appends their results.
|
|
549
|
-
*/
|
|
550
|
-
function buildSubAgentSnapshotMessages(userPrompt, response) {
|
|
551
|
-
const out = [{ role: 'user', content: userPrompt }];
|
|
552
|
-
for (const step of response.steps) {
|
|
553
|
-
out.push(step.message);
|
|
554
|
-
for (const tr of step.toolResults) {
|
|
555
|
-
const resultStr = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result);
|
|
556
|
-
out.push({ role: 'tool', content: resultStr, toolCallId: tr.toolCallId });
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return out;
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Snapshot reconstruction for a resume-time pause. The `priorMessages`
|
|
563
|
-
* already include the original user prompt + every step prior to the
|
|
564
|
-
* resume call. Append the freshly-completed steps' messages and any
|
|
565
|
-
* server-side tool results so the next resume sees the full history.
|
|
566
|
-
*/
|
|
567
|
-
function buildResumeSnapshotMessages(priorMessages, response) {
|
|
568
|
-
const out = [...priorMessages];
|
|
569
|
-
for (const step of response.steps) {
|
|
570
|
-
out.push(step.message);
|
|
571
|
-
for (const tr of step.toolResults) {
|
|
572
|
-
const resultStr = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result);
|
|
573
|
-
out.push({ role: 'tool', content: resultStr, toolCallId: tr.toolCallId });
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return out;
|
|
577
|
-
}
|
|
578
|
-
function generateSubRunId() {
|
|
579
|
-
if (typeof globalThis.crypto?.randomUUID === 'function')
|
|
580
|
-
return globalThis.crypto.randomUUID();
|
|
581
|
-
return `sub-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
582
|
-
}
|
|
583
|
-
// ─── Conversable Agent (conversation persistence) ───────
|
|
584
|
-
/**
|
|
585
|
-
* Wraps an Agent to add conversation memory.
|
|
586
|
-
* Created via `agent.forUser(id)` or `agent.continue(id)`.
|
|
587
|
-
*/
|
|
588
|
-
export class ConversableAgent {
|
|
589
|
-
agent;
|
|
590
|
-
_userId;
|
|
591
|
-
_conversationId;
|
|
592
|
-
constructor(agent) {
|
|
593
|
-
this.agent = agent;
|
|
594
|
-
}
|
|
595
|
-
forUser(userId) {
|
|
596
|
-
this._userId = userId;
|
|
597
|
-
return this;
|
|
598
|
-
}
|
|
599
|
-
continue(conversationId) {
|
|
600
|
-
this._conversationId = conversationId;
|
|
601
|
-
return this;
|
|
602
|
-
}
|
|
603
|
-
async prompt(input, options) {
|
|
604
|
-
const spec = this.toSpec();
|
|
605
|
-
return runWithPersistence(spec, this.agent.constructor.name, resolveConversationStore, input, options, (effOptions) => runAgentLoop(this.agent, input, effOptions)).then((r) => {
|
|
606
|
-
// Track the resolved id back on the wrapper so a subsequent
|
|
607
|
-
// `wrapper.prompt()` call resumes the same thread.
|
|
608
|
-
if (r.conversationId)
|
|
609
|
-
this._conversationId = r.conversationId;
|
|
610
|
-
return r;
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
stream(input, options) {
|
|
614
|
-
const spec = this.toSpec();
|
|
615
|
-
const persisted = runWithPersistenceStreaming(spec, this.agent.constructor.name, resolveConversationStore, input, options, (effOptions) => runAgentLoopStreaming(this.agent, input, effOptions));
|
|
616
|
-
// Update the wrapper's id once the run completes.
|
|
617
|
-
persisted.response.then((r) => { if (r.conversationId)
|
|
618
|
-
this._conversationId = r.conversationId; }, () => { });
|
|
619
|
-
return persisted;
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Translate the wrapper's explicit-form state (`forUser` / `continue`)
|
|
623
|
-
* into a {@link ConversationalSpec}. The explicit chain bypasses the
|
|
624
|
-
* agent's `conversational()` declaration entirely — `forUser` always
|
|
625
|
-
* wins over class defaults.
|
|
626
|
-
*/
|
|
627
|
-
toSpec() {
|
|
628
|
-
if (this._conversationId)
|
|
629
|
-
return { user: this._userId ?? '', id: this._conversationId };
|
|
630
|
-
if (this._userId)
|
|
631
|
-
return { user: this._userId };
|
|
632
|
-
throw new Error('[Rudder AI] ConversableAgent requires forUser() or continue() to be called before prompt().');
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
// ─── Anonymous Agent ─────────────────────────────────────
|
|
636
|
-
class AnonymousAgent extends Agent {
|
|
637
|
-
_instructions;
|
|
638
|
-
_tools;
|
|
639
|
-
_model;
|
|
640
|
-
_middleware;
|
|
641
|
-
constructor(options) {
|
|
642
|
-
super();
|
|
643
|
-
this._instructions = options.instructions;
|
|
644
|
-
this._tools = options.tools ?? [];
|
|
645
|
-
this._model = options.model;
|
|
646
|
-
this._middleware = options.middleware ?? [];
|
|
647
|
-
}
|
|
648
|
-
instructions() { return this._instructions; }
|
|
649
|
-
model() { return this._model; }
|
|
650
|
-
tools() { return this._tools; }
|
|
651
|
-
middleware() { return this._middleware; }
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* Create an anonymous agent inline.
|
|
655
|
-
*
|
|
656
|
-
* @example
|
|
657
|
-
* const response = await agent('You are helpful.').prompt('Hello')
|
|
658
|
-
*
|
|
659
|
-
* @example
|
|
660
|
-
* const response = await agent({
|
|
661
|
-
* instructions: 'You are a search assistant.',
|
|
662
|
-
* tools: [searchTool],
|
|
663
|
-
* model: 'anthropic/claude-sonnet-4-5',
|
|
664
|
-
* }).prompt('Find users named John')
|
|
665
|
-
*/
|
|
666
|
-
export function agent(instructionsOrOptions) {
|
|
667
|
-
const options = typeof instructionsOrOptions === 'string'
|
|
668
|
-
? { instructions: instructionsOrOptions }
|
|
669
|
-
: instructionsOrOptions;
|
|
670
|
-
return new AnonymousAgent(options);
|
|
671
|
-
}
|
|
672
|
-
// ─── Helpers ─────────────────────────────────────────────
|
|
673
|
-
// ─── Conversation Store Registry ────────────────────────
|
|
674
|
-
let _conversationStore;
|
|
675
|
-
/** Set the global conversation store (called by service provider or manually) */
|
|
676
|
-
export function setConversationStore(store) {
|
|
677
|
-
_conversationStore = store;
|
|
678
|
-
}
|
|
679
|
-
function resolveConversationStore() {
|
|
680
|
-
return _conversationStore;
|
|
681
|
-
}
|
|
682
|
-
// ─── User Memory Registry (#A4) ──────────────────────────
|
|
683
|
-
let _userMemory;
|
|
684
|
-
/**
|
|
685
|
-
* Set the global {@link UserMemory} (called by `AiProvider` from
|
|
686
|
-
* `AiConfig.memory`, or manually for tests / standalone setups).
|
|
687
|
-
* Phase 2/3 middleware reads it via `resolveUserMemory()` —
|
|
688
|
-
* imported by the persistence layer the same way
|
|
689
|
-
* `resolveConversationStore` is wired today.
|
|
690
|
-
*/
|
|
691
|
-
export function setUserMemory(memory) {
|
|
692
|
-
_userMemory = memory;
|
|
693
|
-
}
|
|
694
|
-
export function resolveUserMemory() {
|
|
695
|
-
return _userMemory;
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Streaming counterpart of `Agent.prompt`'s auto-persist branch. The spec
|
|
699
|
-
* resolution is async (since `conversational()` may return a Promise), so
|
|
700
|
-
* we defer the decision into the outer wrapper that handles the inner
|
|
701
|
-
* stream's setup the same way `runWithPersistenceStreaming` does for the
|
|
702
|
-
* persisted path.
|
|
703
|
-
*/
|
|
704
|
-
function runStreamWithMaybeAutoPersist(a, input, options) {
|
|
705
|
-
// Synchronous fast path — most agents override neither `conversational()`
|
|
706
|
-
// nor `remembers()`. Skip the async outer entirely when we can prove
|
|
707
|
-
// both are no-ops, sparing a microtask boundary per streaming call.
|
|
708
|
-
const declaredConv = a.conversational();
|
|
709
|
-
const declaredMem = a.remembers();
|
|
710
|
-
const isFast = ((options?.conversation === false ||
|
|
711
|
-
(declaredConv === false && options?.conversation === undefined))
|
|
712
|
-
&& (options?.memory === false ||
|
|
713
|
-
(declaredMem === false && options?.memory === undefined) ||
|
|
714
|
-
options?.messages !== undefined));
|
|
715
|
-
if (isFast) {
|
|
716
|
-
return runAgentLoopStreaming(a, input, options);
|
|
717
|
-
}
|
|
718
|
-
// Async path — resolve memory + conversation specs, then dispatch.
|
|
719
|
-
let resolveResp;
|
|
720
|
-
let rejectResp;
|
|
721
|
-
const responsePromise = new Promise((res, rej) => { resolveResp = res; rejectResp = rej; });
|
|
722
|
-
async function* outer() {
|
|
723
|
-
let effOptions;
|
|
724
|
-
let spec;
|
|
725
|
-
try {
|
|
726
|
-
// Memory auto-cascade BEFORE conversation persistence — same
|
|
727
|
-
// ordering as the non-streaming `Agent.prompt` path.
|
|
728
|
-
effOptions = await prepareOptionsWithMemoryAutoCascade(a, options);
|
|
729
|
-
spec = await resolveAutoPersistSpec(() => a.conversational(), effOptions?.conversation);
|
|
730
|
-
}
|
|
731
|
-
catch (err) {
|
|
732
|
-
rejectResp(err);
|
|
733
|
-
throw err;
|
|
734
|
-
}
|
|
735
|
-
if (!spec) {
|
|
736
|
-
const inner = runAgentLoopStreaming(a, input, effOptions);
|
|
737
|
-
try {
|
|
738
|
-
for await (const chunk of inner.stream)
|
|
739
|
-
yield chunk;
|
|
740
|
-
}
|
|
741
|
-
catch (err) {
|
|
742
|
-
rejectResp(err);
|
|
743
|
-
throw err;
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
const r = await inner.response;
|
|
747
|
-
resolveResp(r);
|
|
748
|
-
}
|
|
749
|
-
catch (err) {
|
|
750
|
-
rejectResp(err);
|
|
751
|
-
throw err;
|
|
752
|
-
}
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const persisted = runWithPersistenceStreaming(spec, a.constructor.name, resolveConversationStore, input, effOptions, (innerOptions) => runAgentLoopStreaming(a, input, innerOptions));
|
|
756
|
-
try {
|
|
757
|
-
for await (const chunk of persisted.stream)
|
|
758
|
-
yield chunk;
|
|
759
|
-
}
|
|
760
|
-
catch (err) {
|
|
761
|
-
rejectResp(err);
|
|
762
|
-
throw err;
|
|
763
|
-
}
|
|
764
|
-
try {
|
|
765
|
-
const r = await persisted.response;
|
|
766
|
-
resolveResp(r);
|
|
767
|
-
}
|
|
768
|
-
catch (err) {
|
|
769
|
-
rejectResp(err);
|
|
770
|
-
throw err;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
return { stream: outer(), response: responsePromise };
|
|
774
|
-
}
|
|
775
|
-
// ─── Helpers ─────────────────────────────────────────────
|
|
776
|
-
function hasToolsMethod(a) {
|
|
777
|
-
return 'tools' in a && typeof a.tools === 'function';
|
|
778
|
-
}
|
|
779
|
-
function getTools(a) {
|
|
780
|
-
return hasToolsMethod(a) ? a.tools() : [];
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Internal symbol used to plumb auto-installed middlewares (today:
|
|
784
|
-
* memory-inject; future: budget-tracker, etc.) through the public
|
|
785
|
-
* `AgentPromptOptions` without polluting its surface. Resolution
|
|
786
|
-
* happens at the `Agent.prompt` / `Agent.stream` boundary; the loop
|
|
787
|
-
* just appends them to the user's `agent.middleware()` array.
|
|
788
|
-
*/
|
|
789
|
-
const EXTRA_MIDDLEWARES = Symbol.for('rudderjs.ai.extraMiddlewares');
|
|
790
|
-
function hasMiddlewareMethod(a) {
|
|
791
|
-
return 'middleware' in a && typeof a.middleware === 'function';
|
|
792
|
-
}
|
|
793
|
-
function getMiddleware(a, options) {
|
|
794
|
-
const own = hasMiddlewareMethod(a) ? a.middleware() : [];
|
|
795
|
-
const extras = options?.[EXTRA_MIDDLEWARES] ?? [];
|
|
796
|
-
return extras.length > 0 ? [...own, ...extras] : own;
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Resolve the effective `remembers()` spec and append the appropriate
|
|
800
|
-
* memory middlewares (inject for Phase 2, extract for Phase 3) to the
|
|
801
|
-
* options' hidden extras list. Skips entirely on:
|
|
802
|
-
* - continuation calls (`options.messages` set) — the system message
|
|
803
|
-
* was already augmented on the original `prompt()`, re-injecting
|
|
804
|
-
* would duplicate the block on every tool round-trip; re-extracting
|
|
805
|
-
* would also double-write the same facts on every round-trip.
|
|
806
|
-
* - specs where neither `inject === 'auto'` nor `extract === 'auto'`
|
|
807
|
-
* apply.
|
|
808
|
-
*
|
|
809
|
-
* Returns options unchanged when no auto-cascade is needed so the
|
|
810
|
-
* downstream conversational/loop path sees the original reference.
|
|
811
|
-
*/
|
|
812
|
-
async function prepareOptionsWithMemoryAutoCascade(a, options) {
|
|
813
|
-
if (options?.messages)
|
|
814
|
-
return options;
|
|
815
|
-
const spec = await resolveRemembersSpec(() => a.remembers(), options?.memory);
|
|
816
|
-
if (!spec)
|
|
817
|
-
return options;
|
|
818
|
-
const installed = [];
|
|
819
|
-
if (spec.inject === 'auto')
|
|
820
|
-
installed.push(withMemoryInject(spec));
|
|
821
|
-
if (spec.extract === 'auto' && spec.extractWith)
|
|
822
|
-
installed.push(withMemoryExtract(spec));
|
|
823
|
-
if (installed.length === 0)
|
|
824
|
-
return options;
|
|
825
|
-
const current = options?.[EXTRA_MIDDLEWARES] ?? [];
|
|
826
|
-
return {
|
|
827
|
-
...options,
|
|
828
|
-
[EXTRA_MIDDLEWARES]: [...current, ...installed],
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
function createMiddlewareContext(messages, model, tools, iteration) {
|
|
832
|
-
const [provider] = AiRegistry.parseModelString(model);
|
|
833
|
-
let aborted = false;
|
|
834
|
-
let abortReason = '';
|
|
835
|
-
return {
|
|
836
|
-
requestId: typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `req-${Date.now()}`,
|
|
837
|
-
iteration,
|
|
838
|
-
chunkIndex: 0,
|
|
839
|
-
messages,
|
|
840
|
-
model,
|
|
841
|
-
provider,
|
|
842
|
-
toolNames: tools.map(t => t.definition.name),
|
|
843
|
-
abort(reason) {
|
|
844
|
-
aborted = true;
|
|
845
|
-
abortReason = reason ?? 'Aborted by middleware';
|
|
846
|
-
},
|
|
847
|
-
get _aborted() { return aborted; },
|
|
848
|
-
get _abortReason() { return abortReason; },
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
function buildUserMessage(input, attachments) {
|
|
852
|
-
if (!attachments?.length)
|
|
853
|
-
return { role: 'user', content: input };
|
|
854
|
-
const parts = [
|
|
855
|
-
{ type: 'text', text: input },
|
|
856
|
-
...attachmentsToContentParts(attachments),
|
|
857
|
-
];
|
|
858
|
-
return { role: 'user', content: parts };
|
|
859
|
-
}
|
|
860
|
-
function buildToolSchemas(tools) {
|
|
861
|
-
return tools.filter(t => !t.definition.lazy).map(toolToSchema);
|
|
862
|
-
}
|
|
863
|
-
function buildToolMap(tools) {
|
|
864
|
-
const map = new Map();
|
|
865
|
-
for (const t of tools)
|
|
866
|
-
map.set(t.definition.name, t);
|
|
867
|
-
return map;
|
|
868
|
-
}
|
|
869
|
-
function addUsage(total, step) {
|
|
870
|
-
total.promptTokens += step.promptTokens;
|
|
871
|
-
total.completionTokens += step.completionTokens;
|
|
872
|
-
total.totalTokens += step.totalTokens;
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* Merge two `TokenUsage` snapshots emitted within a single step, taking the
|
|
876
|
-
* MAX per field. Stream providers may emit usage in multiple chunks (e.g.
|
|
877
|
-
* Anthropic's `message_start` carries promptTokens, `message_delta` carries
|
|
878
|
-
* completionTokens) — a naive last-wins overwrite drops correct earlier
|
|
879
|
-
* values when later chunks under-report. MAX is safe because every chunk is
|
|
880
|
-
* a running snapshot, not a delta: token counts only ever grow within a step.
|
|
881
|
-
*
|
|
882
|
-
* @internal — exported for testing only.
|
|
883
|
-
*/
|
|
884
|
-
export function mergeUsage(a, b) {
|
|
885
|
-
return {
|
|
886
|
-
promptTokens: Math.max(a.promptTokens, b.promptTokens),
|
|
887
|
-
completionTokens: Math.max(a.completionTokens, b.completionTokens),
|
|
888
|
-
totalTokens: Math.max(a.totalTokens, b.totalTokens),
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
function buildMiddlewareConfig(messages, a) {
|
|
892
|
-
const config = { messages };
|
|
893
|
-
const temp = a.temperature();
|
|
894
|
-
const maxTok = a.maxTokens();
|
|
895
|
-
if (temp !== undefined)
|
|
896
|
-
config.temperature = temp;
|
|
897
|
-
if (maxTok !== undefined)
|
|
898
|
-
config.maxTokens = maxTok;
|
|
899
|
-
return config;
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Iterate the failover model list and invoke `call` against each provider
|
|
903
|
-
* adapter until one succeeds. Mutates `loopCtx.failoverAttempts` so the
|
|
904
|
-
* observer event reflects the real number of attempts. A caller-supplied
|
|
905
|
-
* `AbortSignal` short-circuits — abort errors propagate immediately rather
|
|
906
|
-
* than triggering the next failover model.
|
|
907
|
-
*/
|
|
908
|
-
async function runFailover(loopCtx, currentModel, call) {
|
|
909
|
-
const failoverModels = [currentModel, ...loopCtx.agent.failover().filter(m => m !== currentModel)];
|
|
910
|
-
let lastError;
|
|
911
|
-
for (const tryModel of failoverModels) {
|
|
912
|
-
try {
|
|
913
|
-
const adapter = AiRegistry.resolve(tryModel);
|
|
914
|
-
const [, modelId] = AiRegistry.parseModelString(tryModel);
|
|
915
|
-
const reqOptions = {
|
|
916
|
-
model: modelId,
|
|
917
|
-
messages: loopCtx.messages,
|
|
918
|
-
tools: loopCtx.toolSchemas.length > 0 ? loopCtx.toolSchemas : undefined,
|
|
919
|
-
temperature: loopCtx.agent.temperature(),
|
|
920
|
-
maxTokens: loopCtx.agent.maxTokens(),
|
|
921
|
-
signal: loopCtx.options?.signal,
|
|
922
|
-
cache: resolveCacheMarkers(loopCtx.agent, loopCtx.options),
|
|
923
|
-
};
|
|
924
|
-
return await call(adapter, modelId, reqOptions);
|
|
925
|
-
}
|
|
926
|
-
catch (err) {
|
|
927
|
-
// If the abort came from the caller, don't try the next failover
|
|
928
|
-
// model — re-throw so `prompt()` / the stream rejects immediately.
|
|
929
|
-
if (loopCtx.options?.signal?.aborted)
|
|
930
|
-
throw loopCtx.options.signal.reason;
|
|
931
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
932
|
-
loopCtx.failoverAttempts++;
|
|
933
|
-
if (tryModel === failoverModels[failoverModels.length - 1])
|
|
934
|
-
throw lastError;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
throw lastError ?? new Error('No provider available');
|
|
938
|
-
}
|
|
939
|
-
/**
|
|
940
|
-
* Merge agent-level `cacheable()` declaration with per-call override.
|
|
941
|
-
*
|
|
942
|
-
* - Per-call `cache: false` → returns `undefined` (caching disabled).
|
|
943
|
-
* - Per-call `cache: { ... }` → replaces the agent default.
|
|
944
|
-
* - Per-call omitted → uses `agent.cacheable()` unchanged.
|
|
945
|
-
*
|
|
946
|
-
* Returns `undefined` when no markers are set so the provider request
|
|
947
|
-
* carries no `cache` field at all.
|
|
948
|
-
*/
|
|
949
|
-
function resolveCacheMarkers(agent, options) {
|
|
950
|
-
if (options && options.cache === false)
|
|
951
|
-
return undefined;
|
|
952
|
-
const perCall = options?.cache === false ? undefined : options?.cache;
|
|
953
|
-
const config = perCall ?? agent.cacheable();
|
|
954
|
-
if (!config)
|
|
955
|
-
return undefined;
|
|
956
|
-
const markers = {};
|
|
957
|
-
if (config.instructions)
|
|
958
|
-
markers.instructions = true;
|
|
959
|
-
if (config.tools)
|
|
960
|
-
markers.tools = true;
|
|
961
|
-
if (config.messages !== undefined && config.messages > 0) {
|
|
962
|
-
markers.messages = Math.floor(config.messages);
|
|
963
|
-
}
|
|
964
|
-
if (config.ttl)
|
|
965
|
-
markers.ttl = config.ttl;
|
|
966
|
-
// ttl alone with no region markers is meaningless — drop it.
|
|
967
|
-
const hasRegion = markers.instructions || markers.tools || (markers.messages && markers.messages > 0);
|
|
968
|
-
if (!hasRegion)
|
|
969
|
-
return undefined;
|
|
970
|
-
return markers;
|
|
971
|
-
}
|
|
972
|
-
/** Emit the `agent.failed` observer event from the shared loop state. */
|
|
973
|
-
function emitObserverFailed(loopCtx, err, streaming) {
|
|
974
|
-
const obs = _getAiObservers();
|
|
975
|
-
if (!obs)
|
|
976
|
-
return;
|
|
977
|
-
const inputText = loopCtx.options?.messages ? '' : loopCtx.input;
|
|
978
|
-
obs.emit({
|
|
979
|
-
kind: 'agent.failed',
|
|
980
|
-
agentName: loopCtx.agent.constructor.name,
|
|
981
|
-
model: loopCtx.modelString,
|
|
982
|
-
provider: loopCtx.providerName,
|
|
983
|
-
input: inputText,
|
|
984
|
-
output: '',
|
|
985
|
-
steps: _buildObserverSteps(loopCtx.steps, loopCtx.modelString),
|
|
986
|
-
tokens: {
|
|
987
|
-
prompt: loopCtx.totalUsage.promptTokens,
|
|
988
|
-
completion: loopCtx.totalUsage.completionTokens,
|
|
989
|
-
total: loopCtx.totalUsage.totalTokens,
|
|
990
|
-
},
|
|
991
|
-
duration: Math.round(performance.now() - loopCtx.loopStart),
|
|
992
|
-
finishReason: 'error',
|
|
993
|
-
streaming,
|
|
994
|
-
conversationId: null,
|
|
995
|
-
failoverAttempts: loopCtx.failoverAttempts,
|
|
996
|
-
error: err instanceof Error ? err.message : String(err),
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Emit the per-step `agent.step.completed` observer event after each
|
|
1001
|
-
* iteration. Built from the SAME `_buildObserverSteps` mapping used by
|
|
1002
|
-
* the terminal events so consumers see consistent shapes — they just see
|
|
1003
|
-
* the latest step rather than the full array.
|
|
1004
|
-
*/
|
|
1005
|
-
function emitObserverStepCompleted(loopCtx, iteration, streaming) {
|
|
1006
|
-
const obs = _getAiObservers();
|
|
1007
|
-
if (!obs)
|
|
1008
|
-
return;
|
|
1009
|
-
const justPushed = loopCtx.steps[loopCtx.steps.length - 1];
|
|
1010
|
-
if (!justPushed)
|
|
1011
|
-
return;
|
|
1012
|
-
// Re-use _buildObserverSteps so the per-step shape matches the steps[]
|
|
1013
|
-
// entries on the terminal events. Pass a single-element slice since we
|
|
1014
|
-
// only need the latest step's mapping.
|
|
1015
|
-
const built = _buildObserverSteps([justPushed], loopCtx.modelString);
|
|
1016
|
-
const stepEvent = built[0];
|
|
1017
|
-
if (!stepEvent)
|
|
1018
|
-
return;
|
|
1019
|
-
// Override iteration with the loop's iteration counter — _buildObserverSteps
|
|
1020
|
-
// numbers from 1 within the array it sees, but we want the global step
|
|
1021
|
-
// number across the whole run.
|
|
1022
|
-
stepEvent.iteration = iteration + 1;
|
|
1023
|
-
obs.emit({
|
|
1024
|
-
kind: 'agent.step.completed',
|
|
1025
|
-
agentName: loopCtx.agent.constructor.name,
|
|
1026
|
-
model: loopCtx.modelString,
|
|
1027
|
-
provider: loopCtx.providerName,
|
|
1028
|
-
iteration: iteration + 1,
|
|
1029
|
-
step: stepEvent,
|
|
1030
|
-
tokens: {
|
|
1031
|
-
prompt: loopCtx.totalUsage.promptTokens,
|
|
1032
|
-
completion: loopCtx.totalUsage.completionTokens,
|
|
1033
|
-
total: loopCtx.totalUsage.totalTokens,
|
|
1034
|
-
},
|
|
1035
|
-
duration: Math.round(performance.now() - loopCtx.loopStart),
|
|
1036
|
-
streaming,
|
|
1037
|
-
conversationId: null,
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
/** Emit the `agent.completed` observer event from the shared loop state. */
|
|
1041
|
-
function emitObserverCompleted(loopCtx, result, streaming) {
|
|
1042
|
-
const obs = _getAiObservers();
|
|
1043
|
-
if (!obs)
|
|
1044
|
-
return;
|
|
1045
|
-
const inputText = loopCtx.options?.messages ? '' : loopCtx.input;
|
|
1046
|
-
const lastStep = loopCtx.steps[loopCtx.steps.length - 1];
|
|
1047
|
-
obs.emit({
|
|
1048
|
-
kind: 'agent.completed',
|
|
1049
|
-
agentName: loopCtx.agent.constructor.name,
|
|
1050
|
-
model: loopCtx.modelString,
|
|
1051
|
-
provider: loopCtx.providerName,
|
|
1052
|
-
input: inputText,
|
|
1053
|
-
output: result.text,
|
|
1054
|
-
steps: _buildObserverSteps(loopCtx.steps, loopCtx.modelString),
|
|
1055
|
-
tokens: {
|
|
1056
|
-
prompt: loopCtx.totalUsage.promptTokens,
|
|
1057
|
-
completion: loopCtx.totalUsage.completionTokens,
|
|
1058
|
-
total: loopCtx.totalUsage.totalTokens,
|
|
1059
|
-
},
|
|
1060
|
-
duration: Math.round(performance.now() - loopCtx.loopStart),
|
|
1061
|
-
finishReason: result.finishReason ?? lastStep?.finishReason ?? 'stop',
|
|
1062
|
-
streaming,
|
|
1063
|
-
conversationId: null,
|
|
1064
|
-
failoverAttempts: loopCtx.failoverAttempts,
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
/** Build the final `AgentResponse` from accumulated loop state. */
|
|
1068
|
-
function buildAgentResponse(loopCtx) {
|
|
1069
|
-
const lastStep = loopCtx.steps[loopCtx.steps.length - 1];
|
|
1070
|
-
const result = {
|
|
1071
|
-
text: lastStep ? getMessageText(lastStep.message.content) : '',
|
|
1072
|
-
steps: loopCtx.steps,
|
|
1073
|
-
usage: loopCtx.totalUsage,
|
|
1074
|
-
};
|
|
1075
|
-
if (loopCtx.loopFinishReason)
|
|
1076
|
-
result.finishReason = loopCtx.loopFinishReason;
|
|
1077
|
-
if (loopCtx.pendingClientToolCalls.length > 0)
|
|
1078
|
-
result.pendingClientToolCalls = loopCtx.pendingClientToolCalls;
|
|
1079
|
-
if (loopCtx.pendingApprovalToolCall)
|
|
1080
|
-
result.pendingApprovalToolCall = loopCtx.pendingApprovalToolCall;
|
|
1081
|
-
if (loopCtx.resumedToolMessages.length > 0)
|
|
1082
|
-
result.resumedToolMessages = loopCtx.resumedToolMessages;
|
|
1083
|
-
// Internal — consumed by the handoff-aware wrapper, then stripped before
|
|
1084
|
-
// surfacing to public callers.
|
|
1085
|
-
if (loopCtx.pendingHandoff) {
|
|
1086
|
-
result._pendingHandoff = loopCtx.pendingHandoff;
|
|
1087
|
-
result._carriedMessages = loopCtx.messages;
|
|
1088
|
-
}
|
|
1089
|
-
return result;
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Build the shared `LoopContext` for a `prompt()` / `stream()` call, run
|
|
1093
|
-
* approval-resume, and fire `onConfig(init)` + `onStart`. After this returns,
|
|
1094
|
-
* the iteration loop can run with the same setup regardless of streaming
|
|
1095
|
-
* mode.
|
|
1096
|
-
*/
|
|
1097
|
-
async function initializeLoop(a, input, options) {
|
|
1098
|
-
// Honor caller-supplied AbortSignal as early as possible — if the signal
|
|
1099
|
-
// is already aborted on entry, do no work at all.
|
|
1100
|
-
options?.signal?.throwIfAborted();
|
|
1101
|
-
const loopStart = performance.now();
|
|
1102
|
-
const modelString = a.model() ?? AiRegistry.getDefault();
|
|
1103
|
-
const [providerName] = AiRegistry.parseModelString(modelString);
|
|
1104
|
-
const tools = getTools(a);
|
|
1105
|
-
const middlewares = getMiddleware(a, options);
|
|
1106
|
-
const toolSchemas = buildToolSchemas(tools);
|
|
1107
|
-
const toolMap = buildToolMap(tools);
|
|
1108
|
-
const messages = options?.messages
|
|
1109
|
-
? [{ role: 'system', content: a.instructions() }, ...options.messages]
|
|
1110
|
-
: [
|
|
1111
|
-
{ role: 'system', content: a.instructions() },
|
|
1112
|
-
...(options?.history ?? []),
|
|
1113
|
-
buildUserMessage(input, options?.attachments),
|
|
1114
|
-
];
|
|
1115
|
-
const steps = [];
|
|
1116
|
-
const stopConditions = normalizeStopConditions(a.stopWhen());
|
|
1117
|
-
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1118
|
-
// Create middleware context (resume below mutates `messages`, captured by
|
|
1119
|
-
// reference here, so order is safe).
|
|
1120
|
-
const ctx = createMiddlewareContext(messages, modelString, tools, 0);
|
|
1121
|
-
const loopCtx = {
|
|
1122
|
-
agent: a,
|
|
1123
|
-
input,
|
|
1124
|
-
options,
|
|
1125
|
-
modelString,
|
|
1126
|
-
providerName,
|
|
1127
|
-
tools,
|
|
1128
|
-
toolMap,
|
|
1129
|
-
toolSchemas,
|
|
1130
|
-
middlewares,
|
|
1131
|
-
loopStart,
|
|
1132
|
-
ctx,
|
|
1133
|
-
messages,
|
|
1134
|
-
steps,
|
|
1135
|
-
totalUsage,
|
|
1136
|
-
pendingClientToolCalls: [],
|
|
1137
|
-
pendingApprovalToolCall: undefined,
|
|
1138
|
-
loopFinishReason: undefined,
|
|
1139
|
-
stopForClientTools: false,
|
|
1140
|
-
stopForApproval: false,
|
|
1141
|
-
resumedToolMessages: [],
|
|
1142
|
-
failoverAttempts: 0,
|
|
1143
|
-
stopForHandoff: false,
|
|
1144
|
-
};
|
|
1145
|
-
// Resume server tools left pending by a previous approval round-trip.
|
|
1146
|
-
{
|
|
1147
|
-
const resume = await resumePendingToolCalls({ messages, toolMap, options });
|
|
1148
|
-
loopCtx.resumedToolMessages = resume.resumed;
|
|
1149
|
-
if (resume.approvalStillRequired) {
|
|
1150
|
-
loopCtx.pendingApprovalToolCall = resume.approvalStillRequired;
|
|
1151
|
-
loopCtx.loopFinishReason = 'tool_approval_required';
|
|
1152
|
-
loopCtx.stopForApproval = true;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
// onConfig — init phase
|
|
1156
|
-
if (middlewares.length > 0) {
|
|
1157
|
-
const configResult = runOnConfig(middlewares, ctx, buildMiddlewareConfig(messages, a), 'init');
|
|
1158
|
-
if (configResult.messages)
|
|
1159
|
-
messages.splice(0, messages.length, ...configResult.messages);
|
|
1160
|
-
}
|
|
1161
|
-
// onStart
|
|
1162
|
-
if (middlewares.length > 0)
|
|
1163
|
-
await runSequential(middlewares, 'onStart', ctx);
|
|
1164
|
-
return { loopCtx, stopConditions };
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Run the per-iteration prelude — caller-abort check, middleware-abort
|
|
1168
|
-
* check, `onIteration`, `prepareStep`, `onConfig(beforeModel)`. Returns the
|
|
1169
|
-
* resolved model for this step or `{ aborted: true }` if middleware
|
|
1170
|
-
* cancelled the run (caller should `break`). Throws the abort reason if a
|
|
1171
|
-
* caller-supplied AbortSignal fired between iterations.
|
|
1172
|
-
*/
|
|
1173
|
-
async function runIterationPrelude(loopCtx, iteration) {
|
|
1174
|
-
const { agent, options, ctx, middlewares, messages, modelString, steps } = loopCtx;
|
|
1175
|
-
ctx.iteration = iteration;
|
|
1176
|
-
// Reset the streaming chunk index for middlewares that key off it. Harmless
|
|
1177
|
-
// in non-streaming mode where no chunks flow through `onChunk`.
|
|
1178
|
-
ctx.chunkIndex = 0;
|
|
1179
|
-
// Honor caller-supplied AbortSignal between iterations.
|
|
1180
|
-
options?.signal?.throwIfAborted();
|
|
1181
|
-
if (ctx._aborted) {
|
|
1182
|
-
await runOnAbort(middlewares, ctx, ctx._abortReason);
|
|
1183
|
-
return { aborted: true };
|
|
1184
|
-
}
|
|
1185
|
-
if (middlewares.length > 0)
|
|
1186
|
-
await runSequential(middlewares, 'onIteration', ctx);
|
|
1187
|
-
let currentModel = modelString;
|
|
1188
|
-
if (agent.prepareStep) {
|
|
1189
|
-
const prep = await agent.prepareStep({ stepNumber: iteration, steps, messages });
|
|
1190
|
-
if (prep.model)
|
|
1191
|
-
currentModel = prep.model;
|
|
1192
|
-
if (prep.messages)
|
|
1193
|
-
messages.splice(0, messages.length, ...prep.messages);
|
|
1194
|
-
if (prep.system)
|
|
1195
|
-
messages[0] = { role: 'system', content: prep.system };
|
|
1196
|
-
}
|
|
1197
|
-
if (middlewares.length > 0) {
|
|
1198
|
-
const configResult = runOnConfig(middlewares, ctx, buildMiddlewareConfig(messages, agent), 'beforeModel');
|
|
1199
|
-
if (configResult.messages)
|
|
1200
|
-
messages.splice(0, messages.length, ...configResult.messages);
|
|
1201
|
-
}
|
|
1202
|
-
return { currentModel };
|
|
1203
|
-
}
|
|
1204
|
-
// ─── Agent Loop (non-streaming) ──────────────────────────
|
|
1205
|
-
/**
|
|
1206
|
-
* Public entry point for the non-streaming agent loop. Drives
|
|
1207
|
-
* {@link runAgentLoopOnce} once, then — if the model called a {@link handoff}
|
|
1208
|
-
* tool — constructs the target agent, carries the conversation forward, and
|
|
1209
|
-
* recurses. Steps and usage from each hop are merged; the final `text` and
|
|
1210
|
-
* `finishReason` come from the agent that produced the terminal answer.
|
|
1211
|
-
* `handoffPath` records the chain of class names traversed.
|
|
1212
|
-
*/
|
|
1213
|
-
async function runAgentLoop(a, input, options) {
|
|
1214
|
-
const onceResult = await runAgentLoopOnce(a, input, options);
|
|
1215
|
-
if (!onceResult._pendingHandoff) {
|
|
1216
|
-
return stripInternal(onceResult);
|
|
1217
|
-
}
|
|
1218
|
-
const merged = await driveHandoffs(a.constructor.name, onceResult, onceResult._pendingHandoff, onceResult._carriedMessages ?? [], options, 0, runAgentLoopOnce);
|
|
1219
|
-
return merged;
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Streaming counterpart to {@link runAgentLoop}. Iterates handoffs and
|
|
1223
|
-
* pivots the stream to the next agent each time the parent ends with a
|
|
1224
|
-
* pending handoff. Chunks from every hop flow through the same returned
|
|
1225
|
-
* `AsyncIterable`; the resolved `response` carries the merged final state.
|
|
1226
|
-
*/
|
|
1227
|
-
function runAgentLoopStreaming(a, input, options) {
|
|
1228
|
-
let resolveResponse;
|
|
1229
|
-
let rejectResponse;
|
|
1230
|
-
const responsePromise = new Promise((resolve, reject) => {
|
|
1231
|
-
resolveResponse = resolve;
|
|
1232
|
-
rejectResponse = reject;
|
|
1233
|
-
});
|
|
1234
|
-
async function* generateStream() {
|
|
1235
|
-
let currentAgent = a;
|
|
1236
|
-
let currentInput = input;
|
|
1237
|
-
let currentOpts = options;
|
|
1238
|
-
const mergedSteps = [];
|
|
1239
|
-
const mergedUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1240
|
-
const handoffPath = [];
|
|
1241
|
-
let finalResponse;
|
|
1242
|
-
for (let hop = 0; hop <= MAX_HANDOFFS; hop++) {
|
|
1243
|
-
const onceStream = runAgentLoopStreamingOnce(currentAgent, currentInput, currentOpts);
|
|
1244
|
-
// Attach a no-op handler so a rejection from the inner response
|
|
1245
|
-
// promise (e.g. caller-supplied AbortSignal firing mid-stream) is
|
|
1246
|
-
// already observed by the time the `for await` re-throws — without
|
|
1247
|
-
// this, Node logs an unhandledRejection between the stream's throw
|
|
1248
|
-
// and our outer `withRejectOnError`'s catch.
|
|
1249
|
-
onceStream.response.catch(() => { });
|
|
1250
|
-
for await (const chunk of onceStream.stream)
|
|
1251
|
-
yield chunk;
|
|
1252
|
-
const r = await onceStream.response;
|
|
1253
|
-
mergedSteps.push(...r.steps);
|
|
1254
|
-
addUsage(mergedUsage, r.usage);
|
|
1255
|
-
if (r._pendingHandoff && hop < MAX_HANDOFFS) {
|
|
1256
|
-
handoffPath.push(currentAgent.constructor.name);
|
|
1257
|
-
const ChildClass = r._pendingHandoff.spec.AgentClass;
|
|
1258
|
-
currentAgent = new ChildClass();
|
|
1259
|
-
currentInput = r._pendingHandoff.transitionMessage;
|
|
1260
|
-
currentOpts = buildHandoffChildOptions(options, r._carriedMessages ?? []);
|
|
1261
|
-
continue;
|
|
1262
|
-
}
|
|
1263
|
-
if (r._pendingHandoff) {
|
|
1264
|
-
throw new Error(`[Rudder AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
|
|
1265
|
-
}
|
|
1266
|
-
finalResponse = handoffPath.length === 0
|
|
1267
|
-
? stripInternal(r)
|
|
1268
|
-
: mergeFinalHandoff(stripInternal(r), mergedSteps, mergedUsage, handoffPath, currentAgent.constructor.name);
|
|
1269
|
-
break;
|
|
1270
|
-
}
|
|
1271
|
-
if (!finalResponse) {
|
|
1272
|
-
throw new Error(`[Rudder AI] Exceeded max handoffs (${MAX_HANDOFFS}). Likely a cycle between agents.`);
|
|
1273
|
-
}
|
|
1274
|
-
resolveResponse(finalResponse);
|
|
1275
|
-
}
|
|
1276
|
-
async function* withRejectOnError() {
|
|
1277
|
-
try {
|
|
1278
|
-
yield* generateStream();
|
|
1279
|
-
}
|
|
1280
|
-
catch (err) {
|
|
1281
|
-
rejectResponse(err);
|
|
1282
|
-
throw err;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
return {
|
|
1286
|
-
stream: withRejectOnError(),
|
|
1287
|
-
response: responsePromise,
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
async function runAgentLoopOnce(a, input, options) {
|
|
1291
|
-
const { loopCtx, stopConditions } = await initializeLoop(a, input, options);
|
|
1292
|
-
const { ctx, middlewares, messages, steps, totalUsage } = loopCtx;
|
|
1293
|
-
try {
|
|
1294
|
-
if (loopCtx.stopForApproval) {
|
|
1295
|
-
// Approval is still required from the resume — skip the model loop.
|
|
1296
|
-
}
|
|
1297
|
-
else {
|
|
1298
|
-
for (let iteration = 0; iteration < a.maxSteps(); iteration++) {
|
|
1299
|
-
const prelude = await runIterationPrelude(loopCtx, iteration);
|
|
1300
|
-
if ('aborted' in prelude)
|
|
1301
|
-
break;
|
|
1302
|
-
const { currentModel } = prelude;
|
|
1303
|
-
const response = await runFailover(loopCtx, currentModel, (adapter, _, opts) => adapter.generate(opts));
|
|
1304
|
-
addUsage(totalUsage, response.usage);
|
|
1305
|
-
// onUsage
|
|
1306
|
-
if (middlewares.length > 0)
|
|
1307
|
-
await runOnUsage(middlewares, ctx, response.usage);
|
|
1308
|
-
const toolCalls = response.message.toolCalls ?? [];
|
|
1309
|
-
let toolResults = [];
|
|
1310
|
-
if (toolCalls.length > 0) {
|
|
1311
|
-
// Drain `executeToolPhase` to completion, discarding the streamed
|
|
1312
|
-
// chunks — non-streaming callers don't surface them.
|
|
1313
|
-
const phaseGen = executeToolPhase(loopCtx, toolCalls, response.message);
|
|
1314
|
-
while (true) {
|
|
1315
|
-
const next = await phaseGen.next();
|
|
1316
|
-
if (next.done) {
|
|
1317
|
-
toolResults = next.value;
|
|
1318
|
-
break;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
else {
|
|
1323
|
-
messages.push(response.message);
|
|
1324
|
-
}
|
|
1325
|
-
const step = {
|
|
1326
|
-
message: response.message,
|
|
1327
|
-
toolCalls,
|
|
1328
|
-
toolResults,
|
|
1329
|
-
usage: response.usage,
|
|
1330
|
-
finishReason: response.finishReason,
|
|
1331
|
-
};
|
|
1332
|
-
steps.push(step);
|
|
1333
|
-
emitObserverStepCompleted(loopCtx, iteration, false);
|
|
1334
|
-
if (loopCtx.stopForClientTools || loopCtx.stopForApproval || loopCtx.stopForHandoff)
|
|
1335
|
-
break;
|
|
1336
|
-
const shouldStop = stopConditions.some(cond => cond({ steps, iteration, lastMessage: response.message }));
|
|
1337
|
-
if (shouldStop || response.finishReason !== 'tool_calls') {
|
|
1338
|
-
break;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
} // close `else` (skip-loop-when-resume-needs-approval)
|
|
1342
|
-
}
|
|
1343
|
-
catch (err) {
|
|
1344
|
-
// onError
|
|
1345
|
-
if (middlewares.length > 0)
|
|
1346
|
-
await runOnError(middlewares, ctx, err);
|
|
1347
|
-
emitObserverFailed(loopCtx, err, false);
|
|
1348
|
-
throw err;
|
|
1349
|
-
}
|
|
1350
|
-
// onFinish
|
|
1351
|
-
if (middlewares.length > 0)
|
|
1352
|
-
await runSequential(middlewares, 'onFinish', ctx);
|
|
1353
|
-
const result = buildAgentResponse(loopCtx);
|
|
1354
|
-
emitObserverCompleted(loopCtx, result, false);
|
|
1355
|
-
return result;
|
|
1356
|
-
}
|
|
1357
|
-
// ─── Agent Loop (streaming) ──────────────────────────────
|
|
1358
|
-
function runAgentLoopStreamingOnce(a, input, options) {
|
|
1359
|
-
let resolveResponse;
|
|
1360
|
-
let rejectResponse;
|
|
1361
|
-
const responsePromise = new Promise((resolve, reject) => {
|
|
1362
|
-
resolveResponse = resolve;
|
|
1363
|
-
rejectResponse = reject;
|
|
1364
|
-
});
|
|
1365
|
-
async function* generateStream() {
|
|
1366
|
-
const { loopCtx, stopConditions } = await initializeLoop(a, input, options);
|
|
1367
|
-
const { ctx, middlewares, messages, steps, totalUsage } = loopCtx;
|
|
1368
|
-
try {
|
|
1369
|
-
if (loopCtx.stopForApproval) {
|
|
1370
|
-
// Resume detected unfulfilled approval — skip the model loop entirely.
|
|
1371
|
-
}
|
|
1372
|
-
else {
|
|
1373
|
-
for (let iteration = 0; iteration < a.maxSteps(); iteration++) {
|
|
1374
|
-
const prelude = await runIterationPrelude(loopCtx, iteration);
|
|
1375
|
-
if ('aborted' in prelude)
|
|
1376
|
-
break;
|
|
1377
|
-
const { currentModel } = prelude;
|
|
1378
|
-
const streamSource = await runFailover(loopCtx, currentModel, (adapter, _, opts) => adapter.stream(opts));
|
|
1379
|
-
let text = '';
|
|
1380
|
-
let currentToolCalls = [];
|
|
1381
|
-
let stepUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1382
|
-
let finishReason = 'stop';
|
|
1383
|
-
const partialToolCalls = new Map();
|
|
1384
|
-
// Parallel-arg routing — OpenAI streams ≥2 tool calls interleaved by
|
|
1385
|
-
// `index`. Without an index-keyed map the previous "pop last partial"
|
|
1386
|
-
// attached `index=1`'s arg fragments to `index=0`'s partial (or vice
|
|
1387
|
-
// versa), producing `{}` args or wrong args silently. Partials live in
|
|
1388
|
-
// both maps by reference, so the final `JSON.parse` loop below
|
|
1389
|
-
// continues to read from `partialToolCalls`.
|
|
1390
|
-
const partialsByIndex = new Map();
|
|
1391
|
-
for await (const chunk of streamSource) {
|
|
1392
|
-
// onChunk — middleware can transform or drop chunks
|
|
1393
|
-
let processedChunk = chunk;
|
|
1394
|
-
if (middlewares.length > 0) {
|
|
1395
|
-
processedChunk = runOnChunk(middlewares, ctx, chunk);
|
|
1396
|
-
ctx.chunkIndex++;
|
|
1397
|
-
}
|
|
1398
|
-
if (processedChunk)
|
|
1399
|
-
yield processedChunk;
|
|
1400
|
-
// Always process the original chunk for state tracking
|
|
1401
|
-
if (chunk.type === 'text-delta' && chunk.text) {
|
|
1402
|
-
text += chunk.text;
|
|
1403
|
-
}
|
|
1404
|
-
else if (chunk.type === 'tool-call-delta' && chunk.toolCall?.id) {
|
|
1405
|
-
const partial = {
|
|
1406
|
-
id: chunk.toolCall.id,
|
|
1407
|
-
name: chunk.toolCall.name ?? '',
|
|
1408
|
-
argChunks: [],
|
|
1409
|
-
};
|
|
1410
|
-
partialToolCalls.set(chunk.toolCall.id, partial);
|
|
1411
|
-
if (typeof chunk.toolCallIndex === 'number') {
|
|
1412
|
-
partialsByIndex.set(chunk.toolCallIndex, partial);
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
else if (chunk.type === 'tool-call-delta' && chunk.text) {
|
|
1416
|
-
// Route arg-delta to the matching partial by `toolCallIndex` when
|
|
1417
|
-
// the adapter provides it (OpenAI). Fall back to the most recent
|
|
1418
|
-
// partial only for adapters that don't track index — those don't
|
|
1419
|
-
// currently stream parallel tool calls via arg-only deltas, so
|
|
1420
|
-
// the legacy path is still safe.
|
|
1421
|
-
const partial = typeof chunk.toolCallIndex === 'number'
|
|
1422
|
-
? partialsByIndex.get(chunk.toolCallIndex)
|
|
1423
|
-
: Array.from(partialToolCalls.values()).pop();
|
|
1424
|
-
if (partial)
|
|
1425
|
-
partial.argChunks.push(chunk.text);
|
|
1426
|
-
}
|
|
1427
|
-
else if (chunk.type === 'tool-call' && chunk.toolCall) {
|
|
1428
|
-
const tc = chunk.toolCall;
|
|
1429
|
-
currentToolCalls.push(tc);
|
|
1430
|
-
}
|
|
1431
|
-
else if (chunk.type === 'usage' && chunk.usage) {
|
|
1432
|
-
stepUsage = mergeUsage(stepUsage, chunk.usage);
|
|
1433
|
-
}
|
|
1434
|
-
else if (chunk.type === 'finish') {
|
|
1435
|
-
if (chunk.usage)
|
|
1436
|
-
stepUsage = mergeUsage(stepUsage, chunk.usage);
|
|
1437
|
-
finishReason = chunk.finishReason ?? 'stop';
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
// Finalize partial tool calls
|
|
1441
|
-
for (const [, partial] of partialToolCalls) {
|
|
1442
|
-
try {
|
|
1443
|
-
const args = JSON.parse(partial.argChunks.join(''));
|
|
1444
|
-
currentToolCalls.push({ id: partial.id, name: partial.name, arguments: args });
|
|
1445
|
-
}
|
|
1446
|
-
catch {
|
|
1447
|
-
currentToolCalls.push({ id: partial.id, name: partial.name, arguments: {} });
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
addUsage(totalUsage, stepUsage);
|
|
1451
|
-
// onUsage
|
|
1452
|
-
if (middlewares.length > 0)
|
|
1453
|
-
await runOnUsage(middlewares, ctx, stepUsage);
|
|
1454
|
-
let toolResults = [];
|
|
1455
|
-
if (currentToolCalls.length > 0) {
|
|
1456
|
-
const assistantMsg = { role: 'assistant', content: text, toolCalls: currentToolCalls };
|
|
1457
|
-
// Forward chunks from the shared tool-phase generator straight
|
|
1458
|
-
// through to the stream consumer.
|
|
1459
|
-
const phaseGen = executeToolPhase(loopCtx, currentToolCalls, assistantMsg);
|
|
1460
|
-
while (true) {
|
|
1461
|
-
const next = await phaseGen.next();
|
|
1462
|
-
if (next.done) {
|
|
1463
|
-
toolResults = next.value;
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1466
|
-
yield next.value;
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
else {
|
|
1470
|
-
messages.push({ role: 'assistant', content: text });
|
|
1471
|
-
}
|
|
1472
|
-
const step = {
|
|
1473
|
-
message: { role: 'assistant', content: text, ...(currentToolCalls.length > 0 ? { toolCalls: currentToolCalls } : {}) },
|
|
1474
|
-
toolCalls: currentToolCalls,
|
|
1475
|
-
toolResults,
|
|
1476
|
-
usage: stepUsage,
|
|
1477
|
-
finishReason,
|
|
1478
|
-
};
|
|
1479
|
-
steps.push(step);
|
|
1480
|
-
emitObserverStepCompleted(loopCtx, iteration, true);
|
|
1481
|
-
if (loopCtx.stopForClientTools || loopCtx.stopForApproval || loopCtx.stopForHandoff)
|
|
1482
|
-
break;
|
|
1483
|
-
const shouldStop = stopConditions.some(cond => cond({ steps, iteration, lastMessage: step.message }));
|
|
1484
|
-
if (shouldStop || finishReason !== 'tool_calls')
|
|
1485
|
-
break;
|
|
1486
|
-
// Reset for next iteration
|
|
1487
|
-
text = '';
|
|
1488
|
-
currentToolCalls = [];
|
|
1489
|
-
}
|
|
1490
|
-
} // close `else` (skip-loop-when-resume-needs-approval)
|
|
1491
|
-
}
|
|
1492
|
-
catch (err) {
|
|
1493
|
-
// onError
|
|
1494
|
-
if (middlewares.length > 0)
|
|
1495
|
-
await runOnError(middlewares, ctx, err);
|
|
1496
|
-
emitObserverFailed(loopCtx, err, true);
|
|
1497
|
-
throw err;
|
|
1498
|
-
}
|
|
1499
|
-
// onFinish
|
|
1500
|
-
if (middlewares.length > 0)
|
|
1501
|
-
await runSequential(middlewares, 'onFinish', ctx);
|
|
1502
|
-
// Emit pending state to consumers via dedicated chunk types
|
|
1503
|
-
if (loopCtx.pendingClientToolCalls.length > 0) {
|
|
1504
|
-
yield { type: 'pending-client-tools', toolCalls: loopCtx.pendingClientToolCalls };
|
|
1505
|
-
}
|
|
1506
|
-
if (loopCtx.pendingApprovalToolCall) {
|
|
1507
|
-
yield {
|
|
1508
|
-
type: 'pending-approval',
|
|
1509
|
-
toolCall: loopCtx.pendingApprovalToolCall.toolCall,
|
|
1510
|
-
isClientTool: loopCtx.pendingApprovalToolCall.isClientTool,
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
const result = buildAgentResponse(loopCtx);
|
|
1514
|
-
emitObserverCompleted(loopCtx, result, true);
|
|
1515
|
-
resolveResponse(result);
|
|
1516
|
-
}
|
|
1517
|
-
// Outer wrapper: if `generateStream` throws (e.g. the caller's
|
|
1518
|
-
// AbortSignal fired), reject the `response` promise with the same
|
|
1519
|
-
// reason BEFORE re-throwing into the for-await consumer. Without this,
|
|
1520
|
-
// `await response` would hang forever after a mid-stream abort.
|
|
1521
|
-
async function* withRejectOnError() {
|
|
1522
|
-
try {
|
|
1523
|
-
yield* generateStream();
|
|
1524
|
-
}
|
|
1525
|
-
catch (err) {
|
|
1526
|
-
rejectResponse(err);
|
|
1527
|
-
throw err;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
return {
|
|
1531
|
-
stream: withRejectOnError(),
|
|
1532
|
-
response: responsePromise,
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
function normalizeStopConditions(cond) {
|
|
1536
|
-
return Array.isArray(cond) ? cond : [cond];
|
|
1537
|
-
}
|
|
1538
|
-
//# sourceMappingURL=agent.js.map
|