@clinebot/agents 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -0
- package/dist/agent-input.d.ts +2 -0
- package/dist/agent.d.ts +56 -0
- package/dist/extensions.d.ts +21 -0
- package/dist/hooks/engine.d.ts +42 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/lifecycle.d.ts +5 -0
- package/dist/hooks/node.d.ts +2 -0
- package/dist/hooks/subprocess-runner.d.ts +16 -0
- package/dist/hooks/subprocess.d.ts +268 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +49 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +49 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/policies.d.ts +14 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/types.d.ts +35 -0
- package/dist/message-builder.d.ts +31 -0
- package/dist/prompts/cline.d.ts +1 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/runtime/agent-runtime-bus.d.ts +13 -0
- package/dist/runtime/conversation-store.d.ts +16 -0
- package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
- package/dist/runtime/tool-orchestrator.d.ts +39 -0
- package/dist/runtime/turn-processor.d.ts +21 -0
- package/dist/teams/index.d.ts +3 -0
- package/dist/teams/multi-agent.d.ts +566 -0
- package/dist/teams/spawn-agent-tool.d.ts +85 -0
- package/dist/teams/team-tools.d.ts +51 -0
- package/dist/tools/ask-question.d.ts +12 -0
- package/dist/tools/create.d.ts +59 -0
- package/dist/tools/execution.d.ts +61 -0
- package/dist/tools/formatting.d.ts +20 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/registry.d.ts +26 -0
- package/dist/tools/validation.d.ts +27 -0
- package/dist/types.d.ts +826 -0
- package/package.json +54 -0
- package/src/agent-input.ts +116 -0
- package/src/agent.test.ts +931 -0
- package/src/agent.ts +1050 -0
- package/src/example.test.ts +564 -0
- package/src/extensions.ts +337 -0
- package/src/hooks/engine.test.ts +163 -0
- package/src/hooks/engine.ts +537 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/lifecycle.ts +239 -0
- package/src/hooks/node.ts +18 -0
- package/src/hooks/subprocess-runner.ts +140 -0
- package/src/hooks/subprocess.test.ts +180 -0
- package/src/hooks/subprocess.ts +620 -0
- package/src/index.browser.ts +1 -0
- package/src/index.node.ts +21 -0
- package/src/index.ts +133 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/policies.test.ts +51 -0
- package/src/mcp/policies.ts +53 -0
- package/src/mcp/tools.test.ts +76 -0
- package/src/mcp/tools.ts +60 -0
- package/src/mcp/types.ts +41 -0
- package/src/message-builder.test.ts +175 -0
- package/src/message-builder.ts +429 -0
- package/src/prompts/cline.ts +49 -0
- package/src/prompts/index.ts +1 -0
- package/src/runtime/agent-runtime-bus.ts +53 -0
- package/src/runtime/conversation-store.ts +61 -0
- package/src/runtime/lifecycle-orchestrator.ts +90 -0
- package/src/runtime/tool-orchestrator.ts +177 -0
- package/src/runtime/turn-processor.ts +250 -0
- package/src/streaming.test.ts +197 -0
- package/src/streaming.ts +307 -0
- package/src/teams/index.ts +63 -0
- package/src/teams/multi-agent.lifecycle.test.ts +48 -0
- package/src/teams/multi-agent.ts +1866 -0
- package/src/teams/spawn-agent-tool.test.ts +172 -0
- package/src/teams/spawn-agent-tool.ts +223 -0
- package/src/teams/team-tools.test.ts +448 -0
- package/src/teams/team-tools.ts +929 -0
- package/src/tools/ask-question.ts +78 -0
- package/src/tools/create.ts +104 -0
- package/src/tools/execution.ts +311 -0
- package/src/tools/formatting.ts +73 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/tools.test.ts +292 -0
- package/src/tools/validation.ts +73 -0
- package/src/types.ts +966 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Class
|
|
3
|
+
*
|
|
4
|
+
* The main class for building and running agentic loops with LLMs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { providers } from "@clinebot/llms";
|
|
8
|
+
import { buildInitialUserContent } from "./agent-input.js";
|
|
9
|
+
import {
|
|
10
|
+
type ContributionRegistry,
|
|
11
|
+
createContributionRegistry,
|
|
12
|
+
} from "./extensions.js";
|
|
13
|
+
import { HookEngine, registerLifecycleHandlers } from "./hooks/index.js";
|
|
14
|
+
import { MessageBuilder } from "./message-builder.js";
|
|
15
|
+
import { createAgentRuntimeBus } from "./runtime/agent-runtime-bus.js";
|
|
16
|
+
import { ConversationStore } from "./runtime/conversation-store.js";
|
|
17
|
+
import { LifecycleOrchestrator } from "./runtime/lifecycle-orchestrator.js";
|
|
18
|
+
import { ToolOrchestrator } from "./runtime/tool-orchestrator.js";
|
|
19
|
+
import { TurnProcessor } from "./runtime/turn-processor.js";
|
|
20
|
+
import { createToolRegistry, validateTools } from "./tools/index.js";
|
|
21
|
+
import type {
|
|
22
|
+
AgentConfig,
|
|
23
|
+
AgentEvent,
|
|
24
|
+
AgentExtensionRegistry,
|
|
25
|
+
AgentFinishReason,
|
|
26
|
+
AgentResult,
|
|
27
|
+
AgentUsage,
|
|
28
|
+
BasicLogger,
|
|
29
|
+
PendingToolCall,
|
|
30
|
+
Tool,
|
|
31
|
+
ToolApprovalResult,
|
|
32
|
+
ToolCallRecord,
|
|
33
|
+
ToolContext,
|
|
34
|
+
ToolPolicy,
|
|
35
|
+
} from "./types.js";
|
|
36
|
+
|
|
37
|
+
const DEFAULT_REMINDER_TEXT =
|
|
38
|
+
"REMINDER: If you have gathered enough information to answer the user's question, please provide your final answer now without using any more tools.";
|
|
39
|
+
|
|
40
|
+
function resolveKnownModelsFromConfig(
|
|
41
|
+
config: AgentConfig,
|
|
42
|
+
): Record<string, providers.ModelInfo> | undefined {
|
|
43
|
+
if (config.providerConfig?.knownModels) {
|
|
44
|
+
return config.providerConfig.knownModels;
|
|
45
|
+
}
|
|
46
|
+
if (config.knownModels) {
|
|
47
|
+
return config.knownModels;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const providerConfig = providers.toProviderConfig({
|
|
52
|
+
provider: config.providerId,
|
|
53
|
+
model: config.modelId,
|
|
54
|
+
apiKey: config.apiKey,
|
|
55
|
+
baseUrl: config.baseUrl,
|
|
56
|
+
headers: config.headers,
|
|
57
|
+
});
|
|
58
|
+
return providerConfig.knownModels;
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class Agent {
|
|
65
|
+
private config: Required<
|
|
66
|
+
Pick<
|
|
67
|
+
AgentConfig,
|
|
68
|
+
| "providerId"
|
|
69
|
+
| "modelId"
|
|
70
|
+
| "systemPrompt"
|
|
71
|
+
| "tools"
|
|
72
|
+
| "maxParallelToolCalls"
|
|
73
|
+
| "apiTimeoutMs"
|
|
74
|
+
| "maxTokensPerTurn"
|
|
75
|
+
| "reminderAfterIterations"
|
|
76
|
+
| "reminderText"
|
|
77
|
+
| "hookErrorMode"
|
|
78
|
+
>
|
|
79
|
+
> &
|
|
80
|
+
AgentConfig;
|
|
81
|
+
private handler: providers.ApiHandler;
|
|
82
|
+
private toolRegistry: Map<string, Tool>;
|
|
83
|
+
private abortController: AbortController | null = null;
|
|
84
|
+
private contributionRegistry: ContributionRegistry;
|
|
85
|
+
private readonly hookEngine: HookEngine;
|
|
86
|
+
private messageBuilder: MessageBuilder;
|
|
87
|
+
private readonly logger?: BasicLogger;
|
|
88
|
+
private extensionsInitialized = false;
|
|
89
|
+
private activeRunId = "";
|
|
90
|
+
private runState: "idle" | "running" | "shutting_down" = "idle";
|
|
91
|
+
private readonly runtimeBus = createAgentRuntimeBus();
|
|
92
|
+
private readonly conversationStore: ConversationStore;
|
|
93
|
+
private readonly lifecycle: LifecycleOrchestrator;
|
|
94
|
+
private turnProcessor: TurnProcessor;
|
|
95
|
+
private readonly toolOrchestrator: ToolOrchestrator;
|
|
96
|
+
private readonly agentId: string;
|
|
97
|
+
private readonly parentAgentId: string | null;
|
|
98
|
+
|
|
99
|
+
constructor(config: AgentConfig) {
|
|
100
|
+
this.config = {
|
|
101
|
+
...config,
|
|
102
|
+
maxIterations: config.maxIterations,
|
|
103
|
+
maxParallelToolCalls: config.maxParallelToolCalls ?? 8,
|
|
104
|
+
apiTimeoutMs: config.apiTimeoutMs ?? 120000,
|
|
105
|
+
maxTokensPerTurn: config.maxTokensPerTurn ?? 8192,
|
|
106
|
+
reminderAfterIterations: config.reminderAfterIterations ?? 50,
|
|
107
|
+
reminderText: config.reminderText ?? DEFAULT_REMINDER_TEXT,
|
|
108
|
+
hookErrorMode: config.hookErrorMode ?? "ignore",
|
|
109
|
+
extensions: config.extensions ?? [],
|
|
110
|
+
toolPolicies: config.toolPolicies ?? {},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.agentId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
114
|
+
this.parentAgentId = config.parentAgentId ?? null;
|
|
115
|
+
this.conversationStore = new ConversationStore(
|
|
116
|
+
config.initialMessages ?? [],
|
|
117
|
+
);
|
|
118
|
+
this.logger = config.logger;
|
|
119
|
+
|
|
120
|
+
this.contributionRegistry = createContributionRegistry({
|
|
121
|
+
extensions: this.config.extensions,
|
|
122
|
+
});
|
|
123
|
+
this.contributionRegistry.resolve();
|
|
124
|
+
this.contributionRegistry.validate();
|
|
125
|
+
|
|
126
|
+
const defaultFailureMode =
|
|
127
|
+
this.config.hookErrorMode === "throw" ? "fail_closed" : "fail_open";
|
|
128
|
+
this.hookEngine = new HookEngine({
|
|
129
|
+
policies: {
|
|
130
|
+
defaultPolicy: {
|
|
131
|
+
failureMode: defaultFailureMode,
|
|
132
|
+
},
|
|
133
|
+
...this.config.hookPolicies,
|
|
134
|
+
},
|
|
135
|
+
onDispatchError: (error) => {
|
|
136
|
+
this.reportRecoverableError(error);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
registerLifecycleHandlers(this.hookEngine, {
|
|
141
|
+
...this.config,
|
|
142
|
+
extensions: this.contributionRegistry.getValidatedExtensions(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.messageBuilder = new MessageBuilder();
|
|
146
|
+
this.toolRegistry = createToolRegistry([]);
|
|
147
|
+
this.handler = this.createHandlerFromConfig(this.config);
|
|
148
|
+
this.turnProcessor = new TurnProcessor({
|
|
149
|
+
handler: this.handler,
|
|
150
|
+
messageBuilder: this.messageBuilder,
|
|
151
|
+
emit: (event) => this.emit(event),
|
|
152
|
+
});
|
|
153
|
+
this.lifecycle = new LifecycleOrchestrator({
|
|
154
|
+
hookEngine: this.hookEngine,
|
|
155
|
+
runtimeBus: this.runtimeBus,
|
|
156
|
+
getRunId: () =>
|
|
157
|
+
this.activeRunId || this.conversationStore.getConversationId(),
|
|
158
|
+
getAgentId: () => this.agentId,
|
|
159
|
+
getConversationId: () => this.conversationStore.getConversationId(),
|
|
160
|
+
getParentAgentId: () => this.parentAgentId,
|
|
161
|
+
onHookContext: (source, context) =>
|
|
162
|
+
this.appendHookContext(source, context),
|
|
163
|
+
onDispatchError: (error) => this.reportRecoverableError(error),
|
|
164
|
+
});
|
|
165
|
+
this.toolOrchestrator = new ToolOrchestrator({
|
|
166
|
+
getAgentId: () => this.agentId,
|
|
167
|
+
getConversationId: () => this.conversationStore.getConversationId(),
|
|
168
|
+
getParentAgentId: () => this.parentAgentId,
|
|
169
|
+
emit: (event) => this.emit(event),
|
|
170
|
+
dispatchLifecycle: ({ source, iteration, stage, payload }) =>
|
|
171
|
+
this.lifecycle.dispatch(source, {
|
|
172
|
+
stage,
|
|
173
|
+
iteration,
|
|
174
|
+
payload,
|
|
175
|
+
}),
|
|
176
|
+
authorizeToolCall: (call, context) =>
|
|
177
|
+
this.authorizeToolCall(call, context),
|
|
178
|
+
onCancelRequested: () => {
|
|
179
|
+
this.abortController?.abort();
|
|
180
|
+
},
|
|
181
|
+
onLog: (level, message, metadata) => {
|
|
182
|
+
this.log(level, message, metadata);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// onEvent callback and runtime hooks are both runtime-bus subscribers.
|
|
187
|
+
this.runtimeBus.subscribeRuntimeEvent((event) => {
|
|
188
|
+
try {
|
|
189
|
+
this.config.onEvent?.(event);
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore callback errors
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
this.runtimeBus.subscribeRuntimeEvent((event) => {
|
|
195
|
+
this.lifecycle.dispatchRuntimeEvent(event);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async run(
|
|
200
|
+
userMessage: string,
|
|
201
|
+
userImages?: string[],
|
|
202
|
+
userFiles?: string[],
|
|
203
|
+
): Promise<AgentResult> {
|
|
204
|
+
this.assertCanStartRun();
|
|
205
|
+
this.log("info", "Agent run requested", {
|
|
206
|
+
agentId: this.agentId,
|
|
207
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
208
|
+
messageLength: userMessage.length,
|
|
209
|
+
});
|
|
210
|
+
await this.ensureExtensionsInitialized();
|
|
211
|
+
|
|
212
|
+
this.conversationStore.resetForRun();
|
|
213
|
+
|
|
214
|
+
const preparedInput = await this.prepareUserInput(userMessage, "run");
|
|
215
|
+
if (preparedInput.cancel) {
|
|
216
|
+
return this.buildAbortedResult(new Date(), "");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.conversationStore.appendMessage({
|
|
220
|
+
role: "user",
|
|
221
|
+
content: await this.buildInitialUserContent(
|
|
222
|
+
preparedInput.input,
|
|
223
|
+
userImages,
|
|
224
|
+
userFiles,
|
|
225
|
+
),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return this.executeLoop(preparedInput.input);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async continue(
|
|
232
|
+
userMessage: string,
|
|
233
|
+
userImages?: string[],
|
|
234
|
+
userFiles?: string[],
|
|
235
|
+
): Promise<AgentResult> {
|
|
236
|
+
this.assertCanStartRun();
|
|
237
|
+
this.log("info", "Agent continue requested", {
|
|
238
|
+
agentId: this.agentId,
|
|
239
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
240
|
+
messageLength: userMessage.length,
|
|
241
|
+
});
|
|
242
|
+
await this.ensureExtensionsInitialized();
|
|
243
|
+
|
|
244
|
+
const preparedInput = await this.prepareUserInput(userMessage, "continue");
|
|
245
|
+
if (preparedInput.cancel) {
|
|
246
|
+
return this.buildAbortedResult(new Date(), "");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.conversationStore.appendMessage({
|
|
250
|
+
role: "user",
|
|
251
|
+
content: await this.buildInitialUserContent(
|
|
252
|
+
preparedInput.input,
|
|
253
|
+
userImages,
|
|
254
|
+
userFiles,
|
|
255
|
+
),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return this.executeLoop(preparedInput.input);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getMessages(): providers.Message[] {
|
|
262
|
+
return this.conversationStore.getMessages();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
clearHistory(): void {
|
|
266
|
+
this.conversationStore.clearHistory();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
restore(messages: providers.Message[]): void {
|
|
270
|
+
this.conversationStore.restore(messages);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
abort(): void {
|
|
274
|
+
this.abortController?.abort();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
subscribeEvents(listener: (event: AgentEvent) => void): () => void {
|
|
278
|
+
return this.runtimeBus.subscribeRuntimeEvent(listener);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async shutdown(reason?: string): Promise<void> {
|
|
282
|
+
if (this.runState === "running") {
|
|
283
|
+
throw new Error("Cannot shutdown agent while a run is in progress");
|
|
284
|
+
}
|
|
285
|
+
if (this.runState === "shutting_down") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.runState = "shutting_down";
|
|
289
|
+
try {
|
|
290
|
+
await this.lifecycle.dispatch("hook.session_shutdown", {
|
|
291
|
+
stage: "session_shutdown",
|
|
292
|
+
payload: {
|
|
293
|
+
agentId: this.agentId,
|
|
294
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
295
|
+
parentAgentId: this.parentAgentId,
|
|
296
|
+
reason,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
await this.lifecycle.shutdown();
|
|
300
|
+
} finally {
|
|
301
|
+
this.runState = "idle";
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
getExtensionRegistry(): AgentExtensionRegistry {
|
|
306
|
+
return this.contributionRegistry.getRegistrySnapshot();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getAgentId(): string {
|
|
310
|
+
return this.agentId;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getConversationId(): string {
|
|
314
|
+
return this.conversationStore.getConversationId();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
updateConnection(
|
|
318
|
+
overrides: Partial<
|
|
319
|
+
Pick<
|
|
320
|
+
AgentConfig,
|
|
321
|
+
| "providerId"
|
|
322
|
+
| "modelId"
|
|
323
|
+
| "apiKey"
|
|
324
|
+
| "baseUrl"
|
|
325
|
+
| "headers"
|
|
326
|
+
| "knownModels"
|
|
327
|
+
| "reasoningEffort"
|
|
328
|
+
| "thinkingBudgetTokens"
|
|
329
|
+
| "thinking"
|
|
330
|
+
| "abortSignal"
|
|
331
|
+
>
|
|
332
|
+
>,
|
|
333
|
+
): void {
|
|
334
|
+
this.config = {
|
|
335
|
+
...this.config,
|
|
336
|
+
...overrides,
|
|
337
|
+
};
|
|
338
|
+
this.handler = this.createHandlerFromConfig(this.config);
|
|
339
|
+
this.turnProcessor = new TurnProcessor({
|
|
340
|
+
handler: this.handler,
|
|
341
|
+
messageBuilder: this.messageBuilder,
|
|
342
|
+
emit: (event) => this.emit(event),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private assertCanStartRun(): void {
|
|
347
|
+
if (this.runState === "running") {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"Cannot start a new run while another run is already in progress",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (this.runState === "shutting_down") {
|
|
353
|
+
throw new Error("Cannot start a run while agent is shutting down");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private createHandlerFromConfig(config: AgentConfig): providers.ApiHandler {
|
|
358
|
+
const baseProviderConfig =
|
|
359
|
+
config.providerConfig?.providerId === config.providerId
|
|
360
|
+
? config.providerConfig
|
|
361
|
+
: undefined;
|
|
362
|
+
const normalizedProviderConfig: providers.ProviderConfig = {
|
|
363
|
+
...(baseProviderConfig ?? {}),
|
|
364
|
+
providerId: config.providerId,
|
|
365
|
+
modelId: config.modelId,
|
|
366
|
+
apiKey: config.apiKey ?? baseProviderConfig?.apiKey,
|
|
367
|
+
baseUrl: config.baseUrl ?? baseProviderConfig?.baseUrl,
|
|
368
|
+
headers: config.headers ?? baseProviderConfig?.headers,
|
|
369
|
+
knownModels: resolveKnownModelsFromConfig(config),
|
|
370
|
+
maxOutputTokens: config.maxTokensPerTurn,
|
|
371
|
+
reasoningEffort: config.reasoningEffort,
|
|
372
|
+
thinkingBudgetTokens: config.thinkingBudgetTokens,
|
|
373
|
+
thinking: config.thinking,
|
|
374
|
+
abortSignal: config.abortSignal,
|
|
375
|
+
};
|
|
376
|
+
return providers.createHandler(normalizedProviderConfig);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async executeLoop(triggerMessage: string): Promise<AgentResult> {
|
|
380
|
+
if (this.runState !== "idle") {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Cannot start agent run while state is "${this.runState}"`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
this.runState = "running";
|
|
386
|
+
const startedAt = new Date();
|
|
387
|
+
const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
388
|
+
this.activeRunId = runId;
|
|
389
|
+
this.abortController = new AbortController();
|
|
390
|
+
this.log("info", "Agent loop started", {
|
|
391
|
+
agentId: this.agentId,
|
|
392
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
393
|
+
runId,
|
|
394
|
+
triggerLength: triggerMessage.length,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const abortSignal = this.mergeAbortSignals(
|
|
398
|
+
this.config.abortSignal,
|
|
399
|
+
this.abortController.signal,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
let iteration = 0;
|
|
403
|
+
let finishReason: AgentFinishReason = "completed";
|
|
404
|
+
let finalText = "";
|
|
405
|
+
const allToolCalls: ToolCallRecord[] = [];
|
|
406
|
+
const totalUsage: AgentUsage = {
|
|
407
|
+
inputTokens: 0,
|
|
408
|
+
outputTokens: 0,
|
|
409
|
+
cacheReadTokens: 0,
|
|
410
|
+
cacheWriteTokens: 0,
|
|
411
|
+
totalCost: undefined,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
if (!this.conversationStore.isSessionStarted()) {
|
|
416
|
+
const sessionStartControl = await this.lifecycle.dispatch(
|
|
417
|
+
"hook.session_start",
|
|
418
|
+
{
|
|
419
|
+
stage: "session_start",
|
|
420
|
+
payload: {
|
|
421
|
+
agentId: this.agentId,
|
|
422
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
423
|
+
parentAgentId: this.parentAgentId,
|
|
424
|
+
schedule: this.config.schedule,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
if (sessionStartControl?.cancel) {
|
|
429
|
+
finishReason = "aborted";
|
|
430
|
+
}
|
|
431
|
+
this.conversationStore.markSessionStarted();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const runStartControl = await this.lifecycle.dispatch("hook.run_start", {
|
|
435
|
+
stage: "run_start",
|
|
436
|
+
payload: {
|
|
437
|
+
agentId: this.agentId,
|
|
438
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
439
|
+
parentAgentId: this.parentAgentId,
|
|
440
|
+
userMessage: triggerMessage,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
if (runStartControl?.cancel) {
|
|
444
|
+
finishReason = "aborted";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
while (finishReason !== "aborted") {
|
|
448
|
+
if (
|
|
449
|
+
this.config.maxIterations !== undefined &&
|
|
450
|
+
iteration >= this.config.maxIterations
|
|
451
|
+
) {
|
|
452
|
+
finishReason = "max_iterations";
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
if (abortSignal.aborted) {
|
|
456
|
+
finishReason = "aborted";
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
iteration++;
|
|
461
|
+
this.log("debug", "Agent iteration started", {
|
|
462
|
+
agentId: this.agentId,
|
|
463
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
464
|
+
runId,
|
|
465
|
+
iteration,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const iterationStartControl = await this.lifecycle.dispatch(
|
|
469
|
+
"hook.iteration_start",
|
|
470
|
+
{
|
|
471
|
+
stage: "iteration_start",
|
|
472
|
+
iteration,
|
|
473
|
+
payload: {
|
|
474
|
+
agentId: this.agentId,
|
|
475
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
476
|
+
parentAgentId: this.parentAgentId,
|
|
477
|
+
iteration,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
if (iterationStartControl?.cancel) {
|
|
482
|
+
finishReason = "aborted";
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.emit({ type: "iteration_start", iteration });
|
|
487
|
+
|
|
488
|
+
const turnStartControl = await this.lifecycle.dispatch(
|
|
489
|
+
"hook.turn_start",
|
|
490
|
+
{
|
|
491
|
+
stage: "turn_start",
|
|
492
|
+
iteration,
|
|
493
|
+
payload: {
|
|
494
|
+
agentId: this.agentId,
|
|
495
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
496
|
+
parentAgentId: this.parentAgentId,
|
|
497
|
+
iteration,
|
|
498
|
+
messages: this.conversationStore.getMessages(),
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
if (turnStartControl?.cancel) {
|
|
503
|
+
finishReason = "aborted";
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const beforeAgentStartControl = await this.lifecycle.dispatch(
|
|
508
|
+
"hook.before_agent_start",
|
|
509
|
+
{
|
|
510
|
+
stage: "before_agent_start",
|
|
511
|
+
iteration,
|
|
512
|
+
payload: {
|
|
513
|
+
agentId: this.agentId,
|
|
514
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
515
|
+
parentAgentId: this.parentAgentId,
|
|
516
|
+
iteration,
|
|
517
|
+
systemPrompt: this.config.systemPrompt,
|
|
518
|
+
messages: this.conversationStore.getMessages(),
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
const turnSystemPrompt =
|
|
523
|
+
typeof beforeAgentStartControl?.systemPrompt === "string"
|
|
524
|
+
? beforeAgentStartControl.systemPrompt
|
|
525
|
+
: this.config.systemPrompt;
|
|
526
|
+
if (beforeAgentStartControl?.cancel) {
|
|
527
|
+
finishReason = "aborted";
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
if (
|
|
531
|
+
beforeAgentStartControl?.appendMessages &&
|
|
532
|
+
beforeAgentStartControl.appendMessages.length > 0
|
|
533
|
+
) {
|
|
534
|
+
this.conversationStore.appendMessages(
|
|
535
|
+
beforeAgentStartControl.appendMessages,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const { turn, assistantMessage } = await this.turnProcessor.processTurn(
|
|
540
|
+
this.conversationStore.getMessages(),
|
|
541
|
+
turnSystemPrompt,
|
|
542
|
+
this.config.tools,
|
|
543
|
+
abortSignal,
|
|
544
|
+
);
|
|
545
|
+
if (assistantMessage) {
|
|
546
|
+
this.conversationStore.appendMessage(assistantMessage);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const turnEndControl = await this.lifecycle.dispatch("hook.turn_end", {
|
|
550
|
+
stage: "turn_end",
|
|
551
|
+
iteration,
|
|
552
|
+
payload: {
|
|
553
|
+
agentId: this.agentId,
|
|
554
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
555
|
+
parentAgentId: this.parentAgentId,
|
|
556
|
+
iteration,
|
|
557
|
+
turn,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
if (turnEndControl?.cancel) {
|
|
561
|
+
finishReason = "aborted";
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
finalText = turn.text;
|
|
566
|
+
totalUsage.inputTokens += turn.usage.inputTokens;
|
|
567
|
+
totalUsage.outputTokens += turn.usage.outputTokens;
|
|
568
|
+
totalUsage.cacheReadTokens =
|
|
569
|
+
(totalUsage.cacheReadTokens ?? 0) + (turn.usage.cacheReadTokens ?? 0);
|
|
570
|
+
totalUsage.cacheWriteTokens =
|
|
571
|
+
(totalUsage.cacheWriteTokens ?? 0) +
|
|
572
|
+
(turn.usage.cacheWriteTokens ?? 0);
|
|
573
|
+
if (typeof turn.usage.cost === "number") {
|
|
574
|
+
totalUsage.totalCost = (totalUsage.totalCost ?? 0) + turn.usage.cost;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.emit({
|
|
578
|
+
type: "usage",
|
|
579
|
+
inputTokens: turn.usage.inputTokens,
|
|
580
|
+
outputTokens: turn.usage.outputTokens,
|
|
581
|
+
cacheReadTokens: turn.usage.cacheReadTokens,
|
|
582
|
+
cacheWriteTokens: turn.usage.cacheWriteTokens,
|
|
583
|
+
cost: turn.usage.cost,
|
|
584
|
+
totalInputTokens: totalUsage.inputTokens,
|
|
585
|
+
totalOutputTokens: totalUsage.outputTokens,
|
|
586
|
+
totalCost: totalUsage.totalCost,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (turn.toolCalls.length === 0) {
|
|
590
|
+
// Check completion guard before allowing the loop to end.
|
|
591
|
+
// If the guard returns a nudge string, inject it and continue.
|
|
592
|
+
const guardNudge = this.config.completionGuard?.();
|
|
593
|
+
if (guardNudge) {
|
|
594
|
+
this.log("info", "Completion guard prevented early exit", {
|
|
595
|
+
agentId: this.agentId,
|
|
596
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
597
|
+
runId,
|
|
598
|
+
iteration,
|
|
599
|
+
});
|
|
600
|
+
this.conversationStore.appendMessage({
|
|
601
|
+
role: "user",
|
|
602
|
+
content: [{ type: "text", text: guardNudge }],
|
|
603
|
+
});
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
this.emit({
|
|
608
|
+
type: "iteration_end",
|
|
609
|
+
iteration,
|
|
610
|
+
hadToolCalls: false,
|
|
611
|
+
toolCallCount: 0,
|
|
612
|
+
});
|
|
613
|
+
await this.lifecycle.dispatch("hook.iteration_end", {
|
|
614
|
+
stage: "iteration_end",
|
|
615
|
+
iteration,
|
|
616
|
+
payload: {
|
|
617
|
+
agentId: this.agentId,
|
|
618
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
619
|
+
parentAgentId: this.parentAgentId,
|
|
620
|
+
iteration,
|
|
621
|
+
hadToolCalls: false,
|
|
622
|
+
toolCallCount: 0,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
finishReason = "completed";
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const context: ToolContext = {
|
|
630
|
+
agentId: this.agentId,
|
|
631
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
632
|
+
iteration,
|
|
633
|
+
abortSignal,
|
|
634
|
+
};
|
|
635
|
+
const { results: toolResults, cancelRequested } =
|
|
636
|
+
await this.toolOrchestrator.execute(
|
|
637
|
+
this.toolRegistry,
|
|
638
|
+
turn.toolCalls,
|
|
639
|
+
context,
|
|
640
|
+
{ iteration, runId },
|
|
641
|
+
{ maxConcurrency: this.config.maxParallelToolCalls },
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
allToolCalls.push(...toolResults);
|
|
645
|
+
this.conversationStore.appendMessage(
|
|
646
|
+
this.toolOrchestrator.buildToolResultMessage(toolResults, iteration, {
|
|
647
|
+
afterIterations: this.config.reminderAfterIterations,
|
|
648
|
+
text: this.config.reminderText,
|
|
649
|
+
}),
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
this.emit({
|
|
653
|
+
type: "iteration_end",
|
|
654
|
+
iteration,
|
|
655
|
+
hadToolCalls: true,
|
|
656
|
+
toolCallCount: turn.toolCalls.length,
|
|
657
|
+
});
|
|
658
|
+
await this.lifecycle.dispatch("hook.iteration_end", {
|
|
659
|
+
stage: "iteration_end",
|
|
660
|
+
iteration,
|
|
661
|
+
payload: {
|
|
662
|
+
agentId: this.agentId,
|
|
663
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
664
|
+
parentAgentId: this.parentAgentId,
|
|
665
|
+
iteration,
|
|
666
|
+
hadToolCalls: true,
|
|
667
|
+
toolCallCount: turn.toolCalls.length,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
if (cancelRequested) {
|
|
671
|
+
this.log("warn", "Agent iteration cancelled by tool lifecycle", {
|
|
672
|
+
agentId: this.agentId,
|
|
673
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
674
|
+
runId,
|
|
675
|
+
iteration,
|
|
676
|
+
});
|
|
677
|
+
finishReason = "aborted";
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
this.log("debug", "Agent iteration finished", {
|
|
681
|
+
agentId: this.agentId,
|
|
682
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
683
|
+
runId,
|
|
684
|
+
iteration,
|
|
685
|
+
toolCalls: turn.toolCalls.length,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
} catch (error) {
|
|
689
|
+
finishReason = "error";
|
|
690
|
+
this.log("error", "Agent loop failed", {
|
|
691
|
+
agentId: this.agentId,
|
|
692
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
693
|
+
runId,
|
|
694
|
+
error,
|
|
695
|
+
});
|
|
696
|
+
const errorObj =
|
|
697
|
+
error instanceof Error ? error : new Error(String(error));
|
|
698
|
+
await this.lifecycle.dispatch("hook.error", {
|
|
699
|
+
stage: "error",
|
|
700
|
+
iteration,
|
|
701
|
+
payload: {
|
|
702
|
+
agentId: this.agentId,
|
|
703
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
704
|
+
parentAgentId: this.parentAgentId,
|
|
705
|
+
iteration,
|
|
706
|
+
error: errorObj,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
this.emit({
|
|
710
|
+
type: "error",
|
|
711
|
+
error: errorObj,
|
|
712
|
+
recoverable: false,
|
|
713
|
+
iteration,
|
|
714
|
+
});
|
|
715
|
+
throw error;
|
|
716
|
+
} finally {
|
|
717
|
+
this.abortController = null;
|
|
718
|
+
this.activeRunId = "";
|
|
719
|
+
if (this.runState === "running") {
|
|
720
|
+
this.runState = "idle";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const endedAt = new Date();
|
|
725
|
+
const durationMs = endedAt.getTime() - startedAt.getTime();
|
|
726
|
+
const modelInfo = this.handler.getModel();
|
|
727
|
+
|
|
728
|
+
this.emit({
|
|
729
|
+
type: "done",
|
|
730
|
+
reason: finishReason,
|
|
731
|
+
text: finalText,
|
|
732
|
+
iterations: iteration,
|
|
733
|
+
});
|
|
734
|
+
this.log("info", "Agent loop finished", {
|
|
735
|
+
agentId: this.agentId,
|
|
736
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
737
|
+
runId,
|
|
738
|
+
finishReason,
|
|
739
|
+
iterations: iteration,
|
|
740
|
+
durationMs,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const result = {
|
|
744
|
+
text: finalText,
|
|
745
|
+
usage: totalUsage,
|
|
746
|
+
messages: this.conversationStore.getMessages(),
|
|
747
|
+
toolCalls: allToolCalls,
|
|
748
|
+
iterations: iteration,
|
|
749
|
+
finishReason,
|
|
750
|
+
model: {
|
|
751
|
+
id: modelInfo.id,
|
|
752
|
+
provider: this.config.providerId,
|
|
753
|
+
info: modelInfo.info,
|
|
754
|
+
},
|
|
755
|
+
startedAt,
|
|
756
|
+
endedAt,
|
|
757
|
+
durationMs,
|
|
758
|
+
};
|
|
759
|
+
await this.lifecycle.dispatch("hook.run_end", {
|
|
760
|
+
stage: "run_end",
|
|
761
|
+
iteration,
|
|
762
|
+
payload: {
|
|
763
|
+
agentId: this.agentId,
|
|
764
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
765
|
+
parentAgentId: this.parentAgentId,
|
|
766
|
+
result,
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
await this.lifecycle.shutdown();
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private async ensureExtensionsInitialized(): Promise<void> {
|
|
774
|
+
if (this.extensionsInitialized) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
await this.contributionRegistry.initialize();
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (this.config.hookErrorMode === "throw") {
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
this.emit({
|
|
785
|
+
type: "error",
|
|
786
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
787
|
+
recoverable: true,
|
|
788
|
+
iteration: 0,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const mergedTools = [
|
|
792
|
+
...this.config.tools,
|
|
793
|
+
...this.contributionRegistry.getRegisteredTools(),
|
|
794
|
+
];
|
|
795
|
+
validateTools(mergedTools);
|
|
796
|
+
this.config.tools = mergedTools;
|
|
797
|
+
this.toolRegistry = createToolRegistry(mergedTools);
|
|
798
|
+
this.extensionsInitialized = true;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private async prepareUserInput(
|
|
802
|
+
userMessage: string,
|
|
803
|
+
mode: "run" | "continue",
|
|
804
|
+
): Promise<{ input: string; cancel: boolean }> {
|
|
805
|
+
const control = await this.lifecycle.dispatch("hook.input", {
|
|
806
|
+
stage: "input",
|
|
807
|
+
payload: {
|
|
808
|
+
agentId: this.agentId,
|
|
809
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
810
|
+
parentAgentId: this.parentAgentId,
|
|
811
|
+
mode,
|
|
812
|
+
input: userMessage,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
const input =
|
|
816
|
+
Object.hasOwn(control ?? {}, "overrideInput") &&
|
|
817
|
+
typeof control?.overrideInput === "string"
|
|
818
|
+
? control.overrideInput
|
|
819
|
+
: userMessage;
|
|
820
|
+
if (control?.cancel) {
|
|
821
|
+
return { input, cancel: true };
|
|
822
|
+
}
|
|
823
|
+
return { input, cancel: false };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private async buildInitialUserContent(
|
|
827
|
+
userMessage: string,
|
|
828
|
+
userImages?: string[],
|
|
829
|
+
userFiles?: string[],
|
|
830
|
+
): Promise<string | providers.ContentBlock[]> {
|
|
831
|
+
return buildInitialUserContent(
|
|
832
|
+
userMessage,
|
|
833
|
+
userImages,
|
|
834
|
+
userFiles,
|
|
835
|
+
this.config.userFileContentLoader,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private buildAbortedResult(startedAt: Date, text: string): AgentResult {
|
|
840
|
+
const endedAt = new Date();
|
|
841
|
+
const modelInfo = this.handler.getModel();
|
|
842
|
+
return {
|
|
843
|
+
text,
|
|
844
|
+
usage: {
|
|
845
|
+
inputTokens: 0,
|
|
846
|
+
outputTokens: 0,
|
|
847
|
+
cacheReadTokens: 0,
|
|
848
|
+
cacheWriteTokens: 0,
|
|
849
|
+
totalCost: undefined,
|
|
850
|
+
},
|
|
851
|
+
messages: this.conversationStore.getMessages(),
|
|
852
|
+
toolCalls: [],
|
|
853
|
+
iterations: 0,
|
|
854
|
+
finishReason: "aborted",
|
|
855
|
+
model: {
|
|
856
|
+
id: modelInfo.id,
|
|
857
|
+
provider: this.config.providerId,
|
|
858
|
+
info: modelInfo.info,
|
|
859
|
+
},
|
|
860
|
+
startedAt,
|
|
861
|
+
endedAt,
|
|
862
|
+
durationMs: endedAt.getTime() - startedAt.getTime(),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private emit(event: AgentEvent): void {
|
|
867
|
+
this.runtimeBus.emitRuntimeEvent(event);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private reportRecoverableError(error: unknown): void {
|
|
871
|
+
this.log("warn", "Recoverable agent error", {
|
|
872
|
+
agentId: this.agentId,
|
|
873
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
874
|
+
runId: this.activeRunId || this.conversationStore.getConversationId(),
|
|
875
|
+
error,
|
|
876
|
+
});
|
|
877
|
+
this.emit({
|
|
878
|
+
type: "error",
|
|
879
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
880
|
+
recoverable: this.config.hookErrorMode !== "throw",
|
|
881
|
+
iteration: 0,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private resolveToolPolicy(toolName: string): ToolPolicy {
|
|
886
|
+
const globalPolicy = this.config.toolPolicies?.["*"] ?? {};
|
|
887
|
+
const toolPolicy = this.config.toolPolicies?.[toolName] ?? {};
|
|
888
|
+
return {
|
|
889
|
+
...globalPolicy,
|
|
890
|
+
...toolPolicy,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private async requestToolApproval(
|
|
895
|
+
toolName: string,
|
|
896
|
+
toolCallId: string,
|
|
897
|
+
input: unknown,
|
|
898
|
+
context: ToolContext,
|
|
899
|
+
policy: ToolPolicy,
|
|
900
|
+
): Promise<ToolApprovalResult> {
|
|
901
|
+
const callback = this.config.requestToolApproval;
|
|
902
|
+
if (!callback) {
|
|
903
|
+
return {
|
|
904
|
+
approved: false,
|
|
905
|
+
reason: `Tool "${toolName}" requires approval but no approval handler is configured`,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const result = await callback({
|
|
910
|
+
agentId: this.agentId,
|
|
911
|
+
conversationId: this.conversationStore.getConversationId(),
|
|
912
|
+
iteration: context.iteration,
|
|
913
|
+
toolCallId,
|
|
914
|
+
toolName,
|
|
915
|
+
input,
|
|
916
|
+
policy,
|
|
917
|
+
});
|
|
918
|
+
return result;
|
|
919
|
+
} catch (error) {
|
|
920
|
+
return {
|
|
921
|
+
approved: false,
|
|
922
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private async authorizeToolCall(
|
|
928
|
+
call: PendingToolCall,
|
|
929
|
+
context: ToolContext,
|
|
930
|
+
): Promise<{ allowed: true } | { allowed: false; reason: string }> {
|
|
931
|
+
const policy = this.resolveToolPolicy(call.name);
|
|
932
|
+
const enabled = policy.enabled !== false;
|
|
933
|
+
if (!enabled) {
|
|
934
|
+
return {
|
|
935
|
+
allowed: false,
|
|
936
|
+
reason: `Tool "${call.name}" is disabled by policy`,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const autoApprove = policy.autoApprove !== false && call.review !== true;
|
|
941
|
+
if (autoApprove) {
|
|
942
|
+
return { allowed: true };
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const approval = await this.requestToolApproval(
|
|
946
|
+
call.name,
|
|
947
|
+
call.id,
|
|
948
|
+
call.input,
|
|
949
|
+
context,
|
|
950
|
+
call.review === true ? { ...policy, autoApprove: false } : policy,
|
|
951
|
+
);
|
|
952
|
+
if (!approval.approved) {
|
|
953
|
+
return {
|
|
954
|
+
allowed: false,
|
|
955
|
+
reason:
|
|
956
|
+
approval.reason?.trim() || `Tool "${call.name}" was not approved`,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
return { allowed: true };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private appendHookContext(source: string, context: string): void {
|
|
963
|
+
const trimmed = context.trim();
|
|
964
|
+
if (!trimmed) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const text = trimmed.startsWith("<hook_context")
|
|
969
|
+
? trimmed
|
|
970
|
+
: `<hook_context source="${source}">\n${trimmed}\n</hook_context>`;
|
|
971
|
+
|
|
972
|
+
this.conversationStore.appendMessage({
|
|
973
|
+
role: "user",
|
|
974
|
+
content: [
|
|
975
|
+
{
|
|
976
|
+
type: "text",
|
|
977
|
+
text,
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private log(
|
|
984
|
+
level: "debug" | "info" | "warn" | "error",
|
|
985
|
+
message: string,
|
|
986
|
+
metadata?: Record<string, unknown>,
|
|
987
|
+
): void {
|
|
988
|
+
const sink = this.logger?.[level];
|
|
989
|
+
if (!sink) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
if (level === "error") {
|
|
994
|
+
const errorMeta =
|
|
995
|
+
metadata?.error instanceof Error
|
|
996
|
+
? {
|
|
997
|
+
...metadata,
|
|
998
|
+
error: {
|
|
999
|
+
name: metadata.error.name,
|
|
1000
|
+
message: metadata.error.message,
|
|
1001
|
+
stack: metadata.error.stack,
|
|
1002
|
+
},
|
|
1003
|
+
}
|
|
1004
|
+
: metadata;
|
|
1005
|
+
sink(message, errorMeta);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
sink(message, metadata);
|
|
1009
|
+
} catch {
|
|
1010
|
+
// Logging failures must never break agent execution.
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private mergeAbortSignals(
|
|
1015
|
+
...signals: (AbortSignal | undefined)[]
|
|
1016
|
+
): AbortSignal {
|
|
1017
|
+
const activeSignals = signals.filter(
|
|
1018
|
+
(signal): signal is AbortSignal => !!signal,
|
|
1019
|
+
);
|
|
1020
|
+
if (activeSignals.length === 0) {
|
|
1021
|
+
return new AbortController().signal;
|
|
1022
|
+
}
|
|
1023
|
+
if (activeSignals.length === 1) {
|
|
1024
|
+
return activeSignals[0];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const abortSignalCtor = AbortSignal as unknown as {
|
|
1028
|
+
any?: (signals: AbortSignal[]) => AbortSignal;
|
|
1029
|
+
};
|
|
1030
|
+
if (abortSignalCtor.any) {
|
|
1031
|
+
return abortSignalCtor.any(activeSignals);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const controller = new AbortController();
|
|
1035
|
+
for (const signal of activeSignals) {
|
|
1036
|
+
if (signal.aborted) {
|
|
1037
|
+
controller.abort();
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
signal.addEventListener("abort", () => controller.abort(), {
|
|
1041
|
+
once: true,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return controller.signal;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export function createAgent(config: AgentConfig): Agent {
|
|
1049
|
+
return new Agent(config);
|
|
1050
|
+
}
|