@pencil-agent/nano-pencil 2.0.0-beta.2 → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-meta.json +3 -3
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-continuations.d.ts +17 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-continuations.js +60 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-stream-events.d.ts +19 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-stream-events.js +55 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-tool-results.d.ts +10 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-tool-results.js +137 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-tool-summaries.d.ts +22 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop-tool-summaries.js +64 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop.d.ts +26 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop.js +913 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-run-result.d.ts +9 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-run-result.js +32 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent.d.ts +215 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent.js +522 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/errors.d.ts +62 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/errors.js +146 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/index.d.ts +14 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/index.js +19 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/proxy.d.ts +91 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/proxy.js +279 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-agent-loop.d.ts +15 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-agent-loop.js +625 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-streaming-tool-executor.d.ts +33 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-streaming-tool-executor.js +189 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-tool-orchestration.d.ts +35 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-tool-orchestration.js +319 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/types.d.ts +417 -0
- package/dist/node_modules/@pencil-agent/agent-core/dist/types.js +13 -0
- package/dist/node_modules/@pencil-agent/agent-core/package.json +28 -0
- package/dist/node_modules/@pencil-agent/ai/dist/api-registry.d.ts +27 -0
- package/dist/node_modules/@pencil-agent/ai/dist/api-registry.js +152 -0
- package/dist/node_modules/@pencil-agent/ai/dist/cli.d.ts +2 -0
- package/dist/node_modules/@pencil-agent/ai/dist/cli.js +121 -0
- package/dist/node_modules/@pencil-agent/ai/dist/config-path.d.ts +1 -0
- package/dist/node_modules/@pencil-agent/ai/dist/config-path.js +17 -0
- package/dist/node_modules/@pencil-agent/ai/dist/debug-logger.d.ts +94 -0
- package/dist/node_modules/@pencil-agent/ai/dist/debug-logger.js +218 -0
- package/dist/node_modules/@pencil-agent/ai/dist/env-api-keys.d.ts +8 -0
- package/dist/node_modules/@pencil-agent/ai/dist/env-api-keys.js +107 -0
- package/dist/node_modules/@pencil-agent/ai/dist/env.d.ts +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/env.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/events.d.ts +8 -0
- package/dist/node_modules/@pencil-agent/ai/dist/events.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/index.d.ts +27 -0
- package/dist/node_modules/@pencil-agent/ai/dist/index.js +20 -0
- package/dist/node_modules/@pencil-agent/ai/dist/json.d.ts +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/json.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/models.d.ts +31 -0
- package/dist/node_modules/@pencil-agent/ai/dist/models.generated.d.ts +15159 -0
- package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +14928 -0
- package/dist/node_modules/@pencil-agent/ai/dist/models.js +60 -0
- package/dist/node_modules/@pencil-agent/ai/dist/overflow.d.ts +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/overflow.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/amazon-bedrock.d.ts +20 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/amazon-bedrock.js +606 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/anthropic.d.ts +38 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/anthropic.js +737 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/azure-openai-responses.d.ts +21 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/azure-openai-responses.js +193 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/github-copilot-headers.d.ts +13 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/github-copilot-headers.js +34 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google-gemini-cli.d.ts +79 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google-gemini-cli.js +753 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google-shared.d.ts +70 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google-shared.js +311 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google-vertex.d.ts +20 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google-vertex.js +380 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google.d.ts +18 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/google.js +360 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-codex-responses.d.ts +8 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-codex-responses.js +704 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-completions.d.ts +20 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-completions.js +870 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses-shared.d.ts +22 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses-shared.js +432 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses.d.ts +19 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses.js +207 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/register-builtins.d.ts +8 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/register-builtins.js +86 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/simple-options.d.ts +13 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/simple-options.js +40 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/transform-messages.d.ts +13 -0
- package/dist/node_modules/@pencil-agent/ai/dist/providers/transform-messages.js +175 -0
- package/dist/node_modules/@pencil-agent/ai/dist/registry.d.ts +8 -0
- package/dist/node_modules/@pencil-agent/ai/dist/registry.js +8 -0
- package/dist/node_modules/@pencil-agent/ai/dist/schema.d.ts +10 -0
- package/dist/node_modules/@pencil-agent/ai/dist/schema.js +9 -0
- package/dist/node_modules/@pencil-agent/ai/dist/stream.d.ts +25 -0
- package/dist/node_modules/@pencil-agent/ai/dist/stream.js +324 -0
- package/dist/node_modules/@pencil-agent/ai/dist/types.d.ts +306 -0
- package/dist/node_modules/@pencil-agent/ai/dist/types.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/event-stream-types.d.ts +12 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/event-stream-types.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/event-stream.d.ts +31 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/event-stream.js +98 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/http-proxy.d.ts +13 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/http-proxy.js +20 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/json-parse.d.ts +14 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/json-parse.js +34 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/anthropic.d.ts +22 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/anthropic.js +109 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/decode-credential.d.ts +12 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/decode-credential.js +25 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/github-copilot.d.ts +35 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/github-copilot.js +286 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/google-antigravity.d.ts +31 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/google-antigravity.js +378 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/google-gemini-cli.d.ts +31 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/google-gemini-cli.js +483 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/index.d.ts +60 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/index.js +131 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/openai-codex.d.ts +39 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/openai-codex.js +385 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/pkce.d.ts +18 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/pkce.js +36 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/types.d.ts +52 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/oauth/types.js +7 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/overflow.d.ts +57 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/overflow.js +120 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/sanitize-unicode.d.ts +16 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/sanitize-unicode.js +20 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/typebox-helpers.d.ts +22 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/typebox-helpers.js +26 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/validation.d.ts +23 -0
- package/dist/node_modules/@pencil-agent/ai/dist/utils/validation.js +78 -0
- package/dist/node_modules/@pencil-agent/ai/package.json +94 -0
- package/dist/node_modules/@pencil-agent/tui/dist/autocomplete.d.ts +62 -0
- package/dist/node_modules/@pencil-agent/tui/dist/autocomplete.js +624 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/box.d.ts +27 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/box.js +109 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/cancellable-loader.d.ts +27 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/cancellable-loader.js +40 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/editor.d.ts +218 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/editor.js +1697 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/image.d.ts +33 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/image.js +74 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/input.d.ts +42 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/input.js +438 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/loader.d.ts +26 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/loader.js +54 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/markdown.d.ts +100 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/markdown.js +634 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/select-list.d.ts +37 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/select-list.js +157 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/settings-list.d.ts +55 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/settings-list.js +190 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/spacer.d.ts +17 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/spacer.js +28 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/text.d.ts +24 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/text.js +94 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/truncated-text.d.ts +18 -0
- package/dist/node_modules/@pencil-agent/tui/dist/components/truncated-text.js +56 -0
- package/dist/node_modules/@pencil-agent/tui/dist/editor-component.d.ts +51 -0
- package/dist/node_modules/@pencil-agent/tui/dist/editor-component.js +7 -0
- package/dist/node_modules/@pencil-agent/tui/dist/fuzzy.d.ts +32 -0
- package/dist/node_modules/@pencil-agent/tui/dist/fuzzy.js +152 -0
- package/dist/node_modules/@pencil-agent/tui/dist/index.d.ts +28 -0
- package/dist/node_modules/@pencil-agent/tui/dist/index.js +37 -0
- package/dist/node_modules/@pencil-agent/tui/dist/keybindings.d.ts +44 -0
- package/dist/node_modules/@pencil-agent/tui/dist/keybindings.js +119 -0
- package/dist/node_modules/@pencil-agent/tui/dist/keys.d.ts +149 -0
- package/dist/node_modules/@pencil-agent/tui/dist/keys.js +948 -0
- package/dist/node_modules/@pencil-agent/tui/dist/kill-ring.d.ts +33 -0
- package/dist/node_modules/@pencil-agent/tui/dist/kill-ring.js +49 -0
- package/dist/node_modules/@pencil-agent/tui/dist/stdin-buffer.d.ts +38 -0
- package/dist/node_modules/@pencil-agent/tui/dist/stdin-buffer.js +307 -0
- package/dist/node_modules/@pencil-agent/tui/dist/terminal-image.d.ts +73 -0
- package/dist/node_modules/@pencil-agent/tui/dist/terminal-image.js +287 -0
- package/dist/node_modules/@pencil-agent/tui/dist/terminal.d.ts +86 -0
- package/dist/node_modules/@pencil-agent/tui/dist/terminal.js +266 -0
- package/dist/node_modules/@pencil-agent/tui/dist/tui.d.ts +219 -0
- package/dist/node_modules/@pencil-agent/tui/dist/tui.js +1001 -0
- package/dist/node_modules/@pencil-agent/tui/dist/undo-stack.d.ts +22 -0
- package/dist/node_modules/@pencil-agent/tui/dist/undo-stack.js +30 -0
- package/dist/node_modules/@pencil-agent/tui/dist/utils.d.ts +83 -0
- package/dist/node_modules/@pencil-agent/tui/dist/utils.js +811 -0
- package/dist/node_modules/@pencil-agent/tui/package.json +37 -0
- package/package.json +3 -2
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent loop that works with AgentMessage throughout.
|
|
3
|
+
* Transforms to Message[] only at the LLM call boundary.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* [WHO]: Provides agentLoop(), agentLoopContinue(), standard loop event emission, continuation recovery, recovered-error tombstoning, and serial tool execution.
|
|
7
|
+
* [FROM]: Depends on @pencil-agent/ai streams/messages, ./types contracts, ./errors, and shared loop helpers.
|
|
8
|
+
* [TO]: Consumed by agent.ts and package exports as the default agent execution loop.
|
|
9
|
+
* [HERE]: core/lib/agent-core/src/agent-loop.ts within agent-core; standard counterpart to structured-adaptive-agent-loop.ts.
|
|
10
|
+
*/
|
|
11
|
+
import { EventStream } from "@pencil-agent/ai/events";
|
|
12
|
+
import { isContextOverflow } from "@pencil-agent/ai/overflow";
|
|
13
|
+
import { validateToolArguments } from "@pencil-agent/ai/schema";
|
|
14
|
+
import { streamSimple } from "@pencil-agent/ai/stream";
|
|
15
|
+
import { ToolNotFoundError, ValidationError } from "./errors.js";
|
|
16
|
+
import { computeRecoveryMaxTokens, createOutputTokenRecoveryMessage, createTokenBudgetContinuation, } from "./agent-loop-continuations.js";
|
|
17
|
+
import { createInterruptedToolResults, createSkippedToolCallLimitResults, enforceToolResultBatchSize, } from "./agent-loop-tool-results.js";
|
|
18
|
+
import { flushReadyToolUseSummaries, startToolUseSummary, } from "./agent-loop-tool-summaries.js";
|
|
19
|
+
import { buildAgentRunPolicy, resolveAgentRunLoopFramework } from "./agent-run-result.js";
|
|
20
|
+
import { waitForAbortableOperation, waitForAssistantStream, waitForAssistantStreamEvent, } from "./agent-loop-stream-events.js";
|
|
21
|
+
const DEFAULT_MAX_TURNS_PER_PROMPT = 64;
|
|
22
|
+
const DEFAULT_MAX_TOOL_CALLS_PER_PROMPT = 128;
|
|
23
|
+
const DEFAULT_MAX_STOP_HOOK_CONTINUATIONS = 3;
|
|
24
|
+
const DEFAULT_MAX_MODEL_ERROR_RECOVERY_ATTEMPTS = 1;
|
|
25
|
+
const DEFAULT_MAX_OUTPUT_TOKEN_RECOVERY_ATTEMPTS = 1;
|
|
26
|
+
/**
|
|
27
|
+
* Start an agent loop with a new prompt message.
|
|
28
|
+
* The prompt is added to the context and events are emitted for it.
|
|
29
|
+
*/
|
|
30
|
+
export function agentLoop(prompts, context, config, signal, streamFn) {
|
|
31
|
+
const stream = createAgentStream();
|
|
32
|
+
(async () => {
|
|
33
|
+
const newMessages = [...prompts];
|
|
34
|
+
const currentContext = {
|
|
35
|
+
...context,
|
|
36
|
+
messages: [...context.messages, ...prompts],
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
stream.push({ type: "agent_start" });
|
|
40
|
+
stream.push({ type: "turn_start" });
|
|
41
|
+
for (const prompt of prompts) {
|
|
42
|
+
stream.push({ type: "message_start", message: prompt });
|
|
43
|
+
stream.push({ type: "message_end", message: prompt });
|
|
44
|
+
}
|
|
45
|
+
await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
endWithLoopError(stream, newMessages, config, error, signal);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
return stream;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Continue an agent loop from the current context without adding a new message.
|
|
55
|
+
* Used for retries - context already has user message or tool results.
|
|
56
|
+
*
|
|
57
|
+
* **Important:** The last message in context must convert to a `user` or `toolResult` message
|
|
58
|
+
* via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
|
|
59
|
+
* This cannot be validated here since `convertToLlm` is only called once per turn.
|
|
60
|
+
*/
|
|
61
|
+
export function agentLoopContinue(context, config, signal, streamFn) {
|
|
62
|
+
if (context.messages.length === 0) {
|
|
63
|
+
throw new ValidationError("Cannot continue: no messages in context");
|
|
64
|
+
}
|
|
65
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
66
|
+
throw new ValidationError("Cannot continue from message role: assistant");
|
|
67
|
+
}
|
|
68
|
+
const stream = createAgentStream();
|
|
69
|
+
(async () => {
|
|
70
|
+
const newMessages = [];
|
|
71
|
+
const currentContext = { ...context };
|
|
72
|
+
try {
|
|
73
|
+
stream.push({ type: "agent_start" });
|
|
74
|
+
stream.push({ type: "turn_start" });
|
|
75
|
+
await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
endWithLoopError(stream, newMessages, config, error, signal);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
return stream;
|
|
82
|
+
}
|
|
83
|
+
function createAgentStream() {
|
|
84
|
+
return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
|
|
85
|
+
}
|
|
86
|
+
function createLoopLimitMessage(config, errorMessage) {
|
|
87
|
+
return {
|
|
88
|
+
role: "assistant",
|
|
89
|
+
content: [{ type: "text", text: "" }],
|
|
90
|
+
api: config.model.api,
|
|
91
|
+
provider: config.model.provider,
|
|
92
|
+
model: config.model.id,
|
|
93
|
+
usage: {
|
|
94
|
+
input: 0,
|
|
95
|
+
output: 0,
|
|
96
|
+
cacheRead: 0,
|
|
97
|
+
cacheWrite: 0,
|
|
98
|
+
totalTokens: 0,
|
|
99
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
100
|
+
},
|
|
101
|
+
stopReason: "error",
|
|
102
|
+
errorMessage,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function endWithLoopError(stream, newMessages, config, error, signal) {
|
|
107
|
+
const errorMessage = createLoopLimitMessage(config, error instanceof Error ? error.message : String(error));
|
|
108
|
+
if (signal?.aborted) {
|
|
109
|
+
errorMessage.stopReason = "aborted";
|
|
110
|
+
}
|
|
111
|
+
newMessages.push(errorMessage);
|
|
112
|
+
stream.push({ type: "message_start", message: { ...errorMessage } });
|
|
113
|
+
stream.push({ type: "message_end", message: errorMessage });
|
|
114
|
+
stream.push({ type: "turn_end", message: errorMessage, toolResults: [] });
|
|
115
|
+
finishStandardLoop(stream, newMessages, {
|
|
116
|
+
config,
|
|
117
|
+
turnCount: 0,
|
|
118
|
+
toolCallCount: 0,
|
|
119
|
+
startedAt: Date.now(),
|
|
120
|
+
usage: emptyUsage(),
|
|
121
|
+
permissionDenials: [],
|
|
122
|
+
stopReason: errorMessage.stopReason,
|
|
123
|
+
errorMessage: errorMessage.errorMessage,
|
|
124
|
+
errorSubtype: signal?.aborted ? "aborted" : "loop_error",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Main loop logic shared by agentLoop and agentLoopContinue.
|
|
129
|
+
*/
|
|
130
|
+
async function runLoop(currentContext, newMessages, config, signal, stream, streamFn) {
|
|
131
|
+
let firstTurn = true;
|
|
132
|
+
let turnCount = 0;
|
|
133
|
+
let toolCallCount = 0;
|
|
134
|
+
const startedAt = Date.now();
|
|
135
|
+
const usage = emptyUsage();
|
|
136
|
+
const permissionDenials = [];
|
|
137
|
+
const maxTurns = config.maxTurnsPerPrompt ?? DEFAULT_MAX_TURNS_PER_PROMPT;
|
|
138
|
+
const maxToolCalls = config.maxToolCallsPerPrompt ?? DEFAULT_MAX_TOOL_CALLS_PER_PROMPT;
|
|
139
|
+
const maxStopHookContinuations = config.maxStopHookContinuations ?? DEFAULT_MAX_STOP_HOOK_CONTINUATIONS;
|
|
140
|
+
const maxModelErrorRecoveryAttempts = config.maxModelErrorRecoveryAttempts ?? DEFAULT_MAX_MODEL_ERROR_RECOVERY_ATTEMPTS;
|
|
141
|
+
const maxOutputTokenRecoveryAttempts = config.maxOutputTokenRecoveryAttempts ?? DEFAULT_MAX_OUTPUT_TOKEN_RECOVERY_ATTEMPTS;
|
|
142
|
+
let modelErrorRecoveryCount = 0;
|
|
143
|
+
let maxOutputTokensRecoveryCount = 0;
|
|
144
|
+
let tokenBudgetContinuationCount = 0;
|
|
145
|
+
let maxOutputTokensOverride;
|
|
146
|
+
let lastTransition = { reason: "start" };
|
|
147
|
+
const transitions = [];
|
|
148
|
+
const recordTransition = (transition) => {
|
|
149
|
+
lastTransition = transition;
|
|
150
|
+
transitions.push(transition);
|
|
151
|
+
return transition;
|
|
152
|
+
};
|
|
153
|
+
let pendingToolUseSummaries = [];
|
|
154
|
+
let stopHookActive = false;
|
|
155
|
+
let stopHookContinuationCount = 0;
|
|
156
|
+
// Check for steering messages at start (user may have typed while waiting)
|
|
157
|
+
const initialSteeringMessages = await waitForAbortableOperation(config.getSteeringMessages ? config.getSteeringMessages() : [], signal);
|
|
158
|
+
if (initialSteeringMessages.type === "aborted") {
|
|
159
|
+
finishStandardLoopWithAbortedTurn(stream, currentContext, newMessages, {
|
|
160
|
+
config,
|
|
161
|
+
turnCount,
|
|
162
|
+
toolCallCount,
|
|
163
|
+
startedAt,
|
|
164
|
+
usage,
|
|
165
|
+
permissionDenials,
|
|
166
|
+
transitions,
|
|
167
|
+
lastTransition,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let pendingMessages = initialSteeringMessages.value || [];
|
|
172
|
+
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
173
|
+
while (true) {
|
|
174
|
+
let hasMoreToolCalls = true;
|
|
175
|
+
let steeringAfterTools = null;
|
|
176
|
+
// Inner loop: process tool calls and steering messages
|
|
177
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
178
|
+
if (!firstTurn) {
|
|
179
|
+
stream.push({ type: "turn_start" });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
firstTurn = false;
|
|
183
|
+
}
|
|
184
|
+
pendingToolUseSummaries = flushReadyToolUseSummaries(pendingToolUseSummaries, currentContext.messages, newMessages, stream);
|
|
185
|
+
// Process pending messages (inject before next assistant response)
|
|
186
|
+
if (pendingMessages.length > 0) {
|
|
187
|
+
for (const message of pendingMessages) {
|
|
188
|
+
stream.push({ type: "message_start", message });
|
|
189
|
+
stream.push({ type: "message_end", message });
|
|
190
|
+
currentContext.messages.push(message);
|
|
191
|
+
newMessages.push(message);
|
|
192
|
+
}
|
|
193
|
+
pendingMessages = [];
|
|
194
|
+
}
|
|
195
|
+
turnCount++;
|
|
196
|
+
if (turnCount > maxTurns) {
|
|
197
|
+
const limitMessage = createLoopLimitMessage(config, `Stopped after ${maxTurns} assistant turns to prevent a runaway agent loop.`);
|
|
198
|
+
currentContext.messages.push(limitMessage);
|
|
199
|
+
newMessages.push(limitMessage);
|
|
200
|
+
stream.push({ type: "message_start", message: { ...limitMessage } });
|
|
201
|
+
stream.push({ type: "message_end", message: limitMessage });
|
|
202
|
+
stream.push({ type: "turn_end", message: limitMessage, toolResults: [] });
|
|
203
|
+
finishStandardLoop(stream, newMessages, {
|
|
204
|
+
config,
|
|
205
|
+
turnCount,
|
|
206
|
+
toolCallCount,
|
|
207
|
+
startedAt,
|
|
208
|
+
usage,
|
|
209
|
+
permissionDenials,
|
|
210
|
+
stopReason: limitMessage.stopReason,
|
|
211
|
+
transitions,
|
|
212
|
+
lastTransition: recordTransition({
|
|
213
|
+
reason: "max_turns_reached",
|
|
214
|
+
maxTurns,
|
|
215
|
+
turnCount,
|
|
216
|
+
}),
|
|
217
|
+
errorMessage: limitMessage.errorMessage,
|
|
218
|
+
errorSubtype: "max_turns_reached",
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Stream assistant response
|
|
223
|
+
const responseStartMessageCount = newMessages.length;
|
|
224
|
+
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn, maxOutputTokensOverride);
|
|
225
|
+
addUsage(usage, message.usage);
|
|
226
|
+
maxOutputTokensOverride = undefined;
|
|
227
|
+
newMessages.push(message);
|
|
228
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
229
|
+
const errorSubtype = message.stopReason === "aborted"
|
|
230
|
+
? "aborted"
|
|
231
|
+
: isContextOverflow(message, config.model.contextWindow)
|
|
232
|
+
? "context_overflow"
|
|
233
|
+
: "model_error";
|
|
234
|
+
const interruptedToolResults = createInterruptedToolResults(message);
|
|
235
|
+
for (const result of interruptedToolResults) {
|
|
236
|
+
currentContext.messages.push(result);
|
|
237
|
+
newMessages.push(result);
|
|
238
|
+
stream.push({ type: "message_start", message: result });
|
|
239
|
+
stream.push({ type: "message_end", message: result });
|
|
240
|
+
}
|
|
241
|
+
if (message.stopReason === "error" &&
|
|
242
|
+
config.recoverModelError &&
|
|
243
|
+
modelErrorRecoveryCount < maxModelErrorRecoveryAttempts) {
|
|
244
|
+
const attempt = modelErrorRecoveryCount + 1;
|
|
245
|
+
const recovery = await config.recoverModelError({
|
|
246
|
+
message,
|
|
247
|
+
messages: currentContext.messages,
|
|
248
|
+
errorSubtype,
|
|
249
|
+
attempt,
|
|
250
|
+
});
|
|
251
|
+
if (recovery.action === "retry") {
|
|
252
|
+
stream.push({ type: "turn_end", message, toolResults: interruptedToolResults });
|
|
253
|
+
modelErrorRecoveryCount = attempt;
|
|
254
|
+
newMessages.splice(responseStartMessageCount);
|
|
255
|
+
currentContext.messages = recovery.messages;
|
|
256
|
+
recordTransition(recovery.transition ?? {
|
|
257
|
+
reason: "model_error_recovery",
|
|
258
|
+
subtype: errorSubtype,
|
|
259
|
+
attempt,
|
|
260
|
+
});
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
stream.push({ type: "turn_end", message, toolResults: interruptedToolResults });
|
|
265
|
+
finishStandardLoop(stream, newMessages, {
|
|
266
|
+
config,
|
|
267
|
+
turnCount,
|
|
268
|
+
toolCallCount,
|
|
269
|
+
startedAt,
|
|
270
|
+
usage,
|
|
271
|
+
permissionDenials,
|
|
272
|
+
stopReason: message.stopReason,
|
|
273
|
+
transitions,
|
|
274
|
+
lastTransition,
|
|
275
|
+
errorMessage: message.errorMessage,
|
|
276
|
+
errorSubtype,
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Check for tool calls
|
|
281
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
282
|
+
hasMoreToolCalls = toolCalls.length > 0;
|
|
283
|
+
const toolResults = [];
|
|
284
|
+
if (hasMoreToolCalls) {
|
|
285
|
+
if (toolCallCount + toolCalls.length > maxToolCalls) {
|
|
286
|
+
const skippedToolResults = createSkippedToolCallLimitResults(message, `Tool call skipped because this prompt reached the ${maxToolCalls} tool-call limit.`);
|
|
287
|
+
for (const result of skippedToolResults) {
|
|
288
|
+
currentContext.messages.push(result);
|
|
289
|
+
newMessages.push(result);
|
|
290
|
+
stream.push({ type: "message_start", message: result });
|
|
291
|
+
stream.push({ type: "message_end", message: result });
|
|
292
|
+
}
|
|
293
|
+
const limitMessage = createLoopLimitMessage(config, `Stopped before executing ${toolCalls.length} tool call${toolCalls.length === 1 ? "" : "s"} because this prompt reached the ${maxToolCalls} tool-call limit.`);
|
|
294
|
+
currentContext.messages.push(limitMessage);
|
|
295
|
+
newMessages.push(limitMessage);
|
|
296
|
+
stream.push({ type: "message_start", message: { ...limitMessage } });
|
|
297
|
+
stream.push({ type: "message_end", message: limitMessage });
|
|
298
|
+
stream.push({ type: "turn_end", message: limitMessage, toolResults: skippedToolResults });
|
|
299
|
+
finishStandardLoop(stream, newMessages, {
|
|
300
|
+
config,
|
|
301
|
+
turnCount,
|
|
302
|
+
toolCallCount,
|
|
303
|
+
startedAt,
|
|
304
|
+
usage,
|
|
305
|
+
permissionDenials,
|
|
306
|
+
stopReason: limitMessage.stopReason,
|
|
307
|
+
transitions,
|
|
308
|
+
lastTransition: recordTransition({
|
|
309
|
+
reason: "tool_call_limit_reached",
|
|
310
|
+
maxToolCalls,
|
|
311
|
+
requestedToolCalls: toolCalls.length,
|
|
312
|
+
toolCallCount,
|
|
313
|
+
}),
|
|
314
|
+
errorMessage: limitMessage.errorMessage,
|
|
315
|
+
errorSubtype: "tool_call_limit_reached",
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
toolCallCount += toolCalls.length;
|
|
320
|
+
const toolExecution = await executeToolCalls(currentContext.tools, message, signal, stream, config.getSteeringMessages, config.canUseTool);
|
|
321
|
+
toolResults.push(...enforceToolResultBatchSize(toolExecution.toolResults, config.maxToolResultBatchSizeChars));
|
|
322
|
+
permissionDenials.push(...collectPermissionDenials(toolExecution.toolResults));
|
|
323
|
+
steeringAfterTools = toolExecution.steeringMessages ?? null;
|
|
324
|
+
for (const result of toolResults) {
|
|
325
|
+
currentContext.messages.push(result);
|
|
326
|
+
newMessages.push(result);
|
|
327
|
+
stream.push({ type: "message_start", message: result });
|
|
328
|
+
stream.push({ type: "message_end", message: result });
|
|
329
|
+
}
|
|
330
|
+
for (const contextMessage of toolExecution.contextMessages) {
|
|
331
|
+
currentContext.messages.push(contextMessage);
|
|
332
|
+
newMessages.push(contextMessage);
|
|
333
|
+
stream.push({ type: "message_start", message: contextMessage });
|
|
334
|
+
stream.push({ type: "message_end", message: contextMessage });
|
|
335
|
+
}
|
|
336
|
+
const pendingSummary = startToolUseSummary(config, {
|
|
337
|
+
assistantMessage: message,
|
|
338
|
+
toolResults,
|
|
339
|
+
contextMessages: toolExecution.contextMessages,
|
|
340
|
+
messages: currentContext.messages,
|
|
341
|
+
});
|
|
342
|
+
if (pendingSummary) {
|
|
343
|
+
pendingToolUseSummaries.push(pendingSummary);
|
|
344
|
+
}
|
|
345
|
+
recordTransition({ reason: "tool_result", toolCallCount: toolCalls.length });
|
|
346
|
+
}
|
|
347
|
+
stream.push({ type: "turn_end", message, toolResults });
|
|
348
|
+
if (!hasMoreToolCalls &&
|
|
349
|
+
message.stopReason === "length" &&
|
|
350
|
+
maxOutputTokensRecoveryCount < maxOutputTokenRecoveryAttempts) {
|
|
351
|
+
maxOutputTokensRecoveryCount += 1;
|
|
352
|
+
maxOutputTokensOverride = computeRecoveryMaxTokens(config, message);
|
|
353
|
+
pendingMessages = [createOutputTokenRecoveryMessage(maxOutputTokensRecoveryCount)];
|
|
354
|
+
recordTransition({
|
|
355
|
+
reason: "max_output_tokens_recovery",
|
|
356
|
+
attempt: maxOutputTokensRecoveryCount,
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!hasMoreToolCalls && config.runStopHooks && !stopHookActive) {
|
|
361
|
+
stopHookActive = true;
|
|
362
|
+
const stopHookResult = await waitForAbortableOperation(config.runStopHooks({
|
|
363
|
+
message,
|
|
364
|
+
messages: currentContext.messages,
|
|
365
|
+
}), signal);
|
|
366
|
+
stopHookActive = false;
|
|
367
|
+
if (stopHookResult.type === "aborted") {
|
|
368
|
+
finishStandardLoopWithAbortedTurn(stream, currentContext, newMessages, {
|
|
369
|
+
config,
|
|
370
|
+
turnCount,
|
|
371
|
+
toolCallCount,
|
|
372
|
+
startedAt,
|
|
373
|
+
usage,
|
|
374
|
+
permissionDenials,
|
|
375
|
+
transitions,
|
|
376
|
+
lastTransition,
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const resolvedStopHookResult = stopHookResult.value;
|
|
381
|
+
if (resolvedStopHookResult.action === "continue" && resolvedStopHookResult.messages.length > 0) {
|
|
382
|
+
if (stopHookContinuationCount >= maxStopHookContinuations) {
|
|
383
|
+
const limitMessage = createLoopLimitMessage(config, `stop_hook_limit_reached: stopped after ${maxStopHookContinuations} stop-hook continuation turns.`);
|
|
384
|
+
currentContext.messages.push(limitMessage);
|
|
385
|
+
newMessages.push(limitMessage);
|
|
386
|
+
stream.push({ type: "message_start", message: { ...limitMessage } });
|
|
387
|
+
stream.push({ type: "message_end", message: limitMessage });
|
|
388
|
+
stream.push({ type: "turn_end", message: limitMessage, toolResults: [] });
|
|
389
|
+
finishStandardLoop(stream, newMessages, {
|
|
390
|
+
config,
|
|
391
|
+
turnCount,
|
|
392
|
+
toolCallCount,
|
|
393
|
+
startedAt,
|
|
394
|
+
usage,
|
|
395
|
+
permissionDenials,
|
|
396
|
+
stopReason: limitMessage.stopReason,
|
|
397
|
+
transitions,
|
|
398
|
+
lastTransition: recordTransition({
|
|
399
|
+
reason: "stop_hook_limit_reached",
|
|
400
|
+
maxContinuations: maxStopHookContinuations,
|
|
401
|
+
continuationCount: stopHookContinuationCount,
|
|
402
|
+
}),
|
|
403
|
+
errorMessage: limitMessage.errorMessage,
|
|
404
|
+
errorSubtype: "stop_hook_limit_reached",
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
stopHookContinuationCount += 1;
|
|
409
|
+
pendingMessages = resolvedStopHookResult.messages;
|
|
410
|
+
recordTransition({
|
|
411
|
+
reason: "stop_hook_blocking",
|
|
412
|
+
continuationCount: stopHookContinuationCount,
|
|
413
|
+
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!hasMoreToolCalls) {
|
|
418
|
+
const tokenBudgetContinuation = createTokenBudgetContinuation(config, usage.output, tokenBudgetContinuationCount);
|
|
419
|
+
if (tokenBudgetContinuation) {
|
|
420
|
+
tokenBudgetContinuationCount += 1;
|
|
421
|
+
pendingMessages = [tokenBudgetContinuation.message];
|
|
422
|
+
recordTransition({
|
|
423
|
+
reason: "token_budget_continuation",
|
|
424
|
+
continuationCount: tokenBudgetContinuationCount,
|
|
425
|
+
outputTokens: tokenBudgetContinuation.outputTokens,
|
|
426
|
+
targetTokens: tokenBudgetContinuation.targetTokens,
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Get steering messages after turn completes
|
|
432
|
+
if (steeringAfterTools && steeringAfterTools.length > 0) {
|
|
433
|
+
pendingMessages = steeringAfterTools;
|
|
434
|
+
steeringAfterTools = null;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Agent would stop here. Check for follow-up messages.
|
|
441
|
+
const followUpMessagesResult = await waitForAbortableOperation(config.getFollowUpMessages ? config.getFollowUpMessages() : [], signal);
|
|
442
|
+
if (followUpMessagesResult.type === "aborted") {
|
|
443
|
+
finishStandardLoopWithAbortedTurn(stream, currentContext, newMessages, {
|
|
444
|
+
config,
|
|
445
|
+
turnCount,
|
|
446
|
+
toolCallCount,
|
|
447
|
+
startedAt,
|
|
448
|
+
usage,
|
|
449
|
+
permissionDenials,
|
|
450
|
+
transitions,
|
|
451
|
+
lastTransition,
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const followUpMessages = followUpMessagesResult.value || [];
|
|
456
|
+
if (followUpMessages.length > 0) {
|
|
457
|
+
// Set as pending so inner loop processes them
|
|
458
|
+
pendingMessages = followUpMessages;
|
|
459
|
+
recordTransition({ reason: "follow_up" });
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
// No more messages, exit
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
finishStandardLoop(stream, newMessages, {
|
|
466
|
+
config,
|
|
467
|
+
turnCount,
|
|
468
|
+
toolCallCount,
|
|
469
|
+
startedAt,
|
|
470
|
+
usage,
|
|
471
|
+
permissionDenials,
|
|
472
|
+
stopReason: inferStopReason(newMessages),
|
|
473
|
+
transitions,
|
|
474
|
+
lastTransition,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Stream an assistant response from the LLM.
|
|
479
|
+
* This is where AgentMessage[] gets transformed to Message[] for the LLM.
|
|
480
|
+
*/
|
|
481
|
+
async function streamAssistantResponse(context, config, signal, stream, streamFn, maxTokensOverride) {
|
|
482
|
+
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
483
|
+
let messages = context.messages;
|
|
484
|
+
if (config.transformContext) {
|
|
485
|
+
const transformedMessages = await waitForAbortableOperation(config.transformContext(messages, signal), signal);
|
|
486
|
+
if (transformedMessages.type === "aborted") {
|
|
487
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
488
|
+
}
|
|
489
|
+
messages = transformedMessages.value;
|
|
490
|
+
}
|
|
491
|
+
// Convert to LLM-compatible messages (AgentMessage[] → Message[])
|
|
492
|
+
const convertedMessages = await waitForAbortableOperation(config.convertToLlm(messages), signal);
|
|
493
|
+
if (convertedMessages.type === "aborted") {
|
|
494
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
495
|
+
}
|
|
496
|
+
const llmMessages = convertedMessages.value;
|
|
497
|
+
// Build LLM context
|
|
498
|
+
const llmContext = {
|
|
499
|
+
systemPrompt: context.systemPrompt,
|
|
500
|
+
messages: llmMessages,
|
|
501
|
+
tools: context.tools,
|
|
502
|
+
};
|
|
503
|
+
const streamFunction = streamFn || streamSimple;
|
|
504
|
+
// Resolve API key (important for expiring tokens)
|
|
505
|
+
const apiKeyResult = await waitForAbortableOperation(config.getApiKey ? config.getApiKey(config.model.provider) : undefined, signal);
|
|
506
|
+
if (apiKeyResult.type === "aborted") {
|
|
507
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
508
|
+
}
|
|
509
|
+
const resolvedApiKey = apiKeyResult.value || config.apiKey;
|
|
510
|
+
stream.push({
|
|
511
|
+
type: "stream_request_start",
|
|
512
|
+
model: config.model.id,
|
|
513
|
+
provider: config.model.provider,
|
|
514
|
+
api: config.model.api,
|
|
515
|
+
messageCount: llmMessages.length,
|
|
516
|
+
maxTokens: maxTokensOverride ?? config.maxTokens,
|
|
517
|
+
});
|
|
518
|
+
const response = await waitForAssistantStream(streamFunction(config.model, llmContext, {
|
|
519
|
+
...config,
|
|
520
|
+
maxTokens: maxTokensOverride ?? config.maxTokens,
|
|
521
|
+
apiKey: resolvedApiKey,
|
|
522
|
+
signal,
|
|
523
|
+
}), signal);
|
|
524
|
+
if (response === "aborted") {
|
|
525
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
526
|
+
finalMessage.stopReason = "aborted";
|
|
527
|
+
context.messages.push(finalMessage);
|
|
528
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
529
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
530
|
+
return finalMessage;
|
|
531
|
+
}
|
|
532
|
+
let partialMessage = null;
|
|
533
|
+
let addedPartial = false;
|
|
534
|
+
const responseIterator = response[Symbol.asyncIterator]();
|
|
535
|
+
while (true) {
|
|
536
|
+
let nextEvent;
|
|
537
|
+
try {
|
|
538
|
+
nextEvent = await waitForAssistantStreamEvent(responseIterator, signal);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const finalMessage = createLoopLimitMessage(config, error instanceof Error ? error.message : String(error));
|
|
542
|
+
if (addedPartial) {
|
|
543
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
context.messages.push(finalMessage);
|
|
547
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
548
|
+
}
|
|
549
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
550
|
+
return finalMessage;
|
|
551
|
+
}
|
|
552
|
+
if (nextEvent === "aborted") {
|
|
553
|
+
void responseIterator.return?.();
|
|
554
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
555
|
+
finalMessage.stopReason = "aborted";
|
|
556
|
+
if (addedPartial) {
|
|
557
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
context.messages.push(finalMessage);
|
|
561
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
562
|
+
}
|
|
563
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
564
|
+
return finalMessage;
|
|
565
|
+
}
|
|
566
|
+
if (nextEvent.done) {
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
const event = nextEvent.value;
|
|
570
|
+
switch (event.type) {
|
|
571
|
+
case "start":
|
|
572
|
+
partialMessage = event.partial;
|
|
573
|
+
context.messages.push(partialMessage);
|
|
574
|
+
addedPartial = true;
|
|
575
|
+
stream.push({ type: "message_start", message: { ...partialMessage } });
|
|
576
|
+
break;
|
|
577
|
+
case "text_start":
|
|
578
|
+
case "text_delta":
|
|
579
|
+
case "text_end":
|
|
580
|
+
case "thinking_start":
|
|
581
|
+
case "thinking_delta":
|
|
582
|
+
case "thinking_end":
|
|
583
|
+
case "toolcall_start":
|
|
584
|
+
case "toolcall_delta":
|
|
585
|
+
case "toolcall_end":
|
|
586
|
+
if (partialMessage) {
|
|
587
|
+
partialMessage = event.partial;
|
|
588
|
+
context.messages[context.messages.length - 1] = partialMessage;
|
|
589
|
+
stream.push({
|
|
590
|
+
type: "message_update",
|
|
591
|
+
assistantMessageEvent: event,
|
|
592
|
+
message: { ...partialMessage },
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
case "done":
|
|
597
|
+
case "error": {
|
|
598
|
+
const finalMessage = event.type === "done" ? event.message : event.error;
|
|
599
|
+
if (addedPartial) {
|
|
600
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
context.messages.push(finalMessage);
|
|
604
|
+
}
|
|
605
|
+
if (!addedPartial) {
|
|
606
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
607
|
+
}
|
|
608
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
609
|
+
return finalMessage;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const finalMessage = response.resultIfResolved() ??
|
|
614
|
+
createLoopLimitMessage(config, "Provider stream ended without a final assistant message");
|
|
615
|
+
if (addedPartial) {
|
|
616
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
context.messages.push(finalMessage);
|
|
620
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
621
|
+
}
|
|
622
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
623
|
+
return finalMessage;
|
|
624
|
+
}
|
|
625
|
+
function pushAbortedAssistantMessage(context, stream, config) {
|
|
626
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
627
|
+
finalMessage.stopReason = "aborted";
|
|
628
|
+
context.messages.push(finalMessage);
|
|
629
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
630
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
631
|
+
return finalMessage;
|
|
632
|
+
}
|
|
633
|
+
function finishStandardLoopWithAbortedTurn(stream, context, newMessages, options) {
|
|
634
|
+
const finalMessage = createLoopLimitMessage(options.config, "Request was aborted");
|
|
635
|
+
finalMessage.stopReason = "aborted";
|
|
636
|
+
context.messages.push(finalMessage);
|
|
637
|
+
newMessages.push(finalMessage);
|
|
638
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
639
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
640
|
+
stream.push({ type: "turn_end", message: finalMessage, toolResults: [] });
|
|
641
|
+
finishStandardLoop(stream, newMessages, {
|
|
642
|
+
...options,
|
|
643
|
+
stopReason: "aborted",
|
|
644
|
+
errorMessage: finalMessage.errorMessage,
|
|
645
|
+
errorSubtype: "aborted",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function finishStandardLoop(stream, newMessages, options) {
|
|
649
|
+
stream.push({
|
|
650
|
+
type: "agent_result",
|
|
651
|
+
stopReason: options.stopReason ?? inferStopReason(newMessages),
|
|
652
|
+
loopFramework: resolveAgentRunLoopFramework(options.config),
|
|
653
|
+
loopPolicy: buildAgentRunPolicy(options.config),
|
|
654
|
+
turnCount: options.turnCount,
|
|
655
|
+
toolCallCount: options.toolCallCount,
|
|
656
|
+
durationMs: Date.now() - options.startedAt,
|
|
657
|
+
usage: options.usage,
|
|
658
|
+
permissionDenialCount: options.permissionDenials.length,
|
|
659
|
+
permissionDenials: options.permissionDenials,
|
|
660
|
+
transitions: options.transitions && options.transitions.length > 0 ? options.transitions : undefined,
|
|
661
|
+
lastTransition: options.lastTransition,
|
|
662
|
+
errorMessage: options.errorMessage,
|
|
663
|
+
errorSubtype: options.errorSubtype,
|
|
664
|
+
});
|
|
665
|
+
stream.push({ type: "agent_end", messages: newMessages });
|
|
666
|
+
stream.end(newMessages);
|
|
667
|
+
}
|
|
668
|
+
function emptyUsage() {
|
|
669
|
+
return {
|
|
670
|
+
input: 0,
|
|
671
|
+
output: 0,
|
|
672
|
+
cacheRead: 0,
|
|
673
|
+
cacheWrite: 0,
|
|
674
|
+
totalTokens: 0,
|
|
675
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function addUsage(target, usage) {
|
|
679
|
+
target.input += usage.input;
|
|
680
|
+
target.output += usage.output;
|
|
681
|
+
target.cacheRead += usage.cacheRead;
|
|
682
|
+
target.cacheWrite += usage.cacheWrite;
|
|
683
|
+
target.totalTokens += usage.totalTokens;
|
|
684
|
+
target.cost.input += usage.cost.input;
|
|
685
|
+
target.cost.output += usage.cost.output;
|
|
686
|
+
target.cost.cacheRead += usage.cost.cacheRead;
|
|
687
|
+
target.cost.cacheWrite += usage.cost.cacheWrite;
|
|
688
|
+
target.cost.total += usage.cost.total;
|
|
689
|
+
}
|
|
690
|
+
function inferStopReason(messages) {
|
|
691
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
692
|
+
const message = messages[i];
|
|
693
|
+
if (message?.role === "assistant") {
|
|
694
|
+
return message.stopReason ?? "stop";
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return "stop";
|
|
698
|
+
}
|
|
699
|
+
function collectPermissionDenials(toolResults) {
|
|
700
|
+
const denials = [];
|
|
701
|
+
for (const result of toolResults) {
|
|
702
|
+
const details = result.details;
|
|
703
|
+
if (!details || typeof details !== "object")
|
|
704
|
+
continue;
|
|
705
|
+
if (details.errorType !== "permission_denied")
|
|
706
|
+
continue;
|
|
707
|
+
const reason = details.reason;
|
|
708
|
+
denials.push({
|
|
709
|
+
toolCallId: result.toolCallId,
|
|
710
|
+
toolName: result.toolName,
|
|
711
|
+
reason: typeof reason === "string" && reason.length > 0 ? reason : undefined,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return denials;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Execute tool calls from an assistant message.
|
|
718
|
+
*/
|
|
719
|
+
async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages, canUseTool) {
|
|
720
|
+
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
721
|
+
const toolByName = buildToolMap(tools);
|
|
722
|
+
const results = [];
|
|
723
|
+
const contextMessages = [];
|
|
724
|
+
let steeringMessages;
|
|
725
|
+
for (let index = 0; index < toolCalls.length; index++) {
|
|
726
|
+
const toolCall = toolCalls[index];
|
|
727
|
+
const tool = toolByName.get(toolCall.name);
|
|
728
|
+
const startedAt = Date.now();
|
|
729
|
+
stream.push({
|
|
730
|
+
type: "tool_execution_start",
|
|
731
|
+
toolCallId: toolCall.id,
|
|
732
|
+
toolName: toolCall.name,
|
|
733
|
+
args: toolCall.arguments,
|
|
734
|
+
});
|
|
735
|
+
let result;
|
|
736
|
+
let isError = false;
|
|
737
|
+
try {
|
|
738
|
+
if (!tool)
|
|
739
|
+
throw new ToolNotFoundError(toolCall.name);
|
|
740
|
+
const validatedArgs = validateToolArguments(tool, toolCall);
|
|
741
|
+
const validationMessage = await tool.validateInput?.(validatedArgs);
|
|
742
|
+
if (typeof validationMessage === "string" && validationMessage.trim()) {
|
|
743
|
+
throw new Error(validationMessage);
|
|
744
|
+
}
|
|
745
|
+
const permission = await canUseTool?.({
|
|
746
|
+
toolCallId: toolCall.id,
|
|
747
|
+
toolName: tool.name,
|
|
748
|
+
requestedToolName: toolCall.name,
|
|
749
|
+
input: validatedArgs,
|
|
750
|
+
rawInput: toolCall.arguments,
|
|
751
|
+
tool,
|
|
752
|
+
});
|
|
753
|
+
if (permission?.decision === "deny") {
|
|
754
|
+
const reason = permission.reason?.trim();
|
|
755
|
+
result = {
|
|
756
|
+
content: [
|
|
757
|
+
{ type: "text", text: reason ? `Permission denied: ${reason}` : `Permission denied for ${tool.name}` },
|
|
758
|
+
],
|
|
759
|
+
details: {
|
|
760
|
+
errorType: "permission_denied",
|
|
761
|
+
reason,
|
|
762
|
+
toolName: tool.name,
|
|
763
|
+
toolCallId: toolCall.id,
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
isError = true;
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
result = await tool.execute(toolCall.id, validatedArgs, signal, (partialResult) => {
|
|
770
|
+
stream.push({
|
|
771
|
+
type: "tool_execution_update",
|
|
772
|
+
toolCallId: toolCall.id,
|
|
773
|
+
toolName: toolCall.name,
|
|
774
|
+
args: toolCall.arguments,
|
|
775
|
+
partialResult,
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
result = enforceMaxResultSize(result, tool.maxResultSizeChars);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch (e) {
|
|
782
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
783
|
+
result = {
|
|
784
|
+
content: [{ type: "text", text: message }],
|
|
785
|
+
details: isPermissionDeniedMessage(message)
|
|
786
|
+
? {
|
|
787
|
+
errorType: "permission_denied",
|
|
788
|
+
reason: message,
|
|
789
|
+
toolName: tool?.name ?? toolCall.name,
|
|
790
|
+
toolCallId: toolCall.id,
|
|
791
|
+
}
|
|
792
|
+
: {},
|
|
793
|
+
};
|
|
794
|
+
isError = true;
|
|
795
|
+
}
|
|
796
|
+
stream.push({
|
|
797
|
+
type: "tool_execution_end",
|
|
798
|
+
toolCallId: toolCall.id,
|
|
799
|
+
toolName: toolCall.name,
|
|
800
|
+
result,
|
|
801
|
+
isError,
|
|
802
|
+
durationMs: Date.now() - startedAt,
|
|
803
|
+
});
|
|
804
|
+
const toolResultMessage = {
|
|
805
|
+
role: "toolResult",
|
|
806
|
+
toolCallId: toolCall.id,
|
|
807
|
+
toolName: toolCall.name,
|
|
808
|
+
content: result.content,
|
|
809
|
+
details: result.details,
|
|
810
|
+
isError,
|
|
811
|
+
timestamp: Date.now(),
|
|
812
|
+
};
|
|
813
|
+
results.push(toolResultMessage);
|
|
814
|
+
contextMessages.push(...(result.contextMessages ?? []));
|
|
815
|
+
// Check for steering messages - skip remaining tools if user interrupted
|
|
816
|
+
if (getSteeringMessages) {
|
|
817
|
+
const steering = await getSteeringMessages();
|
|
818
|
+
if (steering.length > 0) {
|
|
819
|
+
steeringMessages = steering;
|
|
820
|
+
const remainingCalls = toolCalls.slice(index + 1);
|
|
821
|
+
for (const skipped of remainingCalls) {
|
|
822
|
+
results.push(skipToolCall(skipped, stream));
|
|
823
|
+
}
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return { toolResults: results, contextMessages, steeringMessages };
|
|
829
|
+
}
|
|
830
|
+
function buildToolMap(tools) {
|
|
831
|
+
const toolByName = new Map();
|
|
832
|
+
for (const tool of tools ?? []) {
|
|
833
|
+
toolByName.set(tool.name, tool);
|
|
834
|
+
for (const alias of tool.aliases ?? []) {
|
|
835
|
+
if (!toolByName.has(alias)) {
|
|
836
|
+
toolByName.set(alias, tool);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return toolByName;
|
|
841
|
+
}
|
|
842
|
+
function isPermissionDeniedMessage(message) {
|
|
843
|
+
return /^Permission (denied|request was cancelled)/i.test(message.trim());
|
|
844
|
+
}
|
|
845
|
+
function enforceMaxResultSize(result, maxResultSizeChars) {
|
|
846
|
+
if (!maxResultSizeChars || maxResultSizeChars <= 0)
|
|
847
|
+
return result;
|
|
848
|
+
let remaining = Math.floor(maxResultSizeChars);
|
|
849
|
+
let truncated = false;
|
|
850
|
+
const content = result.content.map((part) => {
|
|
851
|
+
if (part.type !== "text")
|
|
852
|
+
return part;
|
|
853
|
+
if (remaining <= 0) {
|
|
854
|
+
truncated = true;
|
|
855
|
+
return { ...part, text: "" };
|
|
856
|
+
}
|
|
857
|
+
if (part.text.length <= remaining) {
|
|
858
|
+
remaining -= part.text.length;
|
|
859
|
+
return part;
|
|
860
|
+
}
|
|
861
|
+
truncated = true;
|
|
862
|
+
const text = part.text.slice(0, remaining);
|
|
863
|
+
remaining = 0;
|
|
864
|
+
return { ...part, text };
|
|
865
|
+
});
|
|
866
|
+
if (!truncated)
|
|
867
|
+
return result;
|
|
868
|
+
const note = `\n\n[Tool result truncated to ${Math.floor(maxResultSizeChars)} characters.]`;
|
|
869
|
+
let lastTextIndex = -1;
|
|
870
|
+
for (let i = content.length - 1; i >= 0; i -= 1) {
|
|
871
|
+
if (content[i]?.type === "text") {
|
|
872
|
+
lastTextIndex = i;
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (lastTextIndex >= 0) {
|
|
877
|
+
const part = content[lastTextIndex];
|
|
878
|
+
content[lastTextIndex] = { ...part, text: `${part.text}${note}` };
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
content.push({ type: "text", text: note.trimStart() });
|
|
882
|
+
}
|
|
883
|
+
return { ...result, content };
|
|
884
|
+
}
|
|
885
|
+
function skipToolCall(toolCall, stream) {
|
|
886
|
+
const result = {
|
|
887
|
+
content: [{ type: "text", text: "Skipped due to queued user message." }],
|
|
888
|
+
details: {},
|
|
889
|
+
};
|
|
890
|
+
stream.push({
|
|
891
|
+
type: "tool_execution_start",
|
|
892
|
+
toolCallId: toolCall.id,
|
|
893
|
+
toolName: toolCall.name,
|
|
894
|
+
args: toolCall.arguments,
|
|
895
|
+
});
|
|
896
|
+
stream.push({
|
|
897
|
+
type: "tool_execution_end",
|
|
898
|
+
toolCallId: toolCall.id,
|
|
899
|
+
toolName: toolCall.name,
|
|
900
|
+
result,
|
|
901
|
+
isError: true,
|
|
902
|
+
});
|
|
903
|
+
const toolResultMessage = {
|
|
904
|
+
role: "toolResult",
|
|
905
|
+
toolCallId: toolCall.id,
|
|
906
|
+
toolName: toolCall.name,
|
|
907
|
+
content: result.content,
|
|
908
|
+
details: {},
|
|
909
|
+
isError: true,
|
|
910
|
+
timestamp: Date.now(),
|
|
911
|
+
};
|
|
912
|
+
return toolResultMessage;
|
|
913
|
+
}
|