goatchain 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4795 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import OpenAI from "openai";
5
+ import { spawn } from "node:child_process";
6
+ import process from "node:process";
7
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
8
+
9
+ //#region src/agent/checkpointMiddleware.ts
10
+ /**
11
+ * Convert AgentLoopState to AgentLoopCheckpoint
12
+ */
13
+ function toLoopCheckpoint(state, options) {
14
+ let phase = options?.phase;
15
+ if (!phase) if (!state.shouldContinue) phase = "completed";
16
+ else if (state.pendingToolCalls.length > 0) phase = "tool_execution";
17
+ else phase = "llm_call";
18
+ let status = options?.status;
19
+ if (!status) if (phase === "completed") status = `Completed: ${state.stopReason ?? "unknown"}`;
20
+ else if (phase === "tool_execution") status = `Executing tools: ${state.pendingToolCalls.map((tc) => tc.toolCall.function.name).join(", ")}`;
21
+ else if (phase === "approval_pending") status = "Waiting for user approval";
22
+ else status = `Iteration ${state.iteration}: Calling LLM`;
23
+ return {
24
+ sessionId: state.sessionId,
25
+ agentId: state.agentId,
26
+ agentName: options?.agentName,
27
+ iteration: state.iteration,
28
+ phase,
29
+ status,
30
+ modelConfig: options?.modelConfig,
31
+ requestParams: options?.requestParams,
32
+ messages: [...state.messages],
33
+ pendingToolCalls: state.pendingToolCalls.map((tc) => ({
34
+ toolCall: { ...tc.toolCall },
35
+ result: tc.result,
36
+ isError: tc.isError
37
+ })),
38
+ currentResponse: state.currentResponse,
39
+ currentThinking: state.currentThinking,
40
+ shouldContinue: state.shouldContinue,
41
+ stopReason: state.stopReason,
42
+ lastModelStopReason: state.lastModelStopReason,
43
+ usage: { ...state.usage },
44
+ metadata: { ...state.metadata },
45
+ savedAt: Date.now()
46
+ };
47
+ }
48
+ /**
49
+ * Restore AgentLoopState from AgentLoopCheckpoint
50
+ *
51
+ * Note: agentName, phase, and status are display-only fields and are not
52
+ * part of AgentLoopState. They will be recalculated when a new checkpoint is saved.
53
+ */
54
+ function fromLoopCheckpoint(checkpoint) {
55
+ return {
56
+ sessionId: checkpoint.sessionId,
57
+ agentId: checkpoint.agentId,
58
+ iteration: checkpoint.iteration,
59
+ messages: [...checkpoint.messages],
60
+ pendingToolCalls: checkpoint.pendingToolCalls.map((tc) => ({
61
+ toolCall: { ...tc.toolCall },
62
+ result: tc.result,
63
+ isError: tc.isError
64
+ })),
65
+ currentResponse: checkpoint.currentResponse,
66
+ currentThinking: checkpoint.currentThinking,
67
+ shouldContinue: checkpoint.shouldContinue,
68
+ stopReason: checkpoint.stopReason,
69
+ lastModelStopReason: checkpoint.lastModelStopReason,
70
+ usage: { ...checkpoint.usage },
71
+ metadata: { ...checkpoint.metadata }
72
+ };
73
+ }
74
+
75
+ //#endregion
76
+ //#region src/agent/errors.ts
77
+ /**
78
+ * Error thrown when agent execution is aborted via AbortSignal
79
+ */
80
+ var AgentAbortError = class extends Error {
81
+ constructor(message = "Agent execution aborted") {
82
+ super(message);
83
+ this.name = "AgentAbortError";
84
+ }
85
+ };
86
+ /**
87
+ * Error thrown when agent exceeds maximum iterations
88
+ */
89
+ var AgentMaxIterationsError = class extends Error {
90
+ iterations;
91
+ constructor(iterations, message) {
92
+ super(message ?? `Agent exceeded maximum iterations (${iterations})`);
93
+ this.name = "AgentMaxIterationsError";
94
+ this.iterations = iterations;
95
+ }
96
+ };
97
+ /**
98
+ * Internal control-flow error used to stop streaming when execution is paused.
99
+ *
100
+ * This is caught by the Agent and is not intended to be shown to end users.
101
+ */
102
+ var AgentPauseError = class extends Error {
103
+ constructor(message = "Agent execution paused") {
104
+ super(message);
105
+ this.name = "AgentPauseError";
106
+ }
107
+ };
108
+ /**
109
+ * Check if the abort signal has been triggered and throw if so.
110
+ *
111
+ * @param signal - AbortSignal to check
112
+ * @param context - Optional context for error message
113
+ * @throws AgentAbortError if signal is aborted
114
+ */
115
+ function ensureNotAborted(signal, context) {
116
+ if (!signal?.aborted) return;
117
+ const reason = signal?.reason;
118
+ if (reason instanceof Error) throw reason;
119
+ throw new AgentAbortError(typeof reason === "string" ? reason : context ? `${context} aborted` : "Agent execution aborted");
120
+ }
121
+
122
+ //#endregion
123
+ //#region src/agent/middleware.ts
124
+ /**
125
+ * Compose multiple middleware functions into a single function.
126
+ *
127
+ * This implements an immutable onion model where each middleware:
128
+ * - Receives state from the previous middleware (or initial state)
129
+ * - Can transform state before passing to next()
130
+ * - Returns the result from downstream processing
131
+ *
132
+ * Execution order for each iteration:
133
+ * ```
134
+ * outer:before → inner:before → exec (model.stream) → inner:after → outer:after
135
+ * ```
136
+ *
137
+ * State flow:
138
+ * ```
139
+ * initialState → outer(transform?) → inner(transform?) → exec → result
140
+ * ```
141
+ *
142
+ * @param middleware - Array of middleware functions
143
+ * @returns Composed middleware function that can be invoked per iteration
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const composed = compose([
148
+ * async (state, next) => {
149
+ * console.log('outer:before');
150
+ * const result = await next(state);
151
+ * console.log('outer:after');
152
+ * return result;
153
+ * },
154
+ * async (state, next) => {
155
+ * console.log('inner:before');
156
+ * // Transform state before passing down
157
+ * const modified = { ...state, messages: compress(state.messages) };
158
+ * const result = await next(modified);
159
+ * console.log('inner:after');
160
+ * return result;
161
+ * },
162
+ * ]);
163
+ *
164
+ * // Execute with core function
165
+ * const result = await composed(initialState, async (s) => {
166
+ * // Core execution receives transformed state
167
+ * await model.stream(s.messages);
168
+ * return s;
169
+ * });
170
+ * ```
171
+ */
172
+ function compose(middleware) {
173
+ return (initialState, exec) => {
174
+ let index = -1;
175
+ const dispatch = async (i, currentState) => {
176
+ if (i <= index) throw new Error("next() called multiple times");
177
+ index = i;
178
+ if (i === middleware.length) return exec ? await exec(currentState) : currentState;
179
+ const fn = middleware[i];
180
+ return fn(currentState, (nextState) => dispatch(i + 1, nextState));
181
+ };
182
+ return dispatch(0, initialState);
183
+ };
184
+ }
185
+
186
+ //#endregion
187
+ //#region src/agent/types.ts
188
+ /**
189
+ * Create initial AgentLoopState from AgentInput
190
+ */
191
+ function createInitialLoopState(input, agentId, systemPrompt) {
192
+ const messages = [
193
+ {
194
+ role: "system",
195
+ content: systemPrompt
196
+ },
197
+ ...input.messages ?? [],
198
+ {
199
+ role: "user",
200
+ content: input.input
201
+ }
202
+ ];
203
+ return {
204
+ sessionId: input.sessionId,
205
+ agentId,
206
+ messages,
207
+ iteration: 0,
208
+ pendingToolCalls: [],
209
+ currentResponse: "",
210
+ shouldContinue: true,
211
+ usage: {
212
+ promptTokens: 0,
213
+ completionTokens: 0,
214
+ totalTokens: 0
215
+ },
216
+ metadata: {}
217
+ };
218
+ }
219
+ /**
220
+ * Create initial AgentLoopState from an existing message history (no new user message appended).
221
+ */
222
+ function createInitialLoopStateFromMessages(input, agentId, systemPrompt) {
223
+ const normalized = Array.isArray(input.messages) ? input.messages : [];
224
+ const messages = [{
225
+ role: "system",
226
+ content: systemPrompt
227
+ }, ...normalized.length > 0 && normalized[0]?.role === "system" ? normalized.slice(1) : normalized];
228
+ return {
229
+ sessionId: input.sessionId,
230
+ agentId,
231
+ messages,
232
+ iteration: 0,
233
+ pendingToolCalls: [],
234
+ currentResponse: "",
235
+ shouldContinue: true,
236
+ usage: {
237
+ promptTokens: 0,
238
+ completionTokens: 0,
239
+ totalTokens: 0
240
+ },
241
+ metadata: {}
242
+ };
243
+ }
244
+
245
+ //#endregion
246
+ //#region src/agent/agent.ts
247
+ /** Default maximum iterations to prevent infinite loops */
248
+ const DEFAULT_MAX_ITERATIONS = 10;
249
+ /**
250
+ * Agent class - the main orchestrator.
251
+ *
252
+ * Agent is a blueprint/configuration that can handle multiple sessions.
253
+ * It composes model, tools, state store, and session manager.
254
+ *
255
+ * Supports middleware pattern for extensible hooks at each loop iteration.
256
+ */
257
+ var Agent = class Agent {
258
+ id;
259
+ name;
260
+ systemPrompt;
261
+ createdAt;
262
+ _model;
263
+ _tools;
264
+ _stateStore;
265
+ _sessionManager;
266
+ _metadata;
267
+ _middlewares = [];
268
+ _stats = {
269
+ updatedAt: Date.now(),
270
+ totalSessions: 0,
271
+ activeSessions: 0,
272
+ totalUsage: {
273
+ promptTokens: 0,
274
+ completionTokens: 0,
275
+ totalTokens: 0
276
+ }
277
+ };
278
+ constructor(options) {
279
+ this.id = options.id ?? crypto.randomUUID();
280
+ this.name = options.name;
281
+ this.systemPrompt = options.systemPrompt;
282
+ this.createdAt = Date.now();
283
+ this._model = options.model;
284
+ this._tools = options.tools;
285
+ this._stateStore = options.stateStore;
286
+ this._sessionManager = options.sessionManager;
287
+ }
288
+ /**
289
+ * Get the current model
290
+ */
291
+ get model() {
292
+ return this._model;
293
+ }
294
+ /**
295
+ * Get the tool registry
296
+ */
297
+ get tools() {
298
+ return this._tools;
299
+ }
300
+ /**
301
+ * Get the state store
302
+ */
303
+ get stateStore() {
304
+ return this._stateStore;
305
+ }
306
+ /**
307
+ * Get the session manager
308
+ */
309
+ get sessionManager() {
310
+ return this._sessionManager;
311
+ }
312
+ /**
313
+ * Get runtime statistics
314
+ */
315
+ get stats() {
316
+ return { ...this._stats };
317
+ }
318
+ /**
319
+ * Get metadata
320
+ */
321
+ get metadata() {
322
+ return this._metadata;
323
+ }
324
+ /**
325
+ * Set metadata
326
+ */
327
+ set metadata(value) {
328
+ this._metadata = value;
329
+ }
330
+ /**
331
+ * Add middleware to the agent.
332
+ *
333
+ * Middleware runs at each loop iteration and can intercept/transform the loop state.
334
+ * Middleware follows an immutable pattern - it receives state and returns new state via next().
335
+ *
336
+ * @param fn - Middleware function
337
+ * @returns this for chaining
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * // Logging middleware (pass-through)
342
+ * agent.use(async (state, next) => {
343
+ * console.log('Before iteration', state.iteration);
344
+ * const result = await next(state);
345
+ * console.log('After iteration', state.iteration);
346
+ * return result;
347
+ * });
348
+ *
349
+ * // Transforming middleware (e.g., compression)
350
+ * agent.use(async (state, next) => {
351
+ * const compressed = { ...state, messages: compress(state.messages) };
352
+ * return next(compressed);
353
+ * });
354
+ * ```
355
+ */
356
+ use(fn) {
357
+ this._middlewares.push(fn);
358
+ return this;
359
+ }
360
+ /**
361
+ * Switch to a different model
362
+ *
363
+ * @param model - New model to use
364
+ */
365
+ setModel(model) {
366
+ this._model = model;
367
+ this._stats.updatedAt = Date.now();
368
+ }
369
+ /**
370
+ * Update session counts
371
+ *
372
+ * @param total - Total sessions
373
+ * @param active - Active sessions
374
+ */
375
+ updateSessionCounts(total, active) {
376
+ this._stats.totalSessions = total;
377
+ this._stats.activeSessions = active;
378
+ this._stats.updatedAt = Date.now();
379
+ }
380
+ /**
381
+ * Add to total usage statistics
382
+ *
383
+ * @param usage - Usage to add
384
+ * @param usage.promptTokens - Number of prompt tokens
385
+ * @param usage.completionTokens - Number of completion tokens
386
+ * @param usage.totalTokens - Total number of tokens
387
+ */
388
+ addUsage(usage) {
389
+ this._stats.totalUsage.promptTokens += usage.promptTokens;
390
+ this._stats.totalUsage.completionTokens += usage.completionTokens;
391
+ this._stats.totalUsage.totalTokens += usage.totalTokens;
392
+ this._stats.updatedAt = Date.now();
393
+ }
394
+ /**
395
+ * Create a snapshot of the agent for persistence
396
+ *
397
+ * @returns Agent snapshot
398
+ */
399
+ toSnapshot() {
400
+ return {
401
+ id: this.id,
402
+ name: this.name,
403
+ createdAt: this.createdAt,
404
+ config: {
405
+ systemPrompt: this.systemPrompt,
406
+ model: { modelId: this._model.modelId },
407
+ tools: this._tools?.list().map((t) => t.name) ?? []
408
+ },
409
+ stats: { ...this._stats },
410
+ metadata: this._metadata
411
+ };
412
+ }
413
+ /**
414
+ * Restore agent state from a snapshot
415
+ *
416
+ * Note: This restores mutable state (stats, metadata) from a snapshot.
417
+ * The model and tools must be provided separately as they contain runtime instances.
418
+ *
419
+ * @param snapshot - Agent snapshot to restore from
420
+ */
421
+ restoreFromSnapshot(snapshot) {
422
+ this._stats = {
423
+ updatedAt: snapshot.stats.updatedAt,
424
+ totalSessions: snapshot.stats.totalSessions,
425
+ activeSessions: snapshot.stats.activeSessions,
426
+ totalUsage: { ...snapshot.stats.totalUsage }
427
+ };
428
+ this._metadata = snapshot.metadata;
429
+ }
430
+ /**
431
+ * Execute model stream and collect events.
432
+ * This is the core execution that middleware wraps around.
433
+ */
434
+ async executeModelStream(state, signal, tools, modelOverride) {
435
+ const events = [];
436
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
437
+ const ensureToolCall = (callId) => {
438
+ const existing = toolCallAccumulator.get(callId);
439
+ if (existing) return existing;
440
+ const created = {
441
+ argsText: "",
442
+ started: false
443
+ };
444
+ toolCallAccumulator.set(callId, created);
445
+ return created;
446
+ };
447
+ const emitToolCallStartIfNeeded = (callId, toolName) => {
448
+ const entry = ensureToolCall(callId);
449
+ if (toolName && !entry.toolName) entry.toolName = toolName;
450
+ if (!entry.started) {
451
+ entry.started = true;
452
+ events.push({
453
+ type: "tool_call_start",
454
+ callId,
455
+ toolName: entry.toolName
456
+ });
457
+ }
458
+ };
459
+ const emitToolCallDelta = (callId, toolName, argsTextDelta) => {
460
+ const entry = ensureToolCall(callId);
461
+ if (toolName && !entry.toolName) entry.toolName = toolName;
462
+ if (typeof argsTextDelta === "string" && argsTextDelta.length > 0) entry.argsText += argsTextDelta;
463
+ emitToolCallStartIfNeeded(callId, entry.toolName);
464
+ events.push({
465
+ type: "tool_call_delta",
466
+ callId,
467
+ toolName: entry.toolName,
468
+ argsTextDelta
469
+ });
470
+ };
471
+ const finalizeToolCalls = () => {
472
+ for (const [callId, entry] of toolCallAccumulator) {
473
+ if (!entry.toolName) continue;
474
+ const toolCall = {
475
+ id: callId,
476
+ type: "function",
477
+ function: {
478
+ name: entry.toolName,
479
+ arguments: entry.argsText
480
+ }
481
+ };
482
+ if (this._tools) state.pendingToolCalls.push({ toolCall });
483
+ events.push({
484
+ type: "tool_call_end",
485
+ toolCall
486
+ });
487
+ }
488
+ toolCallAccumulator.clear();
489
+ };
490
+ const streamArgs = modelOverride ? {
491
+ model: modelOverride,
492
+ messages: state.messages,
493
+ tools
494
+ } : {
495
+ messages: state.messages,
496
+ tools
497
+ };
498
+ for await (const event of this._model.stream(streamArgs)) {
499
+ ensureNotAborted(signal, "Agent streaming");
500
+ if (event.type === "delta") {
501
+ if (event.chunk.kind === "text") {
502
+ state.currentResponse += event.chunk.text;
503
+ events.push({
504
+ type: "text_delta",
505
+ delta: event.chunk.text
506
+ });
507
+ } else if (event.chunk.kind === "thinking_start") events.push({ type: "thinking_start" });
508
+ else if (event.chunk.kind === "thinking_delta") {
509
+ state.currentThinking = (state.currentThinking ?? "") + event.chunk.text;
510
+ events.push({
511
+ type: "thinking_delta",
512
+ delta: event.chunk.text
513
+ });
514
+ } else if (event.chunk.kind === "thinking_end") events.push({ type: "thinking_end" });
515
+ else if (event.chunk.kind === "tool_call_delta") emitToolCallDelta(event.chunk.callId, event.chunk.toolId, event.chunk.argsTextDelta);
516
+ } else if (event.type === "response_end") {
517
+ finalizeToolCalls();
518
+ state.lastModelStopReason = event.stopReason;
519
+ const raw = event.usage;
520
+ if (raw && typeof raw === "object") {
521
+ const usage = raw;
522
+ if (usage.prompt_tokens || usage.completion_tokens || usage.total_tokens) {
523
+ const normalized = {
524
+ promptTokens: usage.prompt_tokens ?? 0,
525
+ completionTokens: usage.completion_tokens ?? 0,
526
+ totalTokens: usage.total_tokens ?? 0
527
+ };
528
+ state.usage.promptTokens += normalized.promptTokens;
529
+ state.usage.completionTokens += normalized.completionTokens;
530
+ state.usage.totalTokens += normalized.totalTokens;
531
+ this.addUsage(normalized);
532
+ events.push({
533
+ type: "usage",
534
+ usage: normalized
535
+ });
536
+ }
537
+ }
538
+ } else if (event.type === "error") {
539
+ const code = event.error?.code ?? "model_error";
540
+ const message = event.error?.message ?? "Model error";
541
+ const err = /* @__PURE__ */ new Error(`${code}: ${message}`);
542
+ err.code = code;
543
+ state.error = err;
544
+ state.shouldContinue = false;
545
+ state.stopReason = "error";
546
+ events.push({
547
+ type: "error",
548
+ error: err
549
+ });
550
+ } else if (event.type === "text_delta") {
551
+ state.currentResponse += event.delta;
552
+ events.push({
553
+ type: "text_delta",
554
+ delta: event.delta
555
+ });
556
+ } else if (event.type === "thinking_end" || event.type === "thinking_start") events.push({ type: event.type });
557
+ else if (event.type === "thinking_delta") {
558
+ state.currentThinking = (state.currentThinking ?? "") + event.content;
559
+ events.push({
560
+ type: "thinking_delta",
561
+ delta: event.content
562
+ });
563
+ } else if (event.type === "tool_call" && this._tools) {
564
+ const toolCall = event.toolCall;
565
+ events.push({
566
+ type: "tool_call_start",
567
+ callId: toolCall.id,
568
+ toolName: toolCall.function.name
569
+ });
570
+ state.pendingToolCalls.push({ toolCall });
571
+ events.push({
572
+ type: "tool_call_end",
573
+ toolCall
574
+ });
575
+ } else if (event.type === "usage") {
576
+ state.usage.promptTokens += event.usage.promptTokens;
577
+ state.usage.completionTokens += event.usage.completionTokens;
578
+ state.usage.totalTokens += event.usage.totalTokens;
579
+ this.addUsage(event.usage);
580
+ events.push({
581
+ type: "usage",
582
+ usage: event.usage
583
+ });
584
+ }
585
+ }
586
+ if (toolCallAccumulator.size > 0) finalizeToolCalls();
587
+ return events;
588
+ }
589
+ /**
590
+ * Execute a single tool call and return the result event.
591
+ */
592
+ async executeToolCall(tc, ctx, signal) {
593
+ ensureNotAborted(signal, `Tool execution: ${tc.toolCall.function.name}`);
594
+ const tool = this._tools?.get(tc.toolCall.function.name);
595
+ if (!tool) {
596
+ tc.result = `Tool not found: ${tc.toolCall.function.name}`;
597
+ tc.isError = true;
598
+ return {
599
+ type: "tool_result",
600
+ tool_call_id: tc.toolCall.id,
601
+ result: tc.result,
602
+ isError: true
603
+ };
604
+ }
605
+ try {
606
+ const args = typeof tc.toolCall.function.arguments === "string" ? JSON.parse(tc.toolCall.function.arguments) : tc.toolCall.function.arguments;
607
+ tc.result = await tool.execute(args, ctx);
608
+ tc.isError = false;
609
+ return {
610
+ type: "tool_result",
611
+ tool_call_id: tc.toolCall.id,
612
+ result: tc.result
613
+ };
614
+ } catch (error) {
615
+ tc.result = error instanceof Error ? error.message : String(error);
616
+ tc.isError = true;
617
+ return {
618
+ type: "tool_result",
619
+ tool_call_id: tc.toolCall.id,
620
+ result: tc.result,
621
+ isError: true
622
+ };
623
+ }
624
+ }
625
+ createToolExecutionContext(state, signal, input) {
626
+ return {
627
+ sessionId: state.sessionId,
628
+ agentId: state.agentId,
629
+ context: input?.context,
630
+ signal,
631
+ capabilities: input?.capabilities,
632
+ usage: state.usage,
633
+ metadata: {
634
+ ...state.metadata ?? {},
635
+ ...input?.metadata ?? {}
636
+ }
637
+ };
638
+ }
639
+ /**
640
+ * Merge execution results from processedState back to original state.
641
+ *
642
+ * This preserves original messages (for checkpoint) while keeping:
643
+ * - currentResponse, currentThinking (from LLM)
644
+ * - pendingToolCalls (from LLM)
645
+ * - usage (accumulated)
646
+ * - metadata (middleware may update this)
647
+ *
648
+ * Note: If processedState shares references with state (shallow copy),
649
+ * some fields are already synced. This method ensures all fields are merged.
650
+ */
651
+ mergeStateResults(state, processedState) {
652
+ state.currentResponse = processedState.currentResponse;
653
+ state.currentThinking = processedState.currentThinking;
654
+ state.pendingToolCalls = processedState.pendingToolCalls;
655
+ state.usage = processedState.usage;
656
+ state.metadata = processedState.metadata;
657
+ }
658
+ /**
659
+ * Add tool result to message history.
660
+ */
661
+ addToolResultToHistory(state, tc) {
662
+ state.messages.push({
663
+ role: "tool",
664
+ tool_call_id: tc.toolCall.id,
665
+ content: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result)
666
+ });
667
+ }
668
+ /**
669
+ * Add assistant message with tool calls to history.
670
+ */
671
+ addAssistantMessageWithToolCalls(state) {
672
+ state.messages.push({
673
+ role: "assistant",
674
+ content: state.currentResponse || "",
675
+ tool_calls: state.pendingToolCalls.map((tc) => tc.toolCall)
676
+ });
677
+ }
678
+ /**
679
+ * Add final assistant response to history.
680
+ */
681
+ addFinalAssistantMessage(state) {
682
+ if (state.currentResponse) state.messages.push({
683
+ role: "assistant",
684
+ content: state.currentResponse
685
+ });
686
+ }
687
+ /**
688
+ * Stream agent execution with proper agentic loop.
689
+ *
690
+ * The execution follows an immutable middleware pattern with checkpoint support:
691
+ * ```
692
+ * checkpoint:before (save original state)
693
+ * → middleware chain (transforms state immutably)
694
+ * → exec (model.stream with processed state)
695
+ * → merge results back (except messages)
696
+ * checkpoint:after (save original state with original messages)
697
+ * ```
698
+ *
699
+ * Key invariant: checkpoint always saves the original messages, while
700
+ * LLM receives potentially transformed messages (e.g., compressed).
701
+ */
702
+ async *streamWithState(state, input) {
703
+ const maxIterations = input.maxIterations ?? DEFAULT_MAX_ITERATIONS;
704
+ const signal = input.signal;
705
+ const tools = this._tools?.toOpenAIFormat();
706
+ const runUserMiddleware = compose(this._middlewares);
707
+ const toolContext = this.createToolExecutionContext(state, signal, input.toolContext);
708
+ const stateStore = this._stateStore;
709
+ const savePoint = stateStore?.savePoint ?? "before";
710
+ const deleteOnComplete = stateStore?.deleteOnComplete ?? true;
711
+ const checkpointModelConfig = input.model ? {
712
+ modelId: input.model.modelId,
713
+ provider: input.model.provider
714
+ } : { modelId: this._model.modelId };
715
+ while (state.shouldContinue) {
716
+ ensureNotAborted(signal, `Agent iteration ${state.iteration}`);
717
+ if (state.iteration >= maxIterations) {
718
+ state.shouldContinue = false;
719
+ state.stopReason = "max_iterations";
720
+ yield {
721
+ type: "done",
722
+ finalResponse: state.currentResponse,
723
+ stopReason: "max_iterations",
724
+ modelStopReason: state.lastModelStopReason
725
+ };
726
+ break;
727
+ }
728
+ yield {
729
+ type: "iteration_start",
730
+ iteration: state.iteration
731
+ };
732
+ if (state.pendingToolCalls.length > 0) {
733
+ try {
734
+ yield* this.handleToolCalls(state, toolContext, {
735
+ signal,
736
+ toolContextInput: input.toolContext,
737
+ stateStore,
738
+ checkpointModelConfig,
739
+ requestParams: input.requestParams
740
+ });
741
+ } catch (error) {
742
+ if (error instanceof AgentPauseError) return;
743
+ throw error;
744
+ }
745
+ continue;
746
+ }
747
+ state.currentResponse = "";
748
+ state.currentThinking = void 0;
749
+ state.pendingToolCalls = [];
750
+ state.lastModelStopReason = void 0;
751
+ if (stateStore && (savePoint === "before" || savePoint === "both")) {
752
+ const checkpoint = toLoopCheckpoint(state, {
753
+ agentName: this.name,
754
+ modelConfig: checkpointModelConfig,
755
+ requestParams: input.requestParams
756
+ });
757
+ await stateStore.saveCheckpoint(checkpoint);
758
+ }
759
+ let collectedEvents = [];
760
+ const processedState = await runUserMiddleware(state, async (middlewareState) => {
761
+ ensureNotAborted(signal, "Agent model call");
762
+ collectedEvents = await this.executeModelStream(middlewareState, signal, tools, input.model);
763
+ return middlewareState;
764
+ });
765
+ this.mergeStateResults(state, processedState);
766
+ if (stateStore && (savePoint === "after" || savePoint === "both")) {
767
+ const checkpoint = toLoopCheckpoint(state, {
768
+ agentName: this.name,
769
+ modelConfig: checkpointModelConfig,
770
+ requestParams: input.requestParams
771
+ });
772
+ await stateStore.saveCheckpoint(checkpoint);
773
+ }
774
+ for (const event of collectedEvents) yield event;
775
+ ensureNotAborted(signal, `Agent iteration ${state.iteration}`);
776
+ if (state.stopReason === "error") {
777
+ yield {
778
+ type: "iteration_end",
779
+ iteration: state.iteration,
780
+ willContinue: false,
781
+ toolCallCount: state.pendingToolCalls.length
782
+ };
783
+ yield {
784
+ type: "done",
785
+ finalResponse: state.currentResponse,
786
+ stopReason: "error",
787
+ modelStopReason: state.lastModelStopReason
788
+ };
789
+ break;
790
+ }
791
+ if (state.pendingToolCalls.length > 0) try {
792
+ yield* this.handleToolCalls(state, toolContext, {
793
+ signal,
794
+ toolContextInput: input.toolContext,
795
+ stateStore,
796
+ checkpointModelConfig,
797
+ requestParams: input.requestParams
798
+ });
799
+ } catch (error) {
800
+ if (error instanceof AgentPauseError) return;
801
+ throw error;
802
+ }
803
+ else {
804
+ yield* this.handleFinalResponse(state);
805
+ if (stateStore && deleteOnComplete && state.stopReason === "final_response") await stateStore.deleteCheckpoint(state.sessionId);
806
+ }
807
+ }
808
+ }
809
+ async *stream(input) {
810
+ const state = createInitialLoopState(input, this.id, this.systemPrompt);
811
+ yield* this.streamWithState(state, {
812
+ maxIterations: input.maxIterations,
813
+ signal: input.signal,
814
+ model: input.model,
815
+ toolContext: input.toolContext,
816
+ requestParams: input.requestParams
817
+ });
818
+ }
819
+ /**
820
+ * Stream agent execution starting from an existing message history (no new user message appended).
821
+ *
822
+ * Useful for orchestration patterns like "handoff", where a different agent should
823
+ * continue the same conversation state under a different system prompt.
824
+ */
825
+ async *streamFromMessages(input) {
826
+ const state = createInitialLoopStateFromMessages(input, this.id, this.systemPrompt);
827
+ yield* this.streamWithState(state, {
828
+ maxIterations: input.maxIterations,
829
+ signal: input.signal,
830
+ model: input.model,
831
+ toolContext: input.toolContext,
832
+ requestParams: input.requestParams
833
+ });
834
+ }
835
+ /**
836
+ * Stream agent execution from a saved checkpoint.
837
+ *
838
+ * This allows resuming interrupted executions from the exact state
839
+ * where they were saved (e.g., during tool approval waiting).
840
+ *
841
+ * @param input - Input containing the checkpoint to resume from
842
+ *
843
+ * @example
844
+ * ```typescript
845
+ * // Load checkpoint from store
846
+ * const checkpoint = await store.loadLoopCheckpoint(sessionId)
847
+ *
848
+ * if (checkpoint) {
849
+ * // Resume execution from checkpoint
850
+ * for await (const event of agent.streamFromCheckpoint({
851
+ * checkpoint,
852
+ * maxIterations: 10,
853
+ * })) {
854
+ * console.log(event)
855
+ * }
856
+ * }
857
+ * ```
858
+ */
859
+ async *streamFromCheckpoint(input) {
860
+ const state = fromLoopCheckpoint(input.checkpoint);
861
+ const requestParams = input.requestParams ?? input.checkpoint.requestParams;
862
+ yield* this.streamWithState(state, {
863
+ maxIterations: input.maxIterations,
864
+ signal: input.signal,
865
+ model: input.model,
866
+ toolContext: input.toolContext,
867
+ requestParams
868
+ });
869
+ }
870
+ /**
871
+ * Risk levels that require user approval before execution.
872
+ */
873
+ static APPROVAL_REQUIRED_LEVELS = new Set(["high", "critical"]);
874
+ /**
875
+ * Check if a tool requires approval based on its risk level.
876
+ */
877
+ requiresApproval(riskLevel) {
878
+ return Agent.APPROVAL_REQUIRED_LEVELS.has(riskLevel);
879
+ }
880
+ /**
881
+ * Handle tool calls: execute tools, yield results, continue loop.
882
+ */
883
+ async *handleToolCalls(state, toolContext, options) {
884
+ const toolCallCount = state.pendingToolCalls.length;
885
+ const signal = options?.signal;
886
+ const toolContextInput = options?.toolContextInput;
887
+ const onToolApproval = toolContextInput?.onToolApproval;
888
+ const approval = toolContextInput?.approval;
889
+ const approvalStrategy = approval?.strategy ?? "high_risk";
890
+ const approvalEnabled = approval?.autoApprove !== true && (Boolean(onToolApproval) || approval?.strategy != null || approval?.decisions != null);
891
+ const approvalDecisions = approval?.decisions ?? {};
892
+ const shouldRequireApproval = (riskLevel) => {
893
+ if (!approvalEnabled) return false;
894
+ if (approvalStrategy === "all") return true;
895
+ return this.requiresApproval(riskLevel);
896
+ };
897
+ const hasAssistantToolCallMessage = (toolCallId) => {
898
+ return state.messages.some((m) => {
899
+ if (!m || typeof m !== "object" || m.role !== "assistant") return false;
900
+ const tc = m.tool_calls;
901
+ return Array.isArray(tc) && tc.some((c) => c?.id === toolCallId);
902
+ });
903
+ };
904
+ const addAssistantMessageIfNeeded = (toolCallId) => {
905
+ if (!hasAssistantToolCallMessage(toolCallId)) this.addAssistantMessageWithToolCalls(state);
906
+ };
907
+ const alreadyHasToolResultMessage = (toolCallId) => {
908
+ return state.messages.some((m) => m && typeof m === "object" && m.role === "tool" && m.tool_call_id === toolCallId);
909
+ };
910
+ for (const tc of state.pendingToolCalls) {
911
+ if (alreadyHasToolResultMessage(tc.toolCall.id)) continue;
912
+ const tool = this._tools?.get(tc.toolCall.function.name);
913
+ const riskLevel = tool?.riskLevel ?? "safe";
914
+ if (tool && shouldRequireApproval(riskLevel)) {
915
+ let args = {};
916
+ try {
917
+ args = typeof tc.toolCall.function.arguments === "string" ? JSON.parse(tc.toolCall.function.arguments) : tc.toolCall.function.arguments;
918
+ } catch {
919
+ args = { _raw: tc.toolCall.function.arguments };
920
+ }
921
+ const toolName = tc.toolCall.function.name;
922
+ let approvalResult = approvalDecisions[tc.toolCall.id];
923
+ if (!approvalResult && onToolApproval) {
924
+ yield {
925
+ type: "tool_approval_requested",
926
+ tool_call_id: tc.toolCall.id,
927
+ toolName,
928
+ riskLevel,
929
+ args
930
+ };
931
+ approvalResult = await onToolApproval({
932
+ toolName,
933
+ toolCall: tc.toolCall,
934
+ riskLevel,
935
+ args
936
+ });
937
+ }
938
+ if (!approvalResult) {
939
+ const checkpoint = toLoopCheckpoint(state, {
940
+ agentName: this.name,
941
+ phase: "approval_pending",
942
+ status: `Waiting for approval: ${toolName}`,
943
+ modelConfig: options?.checkpointModelConfig,
944
+ requestParams: options?.requestParams
945
+ });
946
+ if (options?.stateStore) await options.stateStore.saveCheckpoint(checkpoint);
947
+ yield {
948
+ type: "requires_action",
949
+ kind: "tool_approval",
950
+ tool_call_id: tc.toolCall.id,
951
+ toolName,
952
+ riskLevel,
953
+ args,
954
+ checkpoint: options?.stateStore ? void 0 : checkpoint,
955
+ checkpointRef: options?.stateStore ? {
956
+ sessionId: checkpoint.sessionId,
957
+ agentId: checkpoint.agentId
958
+ } : void 0
959
+ };
960
+ throw new AgentPauseError();
961
+ }
962
+ if ("pending" in approvalResult && approvalResult.pending) {
963
+ const checkpoint = toLoopCheckpoint(state, {
964
+ agentName: this.name,
965
+ phase: "approval_pending",
966
+ status: approvalResult.reason ? `Waiting for approval: ${toolName} (${approvalResult.reason})` : `Waiting for approval: ${toolName}`,
967
+ modelConfig: options?.checkpointModelConfig,
968
+ requestParams: options?.requestParams
969
+ });
970
+ if (options?.stateStore) await options.stateStore.saveCheckpoint(checkpoint);
971
+ yield {
972
+ type: "requires_action",
973
+ kind: "tool_approval",
974
+ tool_call_id: tc.toolCall.id,
975
+ toolName,
976
+ riskLevel,
977
+ args,
978
+ checkpoint: options?.stateStore ? void 0 : checkpoint,
979
+ checkpointRef: options?.stateStore ? {
980
+ sessionId: checkpoint.sessionId,
981
+ agentId: checkpoint.agentId
982
+ } : void 0
983
+ };
984
+ throw new AgentPauseError();
985
+ }
986
+ if (!("approved" in approvalResult) || !approvalResult.approved) {
987
+ const reason = approvalResult.reason ?? "User denied approval";
988
+ tc.result = `Tool execution skipped: ${reason}`;
989
+ tc.isError = true;
990
+ addAssistantMessageIfNeeded(tc.toolCall.id);
991
+ yield {
992
+ type: "tool_skipped",
993
+ tool_call_id: tc.toolCall.id,
994
+ toolName: tc.toolCall.function.name,
995
+ reason
996
+ };
997
+ yield {
998
+ type: "tool_result",
999
+ tool_call_id: tc.toolCall.id,
1000
+ result: tc.result,
1001
+ isError: true
1002
+ };
1003
+ this.addToolResultToHistory(state, tc);
1004
+ continue;
1005
+ }
1006
+ }
1007
+ addAssistantMessageIfNeeded(tc.toolCall.id);
1008
+ yield await this.executeToolCall(tc, toolContext, signal);
1009
+ this.addToolResultToHistory(state, tc);
1010
+ }
1011
+ state.pendingToolCalls = [];
1012
+ state.iteration++;
1013
+ yield {
1014
+ type: "iteration_end",
1015
+ iteration: state.iteration - 1,
1016
+ willContinue: true,
1017
+ toolCallCount
1018
+ };
1019
+ }
1020
+ /**
1021
+ * Handle final response: add to history, emit done.
1022
+ */
1023
+ async *handleFinalResponse(state) {
1024
+ state.shouldContinue = false;
1025
+ state.stopReason = "final_response";
1026
+ this.addFinalAssistantMessage(state);
1027
+ yield {
1028
+ type: "iteration_end",
1029
+ iteration: state.iteration,
1030
+ willContinue: false,
1031
+ toolCallCount: 0
1032
+ };
1033
+ yield {
1034
+ type: "done",
1035
+ finalResponse: state.currentResponse,
1036
+ stopReason: "final_response",
1037
+ modelStopReason: state.lastModelStopReason
1038
+ };
1039
+ }
1040
+ };
1041
+
1042
+ //#endregion
1043
+ //#region src/state/types.ts
1044
+ /**
1045
+ * Predefined state keys for common data types.
1046
+ */
1047
+ const StateKeys = {
1048
+ CHECKPOINT: "checkpoint",
1049
+ COMPRESSION: "compression",
1050
+ SESSION: "session",
1051
+ COMPRESSION_SNAPSHOT: "compression-snapshot"
1052
+ };
1053
+
1054
+ //#endregion
1055
+ //#region src/state/stateStore.ts
1056
+ /**
1057
+ * Abstract base class for state storage.
1058
+ *
1059
+ * StateStore provides a session-centric storage abstraction where all data
1060
+ * is organized by sessionId. Each session can have multiple keys storing
1061
+ * different types of data (checkpoint, compression state, session info, etc.).
1062
+ *
1063
+ * Subclasses only need to implement the low-level storage primitives:
1064
+ * - _write: Write data to a path
1065
+ * - _read: Read data from a path
1066
+ * - _delete: Delete data at a path
1067
+ * - _exists: Check if data exists at a path
1068
+ * - _list: List all paths with a given prefix
1069
+ *
1070
+ * The base class provides:
1071
+ * - High-level API for session-based storage (save, load, delete, etc.)
1072
+ * - Convenience methods for checkpoint operations
1073
+ * - JSON serialization/deserialization
1074
+ *
1075
+ * @example
1076
+ * ```typescript
1077
+ * // Using with an agent
1078
+ * const store = new FileStateStore({ dir: './state' })
1079
+ *
1080
+ * const agent = new Agent({
1081
+ * name: 'MyAgent',
1082
+ * stateStore: store,
1083
+ * // ...
1084
+ * })
1085
+ *
1086
+ * // Manual state operations
1087
+ * await store.save(sessionId, 'custom-key', { myData: 123 })
1088
+ * const data = await store.load(sessionId, 'custom-key')
1089
+ * ```
1090
+ */
1091
+ var StateStore = class {
1092
+ /**
1093
+ * When to save checkpoints during agent execution.
1094
+ */
1095
+ savePoint;
1096
+ /**
1097
+ * Whether to delete checkpoint after successful completion.
1098
+ */
1099
+ deleteOnComplete;
1100
+ constructor(options) {
1101
+ this.savePoint = options?.savePoint ?? "before";
1102
+ this.deleteOnComplete = options?.deleteOnComplete ?? true;
1103
+ }
1104
+ /**
1105
+ * Save data for a session under a specific key.
1106
+ *
1107
+ * @param sessionId - Session identifier
1108
+ * @param key - Data key (e.g., 'checkpoint', 'compression', or custom keys)
1109
+ * @param data - Data to save (will be JSON serialized)
1110
+ */
1111
+ async save(sessionId, key, data) {
1112
+ const path$1 = this.buildPath(sessionId, key);
1113
+ const serialized = JSON.stringify(data, null, 2);
1114
+ await this._write(path$1, serialized);
1115
+ }
1116
+ /**
1117
+ * Load data for a session by key.
1118
+ *
1119
+ * @param sessionId - Session identifier
1120
+ * @param key - Data key
1121
+ * @returns The data or undefined if not found
1122
+ */
1123
+ async load(sessionId, key) {
1124
+ const path$1 = this.buildPath(sessionId, key);
1125
+ const content = await this._read(path$1);
1126
+ if (content === void 0) return;
1127
+ try {
1128
+ return JSON.parse(content);
1129
+ } catch {
1130
+ return;
1131
+ }
1132
+ }
1133
+ /**
1134
+ * Delete data for a session by key.
1135
+ *
1136
+ * @param sessionId - Session identifier
1137
+ * @param key - Data key
1138
+ */
1139
+ async delete(sessionId, key) {
1140
+ const path$1 = this.buildPath(sessionId, key);
1141
+ await this._delete(path$1);
1142
+ }
1143
+ /**
1144
+ * Delete all data for a session.
1145
+ *
1146
+ * @param sessionId - Session identifier
1147
+ */
1148
+ async deleteSession(sessionId) {
1149
+ const prefix = this.buildPrefix(sessionId);
1150
+ const paths = await this._list(prefix);
1151
+ await Promise.all(paths.map((path$1) => this._delete(path$1)));
1152
+ }
1153
+ /**
1154
+ * List all keys for a session.
1155
+ *
1156
+ * @param sessionId - Session identifier
1157
+ * @returns Array of keys
1158
+ */
1159
+ async listKeys(sessionId) {
1160
+ const prefix = this.buildPrefix(sessionId);
1161
+ return (await this._list(prefix)).map((path$1) => this.extractKey(sessionId, path$1));
1162
+ }
1163
+ /**
1164
+ * Check if data exists for a session key.
1165
+ *
1166
+ * @param sessionId - Session identifier
1167
+ * @param key - Data key
1168
+ * @returns True if data exists
1169
+ */
1170
+ async exists(sessionId, key) {
1171
+ const path$1 = this.buildPath(sessionId, key);
1172
+ return this._exists(path$1);
1173
+ }
1174
+ /**
1175
+ * Save an agent loop checkpoint.
1176
+ *
1177
+ * This is a convenience method that saves the checkpoint under the
1178
+ * predefined CHECKPOINT key with additional metadata.
1179
+ *
1180
+ * @param checkpoint - Checkpoint to save
1181
+ */
1182
+ async saveCheckpoint(checkpoint) {
1183
+ const wrapped = {
1184
+ _meta: {
1185
+ description: "GoatChain Agent Loop Checkpoint - DO NOT EDIT MANUALLY",
1186
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1187
+ agentId: checkpoint.agentId,
1188
+ agentName: checkpoint.agentName,
1189
+ sessionId: checkpoint.sessionId,
1190
+ iteration: checkpoint.iteration,
1191
+ phase: checkpoint.phase,
1192
+ status: checkpoint.status,
1193
+ messageCount: checkpoint.messages.length,
1194
+ toolCallsPending: checkpoint.pendingToolCalls?.length ?? 0
1195
+ },
1196
+ checkpoint
1197
+ };
1198
+ await this.save(checkpoint.sessionId, StateKeys.CHECKPOINT, wrapped);
1199
+ }
1200
+ /**
1201
+ * Load an agent loop checkpoint by session ID.
1202
+ *
1203
+ * @param sessionId - Session identifier
1204
+ * @returns Checkpoint or undefined if not found
1205
+ */
1206
+ async loadCheckpoint(sessionId) {
1207
+ return (await this.load(sessionId, StateKeys.CHECKPOINT))?.checkpoint;
1208
+ }
1209
+ /**
1210
+ * Delete an agent loop checkpoint.
1211
+ *
1212
+ * @param sessionId - Session identifier
1213
+ */
1214
+ async deleteCheckpoint(sessionId) {
1215
+ await this.delete(sessionId, StateKeys.CHECKPOINT);
1216
+ }
1217
+ /**
1218
+ * List all checkpoints across all sessions.
1219
+ *
1220
+ * @returns Array of checkpoints
1221
+ */
1222
+ async listCheckpoints() {
1223
+ const sessionIds = await this.listSessions();
1224
+ const checkpoints = [];
1225
+ for (const sessionId of sessionIds) {
1226
+ const checkpoint = await this.loadCheckpoint(sessionId);
1227
+ if (checkpoint) checkpoints.push(checkpoint);
1228
+ }
1229
+ return checkpoints;
1230
+ }
1231
+ /**
1232
+ * List all session IDs that have stored data.
1233
+ *
1234
+ * @returns Array of session IDs
1235
+ */
1236
+ async listSessions() {
1237
+ const allPaths = await this._list("");
1238
+ const sessionIds = /* @__PURE__ */ new Set();
1239
+ for (const path$1 of allPaths) {
1240
+ const sessionId = this.extractSessionId(path$1);
1241
+ if (sessionId) sessionIds.add(sessionId);
1242
+ }
1243
+ return Array.from(sessionIds);
1244
+ }
1245
+ /**
1246
+ * Build a storage path from sessionId and key.
1247
+ * Default format: `{sessionId}/{key}`
1248
+ *
1249
+ * Subclasses can override this for different path formats.
1250
+ */
1251
+ buildPath(sessionId, key) {
1252
+ return `${sessionId}/${key}`;
1253
+ }
1254
+ /**
1255
+ * Build a prefix for listing all data under a session.
1256
+ * Default format: `{sessionId}/`
1257
+ */
1258
+ buildPrefix(sessionId) {
1259
+ return `${sessionId}/`;
1260
+ }
1261
+ /**
1262
+ * Extract the key from a full path.
1263
+ */
1264
+ extractKey(sessionId, path$1) {
1265
+ const prefix = this.buildPrefix(sessionId);
1266
+ return path$1.startsWith(prefix) ? path$1.slice(prefix.length) : path$1;
1267
+ }
1268
+ /**
1269
+ * Extract the sessionId from a full path.
1270
+ */
1271
+ extractSessionId(path$1) {
1272
+ const parts = path$1.split("/");
1273
+ return parts.length > 0 ? parts[0] : void 0;
1274
+ }
1275
+ };
1276
+
1277
+ //#endregion
1278
+ //#region src/state/FileStateStore.ts
1279
+ /**
1280
+ * File-based implementation of StateStore.
1281
+ *
1282
+ * Stores state data as JSON files in a directory structure organized by session:
1283
+ *
1284
+ * ```
1285
+ * <baseDir>/
1286
+ * <sessionId>/
1287
+ * checkpoint.json
1288
+ * compression.json
1289
+ * session.json
1290
+ * custom-key.json
1291
+ * ```
1292
+ *
1293
+ * @example
1294
+ * ```typescript
1295
+ * const store = new FileStateStore({
1296
+ * dir: './state',
1297
+ * savePoint: 'before',
1298
+ * deleteOnComplete: true,
1299
+ * })
1300
+ *
1301
+ * const agent = new Agent({
1302
+ * name: 'MyAgent',
1303
+ * stateStore: store,
1304
+ * // ...
1305
+ * })
1306
+ * ```
1307
+ */
1308
+ var FileStateStore = class extends StateStore {
1309
+ baseDir;
1310
+ constructor(options) {
1311
+ super(options);
1312
+ this.baseDir = path.resolve(options.dir);
1313
+ this.ensureDir(this.baseDir);
1314
+ }
1315
+ async _write(storagePath, data) {
1316
+ const filePath = this.toFilePath(storagePath);
1317
+ this.ensureDir(path.dirname(filePath));
1318
+ writeFileSync(filePath, data, "utf-8");
1319
+ }
1320
+ async _read(storagePath) {
1321
+ const filePath = this.toFilePath(storagePath);
1322
+ try {
1323
+ if (!existsSync(filePath)) return;
1324
+ return readFileSync(filePath, "utf-8");
1325
+ } catch {
1326
+ return;
1327
+ }
1328
+ }
1329
+ async _delete(storagePath) {
1330
+ const filePath = this.toFilePath(storagePath);
1331
+ if (existsSync(filePath)) rmSync(filePath);
1332
+ const sessionDir = path.dirname(filePath);
1333
+ if (existsSync(sessionDir)) try {
1334
+ if (readdirSync(sessionDir).length === 0) rmSync(sessionDir, { recursive: true });
1335
+ } catch {}
1336
+ }
1337
+ async _exists(storagePath) {
1338
+ return existsSync(this.toFilePath(storagePath));
1339
+ }
1340
+ async _list(prefix) {
1341
+ const results = [];
1342
+ if (!existsSync(this.baseDir)) return results;
1343
+ const sessionDirs = readdirSync(this.baseDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1344
+ for (const sessionId of sessionDirs) {
1345
+ if (prefix && !sessionId.startsWith(prefix.split("/")[0])) continue;
1346
+ const sessionDir = path.join(this.baseDir, sessionId);
1347
+ const files = this.listJsonFiles(sessionDir);
1348
+ for (const file of files) {
1349
+ const storagePath = `${sessionId}/${path.basename(file, ".json")}`;
1350
+ if (!prefix || storagePath.startsWith(prefix)) results.push(storagePath);
1351
+ }
1352
+ }
1353
+ return results;
1354
+ }
1355
+ /**
1356
+ * Convert storage path to file system path.
1357
+ * Storage path: `{sessionId}/{key}`
1358
+ * File path: `{baseDir}/{sessionId}/{key}.json`
1359
+ */
1360
+ toFilePath(storagePath) {
1361
+ return path.join(this.baseDir, `${storagePath}.json`);
1362
+ }
1363
+ /**
1364
+ * Ensure a directory exists.
1365
+ */
1366
+ ensureDir(dir) {
1367
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1368
+ }
1369
+ /**
1370
+ * List all JSON files in a directory.
1371
+ */
1372
+ listJsonFiles(dir) {
1373
+ if (!existsSync(dir)) return [];
1374
+ return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => path.join(dir, f));
1375
+ }
1376
+ /**
1377
+ * Get the base directory path.
1378
+ */
1379
+ getBaseDir() {
1380
+ return this.baseDir;
1381
+ }
1382
+ /**
1383
+ * Clear all state data from the store.
1384
+ * WARNING: This will delete all files in the base directory.
1385
+ */
1386
+ clear() {
1387
+ if (existsSync(this.baseDir)) {
1388
+ rmSync(this.baseDir, { recursive: true });
1389
+ mkdirSync(this.baseDir, { recursive: true });
1390
+ }
1391
+ }
1392
+ };
1393
+
1394
+ //#endregion
1395
+ //#region src/state/InMemoryStateStore.ts
1396
+ /**
1397
+ * In-memory implementation of StateStore.
1398
+ *
1399
+ * Useful for development, testing, and short-lived applications.
1400
+ * Data is lost when the process exits.
1401
+ *
1402
+ * @example
1403
+ * ```typescript
1404
+ * const store = new InMemoryStateStore({
1405
+ * savePoint: 'before',
1406
+ * deleteOnComplete: true,
1407
+ * })
1408
+ *
1409
+ * const agent = new Agent({
1410
+ * name: 'MyAgent',
1411
+ * stateStore: store,
1412
+ * // ...
1413
+ * })
1414
+ * ```
1415
+ */
1416
+ var InMemoryStateStore = class extends StateStore {
1417
+ /**
1418
+ * Internal storage: path -> data
1419
+ */
1420
+ store = /* @__PURE__ */ new Map();
1421
+ constructor(options) {
1422
+ super(options);
1423
+ }
1424
+ async _write(path$1, data) {
1425
+ this.store.set(path$1, data);
1426
+ }
1427
+ async _read(path$1) {
1428
+ return this.store.get(path$1);
1429
+ }
1430
+ async _delete(path$1) {
1431
+ this.store.delete(path$1);
1432
+ }
1433
+ async _exists(path$1) {
1434
+ return this.store.has(path$1);
1435
+ }
1436
+ async _list(prefix) {
1437
+ const paths = [];
1438
+ for (const key of this.store.keys()) if (prefix === "" || key.startsWith(prefix)) paths.push(key);
1439
+ return paths;
1440
+ }
1441
+ /**
1442
+ * Clear all data from the store.
1443
+ * Useful for testing.
1444
+ */
1445
+ clear() {
1446
+ this.store.clear();
1447
+ }
1448
+ /**
1449
+ * Get statistics about the store.
1450
+ */
1451
+ stats() {
1452
+ const sessionIds = /* @__PURE__ */ new Set();
1453
+ for (const key of this.store.keys()) {
1454
+ const sessionId = this.extractSessionId(key);
1455
+ if (sessionId) sessionIds.add(sessionId);
1456
+ }
1457
+ return {
1458
+ entryCount: this.store.size,
1459
+ sessionCount: sessionIds.size
1460
+ };
1461
+ }
1462
+ };
1463
+
1464
+ //#endregion
1465
+ //#region src/agent/tokenCounter.ts
1466
+ let cachedTiktoken;
1467
+ function loadTiktoken() {
1468
+ if (cachedTiktoken !== void 0) return cachedTiktoken;
1469
+ try {
1470
+ const mod = createRequire(import.meta.url)("tiktoken");
1471
+ cachedTiktoken = mod;
1472
+ return mod;
1473
+ } catch {
1474
+ cachedTiktoken = null;
1475
+ return null;
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Default model to use for token encoding.
1480
+ */
1481
+ const DEFAULT_MODEL = "gpt-4o";
1482
+ /**
1483
+ * Count tokens in a string using the specified model's encoding.
1484
+ *
1485
+ * Creates a new encoder for each call and immediately frees it
1486
+ * to avoid memory leaks in long-running processes.
1487
+ *
1488
+ * @param text - The text to count tokens for
1489
+ * @param model - Optional model name for encoding (default: gpt-4o)
1490
+ * @returns Number of tokens
1491
+ */
1492
+ function countTokens(text, model) {
1493
+ if (!text) return 0;
1494
+ const tk = loadTiktoken();
1495
+ if (!tk) return Math.ceil(text.length / 4);
1496
+ let encoder = null;
1497
+ try {
1498
+ encoder = model ? tk.encoding_for_model(model) : tk.encoding_for_model(DEFAULT_MODEL);
1499
+ return encoder.encode(text).length;
1500
+ } catch (_error) {
1501
+ console.error("Error encoding text:", _error);
1502
+ try {
1503
+ encoder = tk.get_encoding("cl100k_base");
1504
+ return encoder.encode(text).length;
1505
+ } catch {
1506
+ return Math.ceil(text.length / 4);
1507
+ }
1508
+ } finally {
1509
+ encoder?.free();
1510
+ }
1511
+ }
1512
+ /**
1513
+ * Count tokens in a message content.
1514
+ * Handles both string content and array content (for multimodal messages).
1515
+ *
1516
+ * @param content - Message content (string or array)
1517
+ * @param model - Optional model name for encoding
1518
+ * @returns Number of tokens
1519
+ */
1520
+ function countContentTokens(content, model) {
1521
+ if (typeof content === "string") return countTokens(content, model);
1522
+ if (Array.isArray(content)) return content.reduce((sum, part) => {
1523
+ if (typeof part === "object" && part !== null && "text" in part) return sum + countTokens(String(part.text), model);
1524
+ return sum;
1525
+ }, 0);
1526
+ return 0;
1527
+ }
1528
+ /**
1529
+ * Count tokens for a single message by serializing it to JSON.
1530
+ *
1531
+ * This method is more accurate as it accounts for JSON structure overhead
1532
+ * that the actual API will see.
1533
+ *
1534
+ * @param message - The message to count tokens for
1535
+ * @param model - Optional model name for encoding
1536
+ * @returns Number of tokens
1537
+ */
1538
+ function countMessageTokens(message, model) {
1539
+ try {
1540
+ return countTokens(JSON.stringify(message), model);
1541
+ } catch {
1542
+ return countMessageTokensManual(message, model);
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Manual token counting for a single message (fallback method).
1547
+ * Includes role overhead and content tokens.
1548
+ */
1549
+ function countMessageTokensManual(message, model) {
1550
+ let tokens = 4;
1551
+ tokens += countContentTokens(message.content, model);
1552
+ if (message.role === "assistant" && message.tool_calls) for (const tc of message.tool_calls) {
1553
+ tokens += countTokens(tc.function.name, model);
1554
+ const args = typeof tc.function.arguments === "string" ? tc.function.arguments : JSON.stringify(tc.function.arguments);
1555
+ tokens += countTokens(args, model);
1556
+ tokens += 10;
1557
+ }
1558
+ if (message.role === "tool" && message.name) tokens += countTokens(message.name, model);
1559
+ if (message.role === "assistant" && message.reasoning_content) tokens += countTokens(message.reasoning_content, model);
1560
+ return tokens;
1561
+ }
1562
+ /**
1563
+ * Count total tokens in a message array.
1564
+ *
1565
+ * Serializes the entire messages array to JSON for accurate counting,
1566
+ * as this reflects what the actual API will receive.
1567
+ *
1568
+ * @param messages - Array of messages
1569
+ * @param model - Optional model name for encoding
1570
+ * @returns Total number of tokens
1571
+ */
1572
+ function countMessagesTokens(messages, model) {
1573
+ if (!messages || messages.length === 0) return 0;
1574
+ try {
1575
+ return countTokens(JSON.stringify(messages), model);
1576
+ } catch {
1577
+ return messages.reduce((sum, msg) => sum + countMessageTokens(msg, model), 3);
1578
+ }
1579
+ }
1580
+
1581
+ //#endregion
1582
+ //#region src/agent/contextCompressionMiddleware.ts
1583
+ /**
1584
+ * Placeholder text for cleared tool outputs.
1585
+ */
1586
+ const CLEARED_TOOL_OUTPUT = "[Old tool result content cleared]";
1587
+ /**
1588
+ * Default summary generation prompt.
1589
+ */
1590
+ const DEFAULT_SUMMARY_PROMPT = `You are Claude Code, Anthropic's official CLI for Claude.
1591
+
1592
+ You are a helpful AI assistant tasked with summarizing conversations.
1593
+
1594
+ Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
1595
+ This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
1596
+
1597
+ Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
1598
+
1599
+ 1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
1600
+ - The user's explicit requests and intents
1601
+ - Your approach to addressing the user's requests
1602
+ - Key decisions, technical concepts and code patterns
1603
+ - Specific details like:
1604
+ - file names
1605
+ - full code snippets
1606
+ - function signatures
1607
+ - file edits
1608
+ - Errors that you ran into and how you fixed them
1609
+ - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
1610
+ 2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
1611
+
1612
+ Your summary should include the following sections:
1613
+
1614
+ 1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
1615
+ 2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
1616
+ 3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
1617
+ 4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
1618
+ 5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
1619
+ 6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
1620
+ 6. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
1621
+ 7. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
1622
+ 8. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
1623
+ If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
1624
+
1625
+ Here's an example of how your output should be structured:
1626
+
1627
+ <example>
1628
+ <analysis>
1629
+ [Your thought process, ensuring all points are covered thoroughly and accurately]
1630
+ </analysis>
1631
+
1632
+ <summary>
1633
+ 1. Primary Request and Intent:
1634
+ [Detailed description]
1635
+
1636
+ 2. Key Technical Concepts:
1637
+ - [Concept 1]
1638
+ - [Concept 2]
1639
+ - [...]
1640
+
1641
+ 3. Files and Code Sections:
1642
+ - [File Name 1]
1643
+ - [Summary of why this file is important]
1644
+ - [Summary of the changes made to this file, if any]
1645
+ - [Important Code Snippet]
1646
+ - [File Name 2]
1647
+ - [Important Code Snippet]
1648
+ - [...]
1649
+
1650
+ 4. Errors and fixes:
1651
+ - [Detailed description of error 1]:
1652
+ - [How you fixed the error]
1653
+ - [User feedback on the error if any]
1654
+ - [...]
1655
+
1656
+ 5. Problem Solving:
1657
+ [Description of solved problems and ongoing troubleshooting]
1658
+
1659
+ 6. All user messages:
1660
+ - [Detailed non tool use user message]
1661
+ - [...]
1662
+
1663
+ 7. Pending Tasks:
1664
+ - [Task 1]
1665
+ - [Task 2]
1666
+ - [...]
1667
+
1668
+ 8. Current Work:
1669
+ [Precise description of current work]
1670
+
1671
+ 9. Optional Next Step:
1672
+ [Optional Next step to take]
1673
+
1674
+ </summary>
1675
+ </example>
1676
+
1677
+ Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
1678
+
1679
+ There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include:
1680
+ <example>
1681
+ ## Compact Instructions
1682
+ When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them.
1683
+ </example>
1684
+
1685
+ <example>
1686
+ # Summary instructions
1687
+ When you are using compact - please focus on test output and code changes. Include file reads verbatim.
1688
+ </example>
1689
+
1690
+ Existing summary (may be empty):
1691
+ {{existingSummary}}
1692
+
1693
+ Messages to summarize:
1694
+ {{toolOutputs}}
1695
+ `;
1696
+ /**
1697
+ * Create a context compression middleware.
1698
+ *
1699
+ * This middleware automatically compresses conversation history when it exceeds
1700
+ * the configured token limit. It follows an immutable pattern - original messages
1701
+ * are never modified. Instead, compressed messages are passed to the LLM while
1702
+ * the original messages remain unchanged (preserved for checkpoint).
1703
+ *
1704
+ * Compression strategy:
1705
+ * 1. Preserve message structure (all messages remain, including tool calls)
1706
+ * 2. Protect recent N turns completely
1707
+ * 3. Protect recent tool outputs up to token limit
1708
+ * 4. Clear old tool outputs with placeholder: "[Old tool result content cleared]"
1709
+ * 5. Optionally generate a summary of cleared content
1710
+ *
1711
+ * @param options - Compression configuration options
1712
+ * @returns Middleware function
1713
+ *
1714
+ * @example
1715
+ * ```ts
1716
+ * const agent = new Agent({ model, ... })
1717
+ *
1718
+ * // Basic usage - just clear old tool outputs
1719
+ * agent.use(createContextCompressionMiddleware({
1720
+ * contextLimit: 128000,
1721
+ * outputLimit: 4096,
1722
+ * }))
1723
+ *
1724
+ * // With summary generation and persistence
1725
+ * agent.use(createContextCompressionMiddleware({
1726
+ * contextLimit: 128000,
1727
+ * outputLimit: 4096,
1728
+ * enableSummary: true,
1729
+ * getModel: () => agent.model,
1730
+ * getStateStore: () => agent.stateStore,
1731
+ * }))
1732
+ * ```
1733
+ */
1734
+ function createContextCompressionMiddleware(options) {
1735
+ const { contextLimit, outputLimit, protectedTurns = 2, protectedToolTokens = 4e4, trimToolOutputThreshold = 2e4, enableSummary = false, model, getModel, summaryPrompt = DEFAULT_SUMMARY_PROMPT, stateStore, getStateStore, persistClearedContent = false, onCompressionStart, onCompressionEnd } = options;
1736
+ if (enableSummary && !model && !getModel) throw new Error("ContextCompressionMiddleware: \"model\" or \"getModel\" required when enableSummary is true");
1737
+ const compressionThreshold = contextLimit - outputLimit;
1738
+ return async (state, next) => {
1739
+ const store = stateStore ?? getStateStore?.();
1740
+ let compressionState;
1741
+ if (store) compressionState = await store.load(state.sessionId, StateKeys.COMPRESSION);
1742
+ let messagesWithSummary = state.messages;
1743
+ if (compressionState?.summary) {
1744
+ if (!messagesWithSummary.some((m) => m.role === "user" && typeof m.content === "string" && m.content.startsWith("[Context from cleared tool outputs]"))) {
1745
+ const systemIndex = messagesWithSummary.findIndex((m) => m.role === "system");
1746
+ const insertIndex = systemIndex >= 0 ? systemIndex + 1 : 0;
1747
+ messagesWithSummary = [
1748
+ ...messagesWithSummary.slice(0, insertIndex),
1749
+ {
1750
+ role: "user",
1751
+ content: `[Context from cleared tool outputs]\n${compressionState.summary}`
1752
+ },
1753
+ ...messagesWithSummary.slice(insertIndex)
1754
+ ];
1755
+ console.log(`📋 [Compression] Auto-loaded summary (${compressionState.summary.length} chars) into messages at index ${insertIndex}`);
1756
+ }
1757
+ }
1758
+ if (countMessagesTokens(messagesWithSummary) > compressionThreshold) {
1759
+ const summaryModel = enableSummary ? model ?? getModel?.() : void 0;
1760
+ const { compressedMessages, stats, summary } = await compressMessagesImmutable(messagesWithSummary, {
1761
+ protectedTurns,
1762
+ protectedToolTokens,
1763
+ trimToolOutputThreshold,
1764
+ targetTokens: compressionThreshold,
1765
+ enableSummary,
1766
+ summaryPrompt
1767
+ }, summaryModel, compressionState?.summary, onCompressionStart, onCompressionEnd);
1768
+ const processedState = {
1769
+ ...state,
1770
+ messages: compressedMessages,
1771
+ metadata: {
1772
+ ...state.metadata,
1773
+ lastCompression: stats
1774
+ }
1775
+ };
1776
+ if (store) {
1777
+ const now = Date.now();
1778
+ const newCompressionState = {
1779
+ lastStats: stats,
1780
+ history: [...compressionState?.history ?? [], stats],
1781
+ summary,
1782
+ updatedAt: now
1783
+ };
1784
+ await store.save(state.sessionId, StateKeys.COMPRESSION, newCompressionState);
1785
+ if (persistClearedContent) {
1786
+ const snapshot = {
1787
+ messages: compressedMessages.map((msg) => ({
1788
+ role: msg.role,
1789
+ content: msg.content,
1790
+ tool_call_id: msg.tool_call_id,
1791
+ name: msg.name,
1792
+ tool_calls: "tool_calls" in msg ? msg.tool_calls : void 0
1793
+ })),
1794
+ stats,
1795
+ timestamp: now
1796
+ };
1797
+ const snapshotKey = `${StateKeys.COMPRESSION_SNAPSHOT}-${now}`;
1798
+ await store.save(state.sessionId, snapshotKey, snapshot);
1799
+ }
1800
+ }
1801
+ return next(processedState);
1802
+ }
1803
+ if (messagesWithSummary !== state.messages) return next({
1804
+ ...state,
1805
+ messages: messagesWithSummary
1806
+ });
1807
+ return next(state);
1808
+ };
1809
+ }
1810
+ /**
1811
+ * Compress messages immutably by creating new messages array with cleared tool outputs.
1812
+ *
1813
+ * Strategy:
1814
+ * 1. Identify protected turns (recent N turns are fully protected)
1815
+ * 2. Identify protected tool outputs (recent outputs up to token limit)
1816
+ * 3. Create new messages with unprotected tool outputs cleared
1817
+ * 4. Trim protected but large tool outputs
1818
+ * 5. Optionally generate summary of cleared content
1819
+ *
1820
+ * This function never modifies the input messages array.
1821
+ */
1822
+ async function compressMessagesImmutable(originalMessages, options, model, existingSummary, onStart, onEnd) {
1823
+ const { protectedTurns, protectedToolTokens, trimToolOutputThreshold, enableSummary, summaryPrompt } = options;
1824
+ const tokensBefore = countMessagesTokens(originalMessages);
1825
+ console.log(`\n🗜️ [Compression] Starting compression process...`);
1826
+ console.log(` 📊 Tokens before: ${tokensBefore.toLocaleString()}`);
1827
+ console.log(` 🎯 Target tokens: ${options.targetTokens.toLocaleString()}`);
1828
+ const protectedBoundary = findProtectedBoundary(originalMessages, protectedTurns);
1829
+ console.log(` 🛡️ Protected turns boundary: index ${protectedBoundary} (protecting ${protectedTurns} turns)`);
1830
+ const { protectedToolIds, unprotectedToolMessages } = categorizeToolMessages(originalMessages, protectedBoundary, protectedToolTokens);
1831
+ console.log(` 🔒 Protected tool outputs: ${protectedToolIds.size}`);
1832
+ console.log(` 🗑️ Tool outputs to clear: ${unprotectedToolMessages.length}`);
1833
+ const initialStats = {
1834
+ tokensBefore,
1835
+ tokensAfter: 0,
1836
+ clearedToolOutputs: unprotectedToolMessages.length,
1837
+ trimmedToolOutputs: 0,
1838
+ summaryGenerated: false,
1839
+ timestamp: Date.now()
1840
+ };
1841
+ onStart?.(initialStats);
1842
+ const clearedContents = [];
1843
+ let trimmedCount = 0;
1844
+ const compressedMessages = originalMessages.map((msg) => {
1845
+ if (msg.role === "tool") {
1846
+ const toolMsg = msg;
1847
+ if (!protectedToolIds.has(toolMsg.tool_call_id)) {
1848
+ const originalContent = typeof toolMsg.content === "string" ? toolMsg.content : JSON.stringify(toolMsg.content);
1849
+ clearedContents.push({
1850
+ name: toolMsg.name,
1851
+ content: originalContent
1852
+ });
1853
+ return {
1854
+ ...toolMsg,
1855
+ content: CLEARED_TOOL_OUTPUT
1856
+ };
1857
+ } else if (countContentTokens(toolMsg.content) > trimToolOutputThreshold) {
1858
+ trimmedCount++;
1859
+ return trimToolOutput(toolMsg, trimToolOutputThreshold);
1860
+ }
1861
+ }
1862
+ return msg;
1863
+ });
1864
+ let newSummary = existingSummary;
1865
+ if (enableSummary && model && clearedContents.length > 0) {
1866
+ console.log(`\n📝 [Compression] Starting summary generation for ${clearedContents.length} cleared tool outputs...`);
1867
+ const generatedSummary = (await generateSummary(clearedContents, model, summaryPrompt, existingSummary)).trim();
1868
+ if (generatedSummary.length > 0) {
1869
+ console.log(`✅ [Compression] Summary generated (${generatedSummary.length} chars)`);
1870
+ newSummary = generatedSummary;
1871
+ const existingSummaryIndex = compressedMessages.findIndex((m) => m.role === "user" && typeof m.content === "string" && m.content.startsWith("[Context from cleared tool outputs]"));
1872
+ if (existingSummaryIndex >= 0) {
1873
+ console.log(`🗑️ [Compression] Removing existing summary message at index ${existingSummaryIndex}`);
1874
+ compressedMessages.splice(existingSummaryIndex, 1);
1875
+ }
1876
+ const systemIndex = compressedMessages.findIndex((m) => m.role === "system");
1877
+ const insertIndex = systemIndex >= 0 ? systemIndex + 1 : 0;
1878
+ console.log(`📌 [Compression] Inserting summary message at index ${insertIndex} (after system message)`);
1879
+ compressedMessages.splice(insertIndex, 0, {
1880
+ role: "user",
1881
+ content: `[Context from cleared tool outputs]\n${newSummary}`
1882
+ });
1883
+ }
1884
+ }
1885
+ let mergedMessages = mergeAdjacentUserMessages(compressedMessages);
1886
+ const tokensAfter = countMessagesTokens(mergedMessages);
1887
+ if (enableSummary && newSummary && tokensAfter > options.targetTokens) {
1888
+ const summaryMessageIndex = compressedMessages.findIndex((m) => m.role === "user" && typeof m.content === "string" && m.content.startsWith("[Context from cleared tool outputs]"));
1889
+ if (summaryMessageIndex >= 0) {
1890
+ const summaryMessage = compressedMessages[summaryMessageIndex];
1891
+ const summaryContent = typeof summaryMessage.content === "string" ? summaryMessage.content : "";
1892
+ const summaryTokens = countContentTokens(summaryContent);
1893
+ const excessTokens = tokensAfter - options.targetTokens;
1894
+ if (summaryTokens > excessTokens) {
1895
+ console.log(` ⚠️ [Compression] Summary too large (${summaryTokens} tokens), truncating...`);
1896
+ const targetSummaryTokens = Math.max(100, summaryTokens - excessTokens - 100);
1897
+ const targetSummaryChars = targetSummaryTokens * 4;
1898
+ const truncatedSummary = summaryContent.slice(0, targetSummaryChars - 43) + "\n\n[... summary truncated due to length ...]";
1899
+ compressedMessages[summaryMessageIndex] = {
1900
+ ...summaryMessage,
1901
+ content: truncatedSummary
1902
+ };
1903
+ console.log(` ✂️ [Compression] Summary truncated from ${summaryTokens} to ~${targetSummaryTokens} tokens`);
1904
+ }
1905
+ }
1906
+ }
1907
+ mergedMessages = mergeAdjacentUserMessages(compressedMessages);
1908
+ const finalTokensAfter = countMessagesTokens(mergedMessages);
1909
+ const tokensSaved = tokensBefore - finalTokensAfter;
1910
+ const compressionRatio = (tokensSaved / tokensBefore * 100).toFixed(1);
1911
+ console.log(`\n✅ [Compression] Compression completed:`);
1912
+ console.log(` 📊 Tokens: ${tokensBefore.toLocaleString()} → ${finalTokensAfter.toLocaleString()} (saved ${tokensSaved.toLocaleString()}, ${compressionRatio}%)`);
1913
+ console.log(` 🗑️ Cleared: ${clearedContents.length} tool outputs`);
1914
+ console.log(` ✂️ Trimmed: ${trimmedCount} tool outputs`);
1915
+ console.log(` 📝 Summary: ${enableSummary && clearedContents.length > 0 ? "Generated" : "Not generated"}`);
1916
+ if (finalTokensAfter > options.targetTokens) console.log(` ⚠️ [Compression] WARNING: Still exceeds target by ${finalTokensAfter - options.targetTokens} tokens`);
1917
+ const finalStats = {
1918
+ tokensBefore,
1919
+ tokensAfter: finalTokensAfter,
1920
+ clearedToolOutputs: clearedContents.length,
1921
+ trimmedToolOutputs: trimmedCount,
1922
+ summaryGenerated: enableSummary && clearedContents.length > 0,
1923
+ timestamp: Date.now()
1924
+ };
1925
+ onEnd?.(finalStats);
1926
+ return {
1927
+ compressedMessages: mergedMessages,
1928
+ stats: finalStats,
1929
+ summary: newSummary,
1930
+ clearedContents
1931
+ };
1932
+ }
1933
+ /**
1934
+ * Find the message index that marks the start of protected turns.
1935
+ * Messages at or after this index are protected.
1936
+ */
1937
+ function findProtectedBoundary(messages, protectedTurns) {
1938
+ if (protectedTurns <= 0) return messages.length;
1939
+ let turnCount = 0;
1940
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === "user") {
1941
+ turnCount++;
1942
+ if (turnCount >= protectedTurns) return i;
1943
+ }
1944
+ return 0;
1945
+ }
1946
+ /**
1947
+ * Categorize tool messages into protected and unprotected groups.
1948
+ */
1949
+ function categorizeToolMessages(messages, protectedBoundary, protectedToolTokens) {
1950
+ const protectedToolIds = /* @__PURE__ */ new Set();
1951
+ const unprotectedToolMessages = [];
1952
+ for (let i = protectedBoundary; i < messages.length; i++) {
1953
+ const msg = messages[i];
1954
+ if (msg.role === "tool") protectedToolIds.add(msg.tool_call_id);
1955
+ }
1956
+ let toolTokens = 0;
1957
+ for (let i = protectedBoundary - 1; i >= 0; i--) {
1958
+ const msg = messages[i];
1959
+ if (msg.role === "tool") {
1960
+ const toolMsg = msg;
1961
+ const msgTokens = countContentTokens(toolMsg.content);
1962
+ if (toolTokens + msgTokens <= protectedToolTokens) {
1963
+ protectedToolIds.add(toolMsg.tool_call_id);
1964
+ toolTokens += msgTokens;
1965
+ }
1966
+ }
1967
+ }
1968
+ for (let i = 0; i < protectedBoundary; i++) {
1969
+ const msg = messages[i];
1970
+ if (msg.role === "tool") {
1971
+ const toolMsg = msg;
1972
+ if (!protectedToolIds.has(toolMsg.tool_call_id)) unprotectedToolMessages.push(toolMsg);
1973
+ }
1974
+ }
1975
+ return {
1976
+ protectedToolIds,
1977
+ unprotectedToolMessages
1978
+ };
1979
+ }
1980
+ /**
1981
+ * Trim a tool message output to fit within token limit.
1982
+ */
1983
+ function trimToolOutput(msg, maxTokens) {
1984
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
1985
+ const maxChars = maxTokens * 4;
1986
+ const truncationMarker = "\n\n[... content truncated due to length ...]";
1987
+ if (content.length <= maxChars) return msg;
1988
+ const truncatedContent = content.slice(0, maxChars - 43) + truncationMarker;
1989
+ return {
1990
+ ...msg,
1991
+ content: truncatedContent
1992
+ };
1993
+ }
1994
+ function mergeAdjacentUserMessages(messages) {
1995
+ const merged = [];
1996
+ for (const msg of messages) {
1997
+ if (msg.role !== "user") {
1998
+ merged.push(msg);
1999
+ continue;
2000
+ }
2001
+ const last = merged[merged.length - 1];
2002
+ if (last && last.role === "user") {
2003
+ if (last.name === msg.name) {
2004
+ merged[merged.length - 1] = {
2005
+ ...last,
2006
+ content: mergeUserContent(last.content, msg.content)
2007
+ };
2008
+ continue;
2009
+ }
2010
+ }
2011
+ merged.push(msg);
2012
+ }
2013
+ return merged;
2014
+ }
2015
+ function mergeUserContent(left, right) {
2016
+ return [...toContentBlocks(left), ...toContentBlocks(right)];
2017
+ }
2018
+ function toContentBlocks(content) {
2019
+ if (typeof content === "string") return content.trim().length > 0 ? [{
2020
+ type: "text",
2021
+ text: content
2022
+ }] : [];
2023
+ if (Array.isArray(content)) {
2024
+ const blocks = [];
2025
+ const items = content;
2026
+ for (const item of items) {
2027
+ if (typeof item === "string") {
2028
+ const trimmed = item.trim();
2029
+ if (trimmed.length > 0) blocks.push({
2030
+ type: "text",
2031
+ text: trimmed
2032
+ });
2033
+ continue;
2034
+ }
2035
+ if (item && typeof item === "object" && "type" in item) {
2036
+ const block = item;
2037
+ if (block.type === "text") {
2038
+ const text = block.text;
2039
+ if (typeof text === "string" && text.trim().length === 0) continue;
2040
+ }
2041
+ blocks.push(block);
2042
+ continue;
2043
+ }
2044
+ if (item !== null && item !== void 0) {
2045
+ const text = String(item);
2046
+ if (text.trim().length > 0) blocks.push({
2047
+ type: "text",
2048
+ text
2049
+ });
2050
+ }
2051
+ }
2052
+ return blocks;
2053
+ }
2054
+ if (content && typeof content === "object" && "type" in content) {
2055
+ const block = content;
2056
+ if (block.type === "text") {
2057
+ const text = block.text;
2058
+ if (typeof text === "string" && text.trim().length === 0) return [];
2059
+ }
2060
+ return [block];
2061
+ }
2062
+ const fallback = String(content);
2063
+ return fallback.trim().length > 0 ? [{
2064
+ type: "text",
2065
+ text: fallback
2066
+ }] : [];
2067
+ }
2068
+ /**
2069
+ * Generate a summary of cleared tool outputs using the model.
2070
+ */
2071
+ async function generateSummary(clearedContents, model, summaryPrompt, existingSummary) {
2072
+ const formattedContent = clearedContents.map(({ name, content }) => {
2073
+ return `## ${name ?? "unknown_tool"}\n${content.length > 5e3 ? `${content.slice(0, 5e3)}...[truncated for summary]` : content}`;
2074
+ }).join("\n\n---\n\n");
2075
+ const fullPrompt = buildSummaryPrompt(summaryPrompt, formattedContent, existingSummary);
2076
+ const promptTokens = Math.ceil(fullPrompt.length / 4);
2077
+ console.log(` 🤖 [Summary] Calling model (${model.modelId || "unknown"}) to generate summary...`);
2078
+ console.log(` 📊 [Summary] Input: ${clearedContents.length} tool outputs, ~${promptTokens} tokens`);
2079
+ console.log(` 📥 [Summary] Input content preview (first 500 chars):`);
2080
+ console.log(` ${formattedContent.slice(0, 500)}${formattedContent.length > 500 ? "..." : ""}`);
2081
+ const startTime = Date.now();
2082
+ const response = await model.invoke([{
2083
+ role: "system",
2084
+ content: "You are a helpful assistant that creates concise summaries."
2085
+ }, {
2086
+ role: "user",
2087
+ content: fullPrompt
2088
+ }], { maxTokens: 1e3 });
2089
+ const duration = Date.now() - startTime;
2090
+ const summary = typeof response.message.content === "string" ? response.message.content : JSON.stringify(response.message.content);
2091
+ console.log(` ⏱️ [Summary] Model call completed in ${duration}ms`);
2092
+ console.log(` 📄 [Summary] Generated summary (${summary.length} chars):`);
2093
+ console.log(` ${summary}`);
2094
+ console.log(` 📊 [Summary] Compression ratio: ${formattedContent.length} → ${summary.length} chars (${((1 - summary.length / formattedContent.length) * 100).toFixed(1)}% reduction)`);
2095
+ return summary;
2096
+ }
2097
+ function buildSummaryPrompt(summaryPrompt, formattedContent, existingSummary) {
2098
+ const hasToolOutputs = summaryPrompt.includes("{{toolOutputs}}");
2099
+ const hasExistingSummary = summaryPrompt.includes("{{existingSummary}}");
2100
+ const baseSummary = existingSummary ?? "";
2101
+ if (hasToolOutputs || hasExistingSummary) return summaryPrompt.split("{{toolOutputs}}").join(formattedContent).split("{{existingSummary}}").join(baseSummary);
2102
+ return `${summaryPrompt}\n\n${baseSummary.length > 0 ? `Existing summary:\n${baseSummary}\n\nTool outputs to summarize:\n${formattedContent}` : `Tool outputs to summarize:\n${formattedContent}`}`;
2103
+ }
2104
+ /**
2105
+ * Manually compress a session by generating a summary from full message history.
2106
+ *
2107
+ * This function extracts all tool outputs from the full messages and generates
2108
+ * a new summary based on the complete context. The summary is saved to
2109
+ * CompressionState and will be automatically loaded by the middleware on next run.
2110
+ *
2111
+ * @param options - Manual compression options
2112
+ * @returns Compression result with generated summary
2113
+ *
2114
+ * @example
2115
+ * ```ts
2116
+ * const result = await compressSessionManually({
2117
+ * sessionId: 'session-123',
2118
+ * fullMessages: allMessages,
2119
+ * model: myModel,
2120
+ * stateStore: myStore,
2121
+ * })
2122
+ * console.log(`Generated summary: ${result.summary}`)
2123
+ * ```
2124
+ */
2125
+ async function compressSessionManually(options) {
2126
+ const { sessionId, fullMessages, model, stateStore, summaryPrompt = DEFAULT_SUMMARY_PROMPT } = options;
2127
+ console.log(`\n📝 [Manual Compression] Starting manual compression for session ${sessionId}...`);
2128
+ console.log(` 📊 Total messages: ${fullMessages.length}`);
2129
+ const toolOutputs = [];
2130
+ for (const msg of fullMessages) if (msg.role === "tool") {
2131
+ const toolMsg = msg;
2132
+ const content = typeof toolMsg.content === "string" ? toolMsg.content : JSON.stringify(toolMsg.content);
2133
+ toolOutputs.push({
2134
+ name: toolMsg.name,
2135
+ content
2136
+ });
2137
+ }
2138
+ console.log(` 🔧 Tool outputs found: ${toolOutputs.length}`);
2139
+ if (toolOutputs.length === 0) {
2140
+ console.log(` ⚠️ [Manual Compression] No tool outputs found, nothing to summarize`);
2141
+ return {
2142
+ summary: "",
2143
+ messageCount: fullMessages.length,
2144
+ toolOutputCount: 0
2145
+ };
2146
+ }
2147
+ console.log(` 🤖 [Manual Compression] Generating summary from ${toolOutputs.length} tool outputs...`);
2148
+ const summary = (await generateSummary(toolOutputs, model, summaryPrompt, void 0)).trim();
2149
+ if (summary.length === 0) {
2150
+ console.log(` ⚠️ [Manual Compression] Summary generation returned empty result`);
2151
+ return {
2152
+ summary: "",
2153
+ messageCount: fullMessages.length,
2154
+ toolOutputCount: toolOutputs.length
2155
+ };
2156
+ }
2157
+ console.log(` ✅ [Manual Compression] Summary generated (${summary.length} chars)`);
2158
+ const existingState = await stateStore.load(sessionId, StateKeys.COMPRESSION);
2159
+ const now = Date.now();
2160
+ const newCompressionState = {
2161
+ lastStats: existingState?.lastStats,
2162
+ history: existingState?.history ?? [],
2163
+ summary,
2164
+ updatedAt: now
2165
+ };
2166
+ await stateStore.save(sessionId, StateKeys.COMPRESSION, newCompressionState);
2167
+ console.log(` 💾 [Manual Compression] Summary saved to CompressionState`);
2168
+ return {
2169
+ summary,
2170
+ messageCount: fullMessages.length,
2171
+ toolOutputCount: toolOutputs.length
2172
+ };
2173
+ }
2174
+
2175
+ //#endregion
2176
+ //#region src/model/base.ts
2177
+ /**
2178
+ * Abstract base class for LLM model providers.
2179
+ *
2180
+ * Implement this class to create custom model integrations
2181
+ * (e.g., OpenAI, Anthropic, local models).
2182
+ */
2183
+ var BaseModel = class {};
2184
+
2185
+ //#endregion
2186
+ //#region src/model/adapter.ts
2187
+ function computeFeatureGrants(options) {
2188
+ const grants = {};
2189
+ const reqs = options.featureRequests || {};
2190
+ for (const [featureId, request] of Object.entries(reqs)) if (options.supportsFeature ? options.supportsFeature(featureId) : false) grants[featureId] = {
2191
+ granted: true,
2192
+ effectiveConfig: request?.config || {}
2193
+ };
2194
+ else grants[featureId] = {
2195
+ granted: false,
2196
+ reason: "feature_not_supported"
2197
+ };
2198
+ return grants;
2199
+ }
2200
+
2201
+ //#endregion
2202
+ //#region src/model/errors.ts
2203
+ /**
2204
+ * Model error class.
2205
+ * Follows SRP: only handles error representation.
2206
+ */
2207
+ var ModelError = class extends Error {
2208
+ code;
2209
+ retryable;
2210
+ status;
2211
+ constructor(message, options) {
2212
+ super(message);
2213
+ this.name = "ModelError";
2214
+ this.code = options.code;
2215
+ this.retryable = options.retryable ?? false;
2216
+ this.status = options.status;
2217
+ }
2218
+ };
2219
+
2220
+ //#endregion
2221
+ //#region src/model/health.ts
2222
+ /**
2223
+ * Model health manager.
2224
+ * Follows SRP: only handles health state tracking.
2225
+ */
2226
+ var ModelHealth = class {
2227
+ state = /* @__PURE__ */ new Map();
2228
+ get(ref) {
2229
+ return this.state.get(this.keyOf(ref)) || {
2230
+ failures: 0,
2231
+ nextRetryAt: 0
2232
+ };
2233
+ }
2234
+ markSuccess(ref) {
2235
+ this.state.set(this.keyOf(ref), {
2236
+ failures: 0,
2237
+ nextRetryAt: 0
2238
+ });
2239
+ }
2240
+ markFailure(ref, options) {
2241
+ const failures = this.get(ref).failures + 1;
2242
+ const delay = Math.min(options.maxDelayMs, options.baseDelayMs * 2 ** Math.min(8, failures - 1));
2243
+ this.state.set(this.keyOf(ref), {
2244
+ failures,
2245
+ nextRetryAt: options.now + delay,
2246
+ lastError: {
2247
+ code: options.code,
2248
+ message: options.message
2249
+ }
2250
+ });
2251
+ }
2252
+ isAvailable(ref, now) {
2253
+ return this.get(ref).nextRetryAt <= now;
2254
+ }
2255
+ keyOf(ref) {
2256
+ return `${ref.provider}:${ref.modelId}`;
2257
+ }
2258
+ };
2259
+ /**
2260
+ * In-memory implementation for backward compatibility.
2261
+ */
2262
+ var InMemoryModelHealth = class extends ModelHealth {};
2263
+
2264
+ //#endregion
2265
+ //#region src/model/router.ts
2266
+ /**
2267
+ * Model router.
2268
+ * Follows SRP: only handles routing logic and fallback selection.
2269
+ */
2270
+ var ModelRouter = class {
2271
+ constructor(health, fallbackOrder) {
2272
+ this.health = health;
2273
+ this.fallbackOrder = fallbackOrder;
2274
+ }
2275
+ /**
2276
+ * Select available models based on health and feature requirements.
2277
+ */
2278
+ select(options) {
2279
+ const { now, requiredFeatures, isFeatureSupported } = options;
2280
+ const available = this.fallbackOrder.filter((ref) => this.health.isAvailable(ref, now));
2281
+ const withFeatures = this.filterByFeatures(available.length ? available : this.fallbackOrder, requiredFeatures, isFeatureSupported);
2282
+ return withFeatures.length ? withFeatures : this.fallbackOrder;
2283
+ }
2284
+ filterByFeatures(candidates, requiredFeatures, isFeatureSupported) {
2285
+ if (!requiredFeatures) return candidates;
2286
+ const requiredIds = Object.entries(requiredFeatures).filter(([, v]) => v?.required).map(([k]) => k);
2287
+ if (!requiredIds.length) return candidates;
2288
+ if (!isFeatureSupported) return [];
2289
+ return candidates.filter((ref) => requiredIds.every((id) => isFeatureSupported(ref, id)));
2290
+ }
2291
+ };
2292
+
2293
+ //#endregion
2294
+ //#region src/model/utils/http.ts
2295
+ /**
2296
+ * HTTP utility functions.
2297
+ * Follows SRP: only handles HTTP-related utilities.
2298
+ */
2299
+ var HttpUtils = class {
2300
+ /**
2301
+ * Determine if an HTTP status code is retryable.
2302
+ * Retryable: 408, 409, 429, 5xx
2303
+ */
2304
+ static isRetryableStatus(status) {
2305
+ return status === 408 || status === 409 || status === 429 || status >= 500 && status <= 599;
2306
+ }
2307
+ /**
2308
+ * Sleep for a specified duration, with abort signal support.
2309
+ */
2310
+ static async sleep(ms, signal) {
2311
+ if (ms <= 0) return Promise.resolve();
2312
+ return new Promise((resolve, reject) => {
2313
+ const timer = setTimeout(() => {
2314
+ cleanup();
2315
+ resolve();
2316
+ }, ms);
2317
+ function cleanup() {
2318
+ clearTimeout(timer);
2319
+ if (signal) signal.removeEventListener("abort", onAbort);
2320
+ }
2321
+ function onAbort() {
2322
+ cleanup();
2323
+ reject(/* @__PURE__ */ new Error("Aborted"));
2324
+ }
2325
+ if (signal) {
2326
+ if (signal.aborted) return onAbort();
2327
+ signal.addEventListener("abort", onAbort);
2328
+ }
2329
+ });
2330
+ }
2331
+ };
2332
+
2333
+ //#endregion
2334
+ //#region src/model/utils/id.ts
2335
+ function createRequestId(prefix = "req") {
2336
+ const rand = Math.random().toString(16).slice(2);
2337
+ return `${prefix}_${Date.now().toString(16)}_${rand}`;
2338
+ }
2339
+
2340
+ //#endregion
2341
+ //#region src/model/utils/retry.ts
2342
+ /**
2343
+ * Retry policy for model requests with exponential backoff and jitter.
2344
+ *
2345
+ * Implements industry-standard retry patterns:
2346
+ * - Exponential backoff: prevents overwhelming recovering services
2347
+ * - Jitter: prevents thundering herd when many clients retry simultaneously
2348
+ * - Max delay cap: prevents unbounded wait times
2349
+ *
2350
+ * @example
2351
+ * ```ts
2352
+ * // Default: exponential backoff with equal jitter
2353
+ * const policy = new RetryPolicy()
2354
+ *
2355
+ * // Custom: 5 attempts, 1s base, 60s max, full jitter
2356
+ * const policy = new RetryPolicy({
2357
+ * maxAttempts: 5,
2358
+ * baseDelayMs: 1000,
2359
+ * maxDelayMs: 60000,
2360
+ * jitter: 'full',
2361
+ * })
2362
+ *
2363
+ * // Usage
2364
+ * for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
2365
+ * try {
2366
+ * await makeRequest()
2367
+ * break
2368
+ * } catch (err) {
2369
+ * if (!policy.canRetry(attempt)) throw err
2370
+ * await sleep(policy.getDelay(attempt))
2371
+ * }
2372
+ * }
2373
+ * ```
2374
+ */
2375
+ var RetryPolicy = class RetryPolicy {
2376
+ maxAttempts;
2377
+ baseDelayMs;
2378
+ maxDelayMs;
2379
+ strategy;
2380
+ jitter;
2381
+ multiplier;
2382
+ _previousDelay;
2383
+ constructor(options = {}) {
2384
+ this.maxAttempts = options.maxAttempts ?? 3;
2385
+ this.baseDelayMs = options.baseDelayMs ?? 500;
2386
+ this.maxDelayMs = options.maxDelayMs ?? 3e4;
2387
+ this.strategy = options.strategy ?? "exponential";
2388
+ this.jitter = options.jitter ?? "equal";
2389
+ this.multiplier = options.multiplier ?? 2;
2390
+ this._previousDelay = this.baseDelayMs;
2391
+ }
2392
+ /**
2393
+ * Calculate delay for a given attempt.
2394
+ *
2395
+ * @param attempt - Current attempt number (1-indexed)
2396
+ * @returns Delay in milliseconds with jitter applied
2397
+ */
2398
+ getDelay(attempt) {
2399
+ let delay;
2400
+ switch (this.strategy) {
2401
+ case "exponential":
2402
+ delay = this.baseDelayMs * this.multiplier ** (attempt - 1);
2403
+ break;
2404
+ case "linear":
2405
+ delay = this.baseDelayMs * attempt;
2406
+ break;
2407
+ case "fixed":
2408
+ delay = this.baseDelayMs;
2409
+ break;
2410
+ }
2411
+ delay = Math.min(delay, this.maxDelayMs);
2412
+ delay = this.applyJitter(delay);
2413
+ this._previousDelay = delay;
2414
+ return Math.floor(delay);
2415
+ }
2416
+ /**
2417
+ * Apply jitter to the calculated delay.
2418
+ */
2419
+ applyJitter(delay) {
2420
+ switch (this.jitter) {
2421
+ case "full": return Math.random() * delay;
2422
+ case "equal": return delay / 2 + Math.random() * delay / 2;
2423
+ case "decorrelated": return Math.min(this.maxDelayMs, Math.random() * (this._previousDelay * 3 - this.baseDelayMs) + this.baseDelayMs);
2424
+ case "none":
2425
+ default: return delay;
2426
+ }
2427
+ }
2428
+ /**
2429
+ * Check if another retry is allowed.
2430
+ *
2431
+ * @param attempt - Current attempt number (1-indexed)
2432
+ * @returns true if more retries are allowed
2433
+ */
2434
+ canRetry(attempt) {
2435
+ return attempt < this.maxAttempts;
2436
+ }
2437
+ /**
2438
+ * Reset internal state (useful for decorrelated jitter).
2439
+ */
2440
+ reset() {
2441
+ this._previousDelay = this.baseDelayMs;
2442
+ }
2443
+ /**
2444
+ * Get a human-readable description of the policy.
2445
+ */
2446
+ toString() {
2447
+ return `RetryPolicy(${this.strategy}, max=${this.maxAttempts}, base=${this.baseDelayMs}ms, cap=${this.maxDelayMs}ms, jitter=${this.jitter})`;
2448
+ }
2449
+ /**
2450
+ * Create default retry policy for API calls.
2451
+ * - 3 attempts
2452
+ * - 500ms base delay
2453
+ * - 30s max delay
2454
+ * - Exponential backoff with equal jitter
2455
+ */
2456
+ static default = new RetryPolicy();
2457
+ /**
2458
+ * Create aggressive retry policy for critical operations.
2459
+ * - 5 attempts
2460
+ * - 1s base delay
2461
+ * - 60s max delay
2462
+ */
2463
+ static aggressive = new RetryPolicy({
2464
+ maxAttempts: 5,
2465
+ baseDelayMs: 1e3,
2466
+ maxDelayMs: 6e4
2467
+ });
2468
+ /**
2469
+ * Create gentle retry policy for rate-limited APIs.
2470
+ * - 3 attempts
2471
+ * - 2s base delay
2472
+ * - 30s max delay
2473
+ * - Full jitter for better spread
2474
+ */
2475
+ static gentle = new RetryPolicy({
2476
+ maxAttempts: 3,
2477
+ baseDelayMs: 2e3,
2478
+ maxDelayMs: 3e4,
2479
+ jitter: "full"
2480
+ });
2481
+ };
2482
+
2483
+ //#endregion
2484
+ //#region src/model/createModel.ts
2485
+ function createModel(options) {
2486
+ const adapterByProvider = new Map(options.adapters.map((a) => [a.provider, a]));
2487
+ const health = options.health ?? new InMemoryModelHealth();
2488
+ const initialFallbackOrder = options.routing?.fallbackOrder ?? (() => {
2489
+ const derived = [];
2490
+ for (const adapter of options.adapters) if (adapter.defaultModelId) derived.push({
2491
+ provider: adapter.provider,
2492
+ modelId: adapter.defaultModelId
2493
+ });
2494
+ if (derived.length === 0) throw new ModelError("No routing configuration and no adapter with defaultModelId provided. Either provide options.routing.fallbackOrder or set defaultModelId on your adapters.", {
2495
+ code: "missing_routing",
2496
+ retryable: false
2497
+ });
2498
+ return derived;
2499
+ })();
2500
+ const router = new ModelRouter(health, initialFallbackOrder);
2501
+ const retryPolicy = new RetryPolicy({
2502
+ maxAttempts: options.retry?.maxAttemptsPerModel ?? 3,
2503
+ baseDelayMs: options.retry?.baseDelayMs ?? 500,
2504
+ maxDelayMs: options.retry?.maxDelayMs ?? 3e4,
2505
+ strategy: options.retry?.strategy ?? "exponential",
2506
+ jitter: options.retry?.jitter ?? "equal"
2507
+ });
2508
+ const defaultTimeoutMs = options.timeoutMs ?? 6e4;
2509
+ function getAdapter(ref) {
2510
+ const adapter = adapterByProvider.get(ref.provider);
2511
+ if (!adapter) throw new ModelError(`No adapter for provider: ${ref.provider}`, {
2512
+ code: "adapter_missing",
2513
+ retryable: false
2514
+ });
2515
+ return adapter;
2516
+ }
2517
+ function isFeatureSupported(ref, featureId) {
2518
+ const adapter = adapterByProvider.get(ref.provider);
2519
+ if (!adapter?.supportsFeature) return false;
2520
+ return adapter.supportsFeature(ref.modelId, featureId);
2521
+ }
2522
+ async function* stream(requestIn) {
2523
+ const now = Date.now();
2524
+ const requestId = requestIn.requestId || createRequestId("req");
2525
+ const order = requestIn.model ? [requestIn.model] : router.select({
2526
+ now,
2527
+ requiredFeatures: requestIn.features,
2528
+ isFeatureSupported
2529
+ });
2530
+ let lastError;
2531
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
2532
+ for (const modelRef of order) {
2533
+ const adapter = getAdapter(modelRef);
2534
+ const timeoutMs = requestIn.timeoutMs ?? defaultTimeoutMs;
2535
+ const featureGrants = computeFeatureGrants({
2536
+ modelId: modelRef.modelId,
2537
+ featureRequests: requestIn.features,
2538
+ supportsFeature: (fid) => adapter.supportsFeature?.(modelRef.modelId, fid) ?? false
2539
+ });
2540
+ if (Object.entries(requestIn.features || {}).some(([fid, req$1]) => req$1?.required && !featureGrants[fid]?.granted)) continue;
2541
+ const controller = new AbortController();
2542
+ const signal = requestIn.signal;
2543
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2544
+ const abortListener = () => controller.abort();
2545
+ if (signal) if (signal.aborted) controller.abort();
2546
+ else signal.addEventListener("abort", abortListener);
2547
+ const req = {
2548
+ requestId,
2549
+ model: modelRef,
2550
+ input: requestIn.input,
2551
+ messages: requestIn.messages,
2552
+ tools: requestIn.tools,
2553
+ features: requestIn.features,
2554
+ stream: requestIn.stream ?? true,
2555
+ signal: controller.signal,
2556
+ timeoutMs
2557
+ };
2558
+ try {
2559
+ for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) try {
2560
+ for await (const ev of adapter.stream({
2561
+ request: req,
2562
+ featureGrants,
2563
+ featureRequests: requestIn.features
2564
+ })) if (ev.type === "delta") {
2565
+ if (ev.chunk.kind === "text") yield {
2566
+ type: "text_delta",
2567
+ delta: ev.chunk.text
2568
+ };
2569
+ else if (ev.chunk.kind === "thinking_delta") yield {
2570
+ type: ev.chunk.kind,
2571
+ content: ev.chunk.text
2572
+ };
2573
+ else if (ev.chunk.kind === "thinking_end" || ev.chunk.kind === "thinking_start") yield { type: ev.chunk.kind };
2574
+ else if (ev.chunk.kind === "tool_call_delta") {
2575
+ const existing = toolCallAccumulator.get(ev.chunk.callId) ?? { argsText: "" };
2576
+ if (ev.chunk.toolId) existing.toolId = ev.chunk.toolId;
2577
+ if (ev.chunk.argsTextDelta) existing.argsText += ev.chunk.argsTextDelta;
2578
+ toolCallAccumulator.set(ev.chunk.callId, existing);
2579
+ }
2580
+ } else if (ev.type === "response_end") {
2581
+ for (const [callId, data] of toolCallAccumulator) if (data.toolId) yield {
2582
+ type: "tool_call",
2583
+ toolCall: {
2584
+ id: callId,
2585
+ type: "function",
2586
+ function: {
2587
+ name: data.toolId,
2588
+ arguments: data.argsText
2589
+ }
2590
+ }
2591
+ };
2592
+ toolCallAccumulator.clear();
2593
+ if (ev.usage && typeof ev.usage === "object") {
2594
+ const usage = ev.usage;
2595
+ if (usage.prompt_tokens || usage.completion_tokens || usage.total_tokens) yield {
2596
+ type: "usage",
2597
+ usage: {
2598
+ promptTokens: usage.prompt_tokens ?? 0,
2599
+ completionTokens: usage.completion_tokens ?? 0,
2600
+ totalTokens: usage.total_tokens ?? 0
2601
+ }
2602
+ };
2603
+ }
2604
+ yield { type: "done" };
2605
+ } else yield ev;
2606
+ health.markSuccess(modelRef);
2607
+ return;
2608
+ } catch (err) {
2609
+ lastError = err;
2610
+ const modelError = normalizeModelError(err);
2611
+ if (!modelError.retryable || !retryPolicy.canRetry(attempt)) throw modelError;
2612
+ const delayMs = retryPolicy.getDelay(attempt);
2613
+ options.retry?.onRetry?.({
2614
+ attempt,
2615
+ maxAttempts: retryPolicy.maxAttempts,
2616
+ delayMs,
2617
+ error: {
2618
+ code: modelError.code,
2619
+ message: modelError.message,
2620
+ retryable: modelError.retryable
2621
+ },
2622
+ model: modelRef,
2623
+ request: req
2624
+ });
2625
+ await HttpUtils.sleep(delayMs, controller.signal);
2626
+ }
2627
+ } catch (err) {
2628
+ const modelError = normalizeModelError(err);
2629
+ health.markFailure(modelRef, {
2630
+ now: Date.now(),
2631
+ baseDelayMs: retryPolicy.baseDelayMs,
2632
+ maxDelayMs: retryPolicy.maxDelayMs,
2633
+ code: modelError.code,
2634
+ message: modelError.message
2635
+ });
2636
+ yield {
2637
+ type: "error",
2638
+ requestId,
2639
+ error: {
2640
+ code: modelError.code,
2641
+ message: modelError.message,
2642
+ retryable: modelError.retryable
2643
+ }
2644
+ };
2645
+ lastError = modelError;
2646
+ continue;
2647
+ } finally {
2648
+ clearTimeout(timeout);
2649
+ if (signal) signal.removeEventListener("abort", abortListener);
2650
+ }
2651
+ }
2652
+ throw normalizeModelError(lastError || new ModelError("All models failed", { code: "all_models_failed" }));
2653
+ }
2654
+ async function run(request) {
2655
+ let text = "";
2656
+ let stopReason = "error";
2657
+ let usage;
2658
+ let model;
2659
+ let requestId = request.requestId || "";
2660
+ let featureGrants = {};
2661
+ for await (const ev of stream({
2662
+ ...request,
2663
+ stream: true
2664
+ })) if (ev.type === "response_start") {
2665
+ requestId = ev.requestId;
2666
+ model = ev.model;
2667
+ featureGrants = ev.featureGrants;
2668
+ } else if (ev.type === "delta" && ev.chunk.kind === "text") text += ev.chunk.text;
2669
+ else if (ev.type === "text_delta") text += ev.delta;
2670
+ else if (ev.type === "response_end") {
2671
+ stopReason = ev.stopReason;
2672
+ usage = ev.usage;
2673
+ } else if (ev.type === "error") stopReason = "error";
2674
+ if (!model) throw new ModelError("Missing response_start from adapter", {
2675
+ code: "protocol_error",
2676
+ retryable: false
2677
+ });
2678
+ return {
2679
+ requestId,
2680
+ model,
2681
+ text,
2682
+ stopReason,
2683
+ usage,
2684
+ featureGrants
2685
+ };
2686
+ }
2687
+ return {
2688
+ get modelId() {
2689
+ return initialFallbackOrder[0]?.modelId ?? "unknown";
2690
+ },
2691
+ setModelId(modelId) {
2692
+ const primary = initialFallbackOrder[0];
2693
+ if (!primary) throw new ModelError("No primary model to update", {
2694
+ code: "no_primary_model",
2695
+ retryable: false
2696
+ });
2697
+ initialFallbackOrder[0] = {
2698
+ provider: primary.provider,
2699
+ modelId
2700
+ };
2701
+ },
2702
+ invoke: async (messages, options$1) => {
2703
+ const result = await run({
2704
+ messages,
2705
+ tools: options$1?.tools
2706
+ });
2707
+ return {
2708
+ message: {
2709
+ role: "assistant",
2710
+ content: result.text
2711
+ },
2712
+ usage: result.usage
2713
+ };
2714
+ },
2715
+ stream: ((messagesOrRequest, options$1) => {
2716
+ if (Array.isArray(messagesOrRequest)) return stream({
2717
+ messages: messagesOrRequest,
2718
+ tools: options$1?.tools
2719
+ });
2720
+ return stream(messagesOrRequest);
2721
+ }),
2722
+ run
2723
+ };
2724
+ }
2725
+ function normalizeModelError(err) {
2726
+ if (err instanceof ModelError) return err;
2727
+ if (err && typeof err === "object") {
2728
+ const status = typeof err.status === "number" ? err.status : void 0;
2729
+ const code = typeof err.code === "string" ? err.code : status ? `http_${status}` : "unknown_error";
2730
+ return new ModelError(typeof err.message === "string" ? err.message : "Unknown error", {
2731
+ code,
2732
+ retryable: status ? HttpUtils.isRetryableStatus(status) : false,
2733
+ status
2734
+ });
2735
+ }
2736
+ return new ModelError(String(err || "Unknown error"), {
2737
+ code: "unknown_error",
2738
+ retryable: false
2739
+ });
2740
+ }
2741
+
2742
+ //#endregion
2743
+ //#region src/model/openai/createOpenAIAdapter.ts
2744
+ /**
2745
+ * Convert Message content to OpenAI format
2746
+ * Handles string, ContentBlock, and ContentBlock[]
2747
+ */
2748
+ function toOpenAIContent(content) {
2749
+ if (typeof content === "string") return content.trim().length === 0 ? "" : content;
2750
+ if (content === null || content === void 0) return "";
2751
+ if (typeof content === "object" && "type" in content) {
2752
+ const block = content;
2753
+ if (block.type === "text" && block.text) return block.text;
2754
+ if (block.type === "image" && block.source?.data) return [{
2755
+ type: "image_url",
2756
+ image_url: { url: block.source.data }
2757
+ }];
2758
+ }
2759
+ if (Array.isArray(content)) {
2760
+ const result = [];
2761
+ for (const item of content) if (typeof item === "string") result.push({
2762
+ type: "text",
2763
+ text: item
2764
+ });
2765
+ else if (item && typeof item === "object" && "type" in item) {
2766
+ const block = item;
2767
+ if (block.type === "text" && block.text) result.push({
2768
+ type: "text",
2769
+ text: block.text
2770
+ });
2771
+ else if (block.type === "image" && block.source?.data) result.push({
2772
+ type: "image_url",
2773
+ image_url: { url: block.source.data }
2774
+ });
2775
+ }
2776
+ return result.length > 0 ? result : "";
2777
+ }
2778
+ return String(content);
2779
+ }
2780
+ /**
2781
+ * Convert Message to OpenAI format.
2782
+ *
2783
+ * The main conversion is for the `content` field, which may be in MCP format
2784
+ * (ContentBlock) and needs to be converted to OpenAI format (string or array).
2785
+ * Other fields like tool_call_id and tool_calls are already OpenAI-compatible.
2786
+ */
2787
+ function toOpenAIMessage(message) {
2788
+ return {
2789
+ ...message,
2790
+ content: toOpenAIContent(message.content)
2791
+ };
2792
+ }
2793
+ function createOpenAIAdapter(options = {}) {
2794
+ const baseUrl = options.baseUrl;
2795
+ const apiKeySecretName = options.apiKeySecretName || "OPENAI_API_KEY";
2796
+ const secretProvider = options.secretProvider;
2797
+ function getApiKey() {
2798
+ const key = secretProvider?.get(apiKeySecretName);
2799
+ if (!key) throw new ModelError(`Missing secret: ${apiKeySecretName}`, {
2800
+ code: "missing_api_key",
2801
+ retryable: false
2802
+ });
2803
+ return key;
2804
+ }
2805
+ function createClient() {
2806
+ const apiKey = getApiKey();
2807
+ if (typeof baseUrl === "string" && /\/chat\/completions\/?$/.test(baseUrl)) return new OpenAI({
2808
+ apiKey,
2809
+ organization: options.organization,
2810
+ project: options.project,
2811
+ baseURL: "https://api.openai.com/v1",
2812
+ fetch: (input, init) => {
2813
+ return fetch(baseUrl, init);
2814
+ }
2815
+ });
2816
+ return new OpenAI({
2817
+ apiKey,
2818
+ organization: options.organization,
2819
+ project: options.project,
2820
+ baseURL: baseUrl || void 0
2821
+ });
2822
+ }
2823
+ function toModelRef(request) {
2824
+ return {
2825
+ provider: "openai",
2826
+ modelId: request.model.modelId
2827
+ };
2828
+ }
2829
+ function toModelError(err) {
2830
+ if (err instanceof ModelError) return err;
2831
+ const status = typeof err?.status === "number" ? err.status : void 0;
2832
+ return new ModelError(typeof err?.message === "string" ? err.message : "OpenAI error", {
2833
+ code: typeof err?.code === "string" ? err.code : status ? `openai_http_${status}` : "openai_error",
2834
+ retryable: status ? HttpUtils.isRetryableStatus(status) : false,
2835
+ status
2836
+ });
2837
+ }
2838
+ function supportsFeature(_modelId, _featureId) {
2839
+ return false;
2840
+ }
2841
+ async function* stream(args) {
2842
+ const { request, featureGrants } = args;
2843
+ const requestId = request.requestId || createRequestId("req");
2844
+ const client = createClient();
2845
+ const model = request.model.modelId;
2846
+ const shouldStream = request.stream !== false;
2847
+ const messages = [];
2848
+ if (request.messages && Array.isArray(request.messages)) messages.push(...request.messages.map(toOpenAIMessage));
2849
+ else {
2850
+ if (typeof request.instructions === "string" && request.instructions.length > 0) messages.push({
2851
+ role: "system",
2852
+ content: request.instructions
2853
+ });
2854
+ messages.push({
2855
+ role: "user",
2856
+ content: request.input
2857
+ });
2858
+ }
2859
+ const reasoningEffort = (() => {
2860
+ const effort = request.reasoning?.effort;
2861
+ if (typeof effort !== "string") return void 0;
2862
+ if (effort === "none" || effort === "minimal" || effort === "low" || effort === "medium" || effort === "high" || effort === "xhigh") return effort;
2863
+ })();
2864
+ const body = {
2865
+ model,
2866
+ messages,
2867
+ stream: shouldStream,
2868
+ stream_options: shouldStream ? { include_usage: true } : void 0,
2869
+ metadata: request.metadata ?? void 0,
2870
+ reasoning_effort: reasoningEffort,
2871
+ max_completion_tokens: request.maxOutputTokens ?? void 0,
2872
+ temperature: request.temperature ?? void 0,
2873
+ top_p: request.topP ?? void 0,
2874
+ presence_penalty: request.presencePenalty ?? void 0,
2875
+ frequency_penalty: request.frequencyPenalty ?? void 0,
2876
+ seed: request.seed ?? void 0
2877
+ };
2878
+ if (request.tools && Array.isArray(request.tools) && request.tools.length > 0) body.tools = request.tools;
2879
+ try {
2880
+ yield {
2881
+ type: "response_start",
2882
+ requestId,
2883
+ model: toModelRef(request),
2884
+ featureGrants
2885
+ };
2886
+ if (!shouldStream) {
2887
+ const resp = await client.chat.completions.create({
2888
+ ...body,
2889
+ stream: false
2890
+ }, {
2891
+ signal: request.signal,
2892
+ timeout: request.timeoutMs
2893
+ });
2894
+ const choice = Array.isArray(resp?.choices) ? resp.choices[0] : void 0;
2895
+ const message = choice?.message;
2896
+ const reasoningContent = typeof message?.reasoning_content === "string" ? message.reasoning_content : "";
2897
+ if (reasoningContent.length > 0) {
2898
+ yield {
2899
+ type: "delta",
2900
+ requestId,
2901
+ chunk: { kind: "thinking_start" }
2902
+ };
2903
+ yield {
2904
+ type: "delta",
2905
+ requestId,
2906
+ chunk: {
2907
+ kind: "thinking_delta",
2908
+ text: reasoningContent
2909
+ }
2910
+ };
2911
+ yield {
2912
+ type: "delta",
2913
+ requestId,
2914
+ chunk: { kind: "thinking_end" }
2915
+ };
2916
+ }
2917
+ const text = typeof message?.content === "string" ? message.content : "";
2918
+ if (text.length > 0) yield {
2919
+ type: "delta",
2920
+ requestId,
2921
+ chunk: {
2922
+ kind: "text",
2923
+ text
2924
+ }
2925
+ };
2926
+ const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
2927
+ for (let i = 0; i < toolCalls.length; i++) {
2928
+ const tc = toolCalls[i];
2929
+ const callId = typeof tc?.id === "string" ? tc.id : `call_${i}`;
2930
+ const toolId = typeof tc?.function?.name === "string" ? tc.function.name : void 0;
2931
+ const args$1 = typeof tc?.function?.arguments === "string" ? tc.function.arguments : void 0;
2932
+ if (args$1 || toolId) yield {
2933
+ type: "delta",
2934
+ requestId,
2935
+ chunk: {
2936
+ kind: "tool_call_delta",
2937
+ callId,
2938
+ toolId,
2939
+ argsTextDelta: args$1
2940
+ }
2941
+ };
2942
+ }
2943
+ yield {
2944
+ type: "response_end",
2945
+ requestId,
2946
+ stopReason: toStopReason(choice?.finish_reason),
2947
+ usage: resp?.usage
2948
+ };
2949
+ return;
2950
+ }
2951
+ const stream$1 = await client.chat.completions.create({
2952
+ ...body,
2953
+ stream: true
2954
+ }, {
2955
+ signal: request.signal,
2956
+ timeout: request.timeoutMs
2957
+ });
2958
+ let finishReason = null;
2959
+ let lastUsage = void 0;
2960
+ const callIdByIndex = /* @__PURE__ */ new Map();
2961
+ let inThinkingPhase = false;
2962
+ for await (const chunk of stream$1) {
2963
+ if (chunk?.usage != null) lastUsage = chunk.usage;
2964
+ const choices = Array.isArray(chunk?.choices) ? chunk.choices : [];
2965
+ for (const choice of choices) {
2966
+ if (choice?.finish_reason != null) finishReason = choice.finish_reason;
2967
+ const delta = choice?.delta;
2968
+ const reasoningContent = delta?.reasoning_content;
2969
+ if (typeof reasoningContent === "string" && reasoningContent.length > 0) {
2970
+ if (!inThinkingPhase) {
2971
+ inThinkingPhase = true;
2972
+ yield {
2973
+ type: "delta",
2974
+ requestId,
2975
+ chunk: { kind: "thinking_start" }
2976
+ };
2977
+ }
2978
+ yield {
2979
+ type: "delta",
2980
+ requestId,
2981
+ chunk: {
2982
+ kind: "thinking_delta",
2983
+ text: reasoningContent
2984
+ }
2985
+ };
2986
+ }
2987
+ const content = delta?.content;
2988
+ if (typeof content === "string" && content.length > 0) {
2989
+ if (inThinkingPhase) {
2990
+ inThinkingPhase = false;
2991
+ yield {
2992
+ type: "delta",
2993
+ requestId,
2994
+ chunk: { kind: "thinking_end" }
2995
+ };
2996
+ }
2997
+ yield {
2998
+ type: "delta",
2999
+ requestId,
3000
+ chunk: {
3001
+ kind: "text",
3002
+ text: content
3003
+ }
3004
+ };
3005
+ }
3006
+ const toolCalls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : [];
3007
+ for (const tc of toolCalls) {
3008
+ const index = typeof tc?.index === "number" ? tc.index : 0;
3009
+ const callId = callIdByIndex.get(index) || (typeof tc?.id === "string" ? tc.id : `call_${index}`);
3010
+ callIdByIndex.set(index, callId);
3011
+ const toolId = typeof tc?.function?.name === "string" ? tc.function.name : void 0;
3012
+ const argsDelta = typeof tc?.function?.arguments === "string" ? tc.function.arguments : void 0;
3013
+ if (toolId || argsDelta) yield {
3014
+ type: "delta",
3015
+ requestId,
3016
+ chunk: {
3017
+ kind: "tool_call_delta",
3018
+ callId,
3019
+ toolId,
3020
+ argsTextDelta: argsDelta
3021
+ }
3022
+ };
3023
+ }
3024
+ }
3025
+ }
3026
+ if (inThinkingPhase) yield {
3027
+ type: "delta",
3028
+ requestId,
3029
+ chunk: { kind: "thinking_end" }
3030
+ };
3031
+ yield {
3032
+ type: "response_end",
3033
+ requestId,
3034
+ stopReason: toStopReason(finishReason),
3035
+ usage: lastUsage
3036
+ };
3037
+ } catch (err) {
3038
+ if (err?.name === "AbortError") throw new ModelError("Aborted", {
3039
+ code: "aborted",
3040
+ retryable: false
3041
+ });
3042
+ throw toModelError(err);
3043
+ }
3044
+ }
3045
+ return {
3046
+ provider: "openai",
3047
+ defaultModelId: options.defaultModelId,
3048
+ supportsFeature,
3049
+ stream
3050
+ };
3051
+ }
3052
+ function toStopReason(finishReason) {
3053
+ if (finishReason === "tool_calls" || finishReason === "function_call") return "tool_call";
3054
+ if (finishReason === "length") return "length";
3055
+ if (finishReason === "content_filter") return "error";
3056
+ if (finishReason === "stop" || finishReason == null) return "final";
3057
+ return "final";
3058
+ }
3059
+
3060
+ //#endregion
3061
+ //#region src/session/base.ts
3062
+ /**
3063
+ * Abstract base class for a session.
3064
+ *
3065
+ * A session represents a single conversation instance with an agent.
3066
+ * One agent can have multiple concurrent sessions.
3067
+ */
3068
+ var BaseSession = class {
3069
+ /**
3070
+ * Set model override for this session
3071
+ *
3072
+ * @param model - Model configuration to use for this session
3073
+ */
3074
+ setModelOverride(model) {
3075
+ if (!this.configOverride) this.configOverride = {};
3076
+ this.configOverride.model = model;
3077
+ this.updatedAt = Date.now();
3078
+ }
3079
+ /**
3080
+ * Clear model override (use agent's default model)
3081
+ */
3082
+ clearModelOverride() {
3083
+ if (this.configOverride) {
3084
+ delete this.configOverride.model;
3085
+ this.updatedAt = Date.now();
3086
+ }
3087
+ }
3088
+ /**
3089
+ * Set system prompt override for this session
3090
+ *
3091
+ * @param systemPrompt - System prompt to use for this session
3092
+ */
3093
+ setSystemPromptOverride(systemPrompt) {
3094
+ if (!this.configOverride) this.configOverride = {};
3095
+ this.configOverride.systemPromptOverride = systemPrompt;
3096
+ this.updatedAt = Date.now();
3097
+ }
3098
+ /**
3099
+ * Clear system prompt override
3100
+ */
3101
+ clearSystemPromptOverride() {
3102
+ if (this.configOverride) {
3103
+ delete this.configOverride.systemPromptOverride;
3104
+ this.updatedAt = Date.now();
3105
+ }
3106
+ }
3107
+ /**
3108
+ * Disable specific tools for this session
3109
+ *
3110
+ * @param toolNames - Names of tools to disable
3111
+ */
3112
+ disableTools(toolNames) {
3113
+ if (!this.configOverride) this.configOverride = {};
3114
+ this.configOverride.disabledTools = [...this.configOverride.disabledTools ?? [], ...toolNames];
3115
+ this.updatedAt = Date.now();
3116
+ }
3117
+ /**
3118
+ * Re-enable all tools for this session
3119
+ */
3120
+ enableAllTools() {
3121
+ if (this.configOverride) {
3122
+ delete this.configOverride.disabledTools;
3123
+ this.updatedAt = Date.now();
3124
+ }
3125
+ }
3126
+ /**
3127
+ * Update session status
3128
+ *
3129
+ * @param status - New status
3130
+ * @param errorMessage - Error message (if status is 'error')
3131
+ */
3132
+ setStatus(status, errorMessage) {
3133
+ this.status = status;
3134
+ this.errorMessage = errorMessage;
3135
+ this.updatedAt = Date.now();
3136
+ }
3137
+ /**
3138
+ * Mark session as active (update lastActiveAt)
3139
+ */
3140
+ markActive() {
3141
+ this.lastActiveAt = Date.now();
3142
+ this.updatedAt = Date.now();
3143
+ }
3144
+ /**
3145
+ * Add a message to the conversation history
3146
+ *
3147
+ * @param message - Message to add
3148
+ */
3149
+ addMessage(message) {
3150
+ this.messages.push(message);
3151
+ this.markActive();
3152
+ }
3153
+ /**
3154
+ * Get preview of the last message (truncated for display)
3155
+ *
3156
+ * @param maxLength - Maximum length of preview
3157
+ * @returns Preview string or undefined if no messages
3158
+ */
3159
+ getLastMessagePreview(maxLength = 100) {
3160
+ if (this.messages.length === 0) return void 0;
3161
+ const lastMessage = this.messages[this.messages.length - 1];
3162
+ const content = typeof lastMessage.content === "string" ? lastMessage.content : JSON.stringify(lastMessage.content);
3163
+ return content.length > maxLength ? `${content.substring(0, maxLength)}...` : content;
3164
+ }
3165
+ /**
3166
+ * Add usage statistics
3167
+ *
3168
+ * @param usage - Usage to add
3169
+ * @param usage.promptTokens - Number of prompt tokens
3170
+ * @param usage.completionTokens - Number of completion tokens
3171
+ * @param usage.totalTokens - Total number of tokens
3172
+ */
3173
+ addUsage(usage) {
3174
+ this.usage.promptTokens += usage.promptTokens;
3175
+ this.usage.completionTokens += usage.completionTokens;
3176
+ this.usage.totalTokens += usage.totalTokens;
3177
+ this.updatedAt = Date.now();
3178
+ }
3179
+ /**
3180
+ * Record a response with its duration
3181
+ *
3182
+ * @param durationMs - Response duration in milliseconds
3183
+ */
3184
+ recordResponse(durationMs) {
3185
+ const currentTotal = (this.avgResponseTime ?? 0) * this.responseCount;
3186
+ this.responseCount++;
3187
+ this.avgResponseTime = (currentTotal + durationMs) / this.responseCount;
3188
+ this.updatedAt = Date.now();
3189
+ }
3190
+ /**
3191
+ * Increment tool call count
3192
+ */
3193
+ incrementToolCallCount() {
3194
+ this.toolCallCount++;
3195
+ this.updatedAt = Date.now();
3196
+ }
3197
+ /**
3198
+ * Create a snapshot of the session for persistence
3199
+ *
3200
+ * @returns Session snapshot
3201
+ */
3202
+ toSnapshot() {
3203
+ const state = {
3204
+ status: this.status,
3205
+ updatedAt: this.updatedAt,
3206
+ lastActiveAt: this.lastActiveAt,
3207
+ title: this.title,
3208
+ errorMessage: this.errorMessage
3209
+ };
3210
+ const context = {
3211
+ messages: [...this.messages],
3212
+ messageCount: this.messages.length,
3213
+ lastMessagePreview: this.getLastMessagePreview(),
3214
+ toolCallCount: this.toolCallCount
3215
+ };
3216
+ const stats = {
3217
+ usage: { ...this.usage },
3218
+ responseCount: this.responseCount,
3219
+ avgResponseTime: this.avgResponseTime
3220
+ };
3221
+ return {
3222
+ id: this.id,
3223
+ agentId: this.agentId,
3224
+ createdAt: this.createdAt,
3225
+ state,
3226
+ configOverride: this.configOverride ? { ...this.configOverride } : void 0,
3227
+ context,
3228
+ stats,
3229
+ metadata: this.metadata ? { ...this.metadata } : void 0
3230
+ };
3231
+ }
3232
+ /**
3233
+ * Restore session state from a snapshot
3234
+ *
3235
+ * @param snapshot - Session snapshot to restore from
3236
+ */
3237
+ restoreFromSnapshot(snapshot) {
3238
+ this.status = snapshot.state.status;
3239
+ this.updatedAt = snapshot.state.updatedAt;
3240
+ this.lastActiveAt = snapshot.state.lastActiveAt;
3241
+ this.title = snapshot.state.title;
3242
+ this.errorMessage = snapshot.state.errorMessage;
3243
+ this.configOverride = snapshot.configOverride ? { ...snapshot.configOverride } : void 0;
3244
+ this.messages = [...snapshot.context.messages];
3245
+ this.toolCallCount = snapshot.context.toolCallCount;
3246
+ this.usage = { ...snapshot.stats.usage };
3247
+ this.responseCount = snapshot.stats.responseCount;
3248
+ this.avgResponseTime = snapshot.stats.avgResponseTime;
3249
+ this.metadata = snapshot.metadata ? { ...snapshot.metadata } : {};
3250
+ }
3251
+ };
3252
+
3253
+ //#endregion
3254
+ //#region src/session/manager.ts
3255
+ /**
3256
+ * Abstract base class for session management.
3257
+ *
3258
+ * Handles session lifecycle: creation, retrieval, listing, and destruction.
3259
+ * Implement this class for different storage backends (e.g., Redis, SQLite, in-memory).
3260
+ */
3261
+ var BaseSessionManager = class {};
3262
+
3263
+ //#endregion
3264
+ //#region src/tool/base.ts
3265
+ /**
3266
+ * Helper to create a text content block for CallToolResult
3267
+ */
3268
+ function textContent(text) {
3269
+ return { content: [{
3270
+ type: "text",
3271
+ text
3272
+ }] };
3273
+ }
3274
+ /**
3275
+ * Helper to create an error result for CallToolResult
3276
+ */
3277
+ function errorContent(error) {
3278
+ return {
3279
+ content: [{
3280
+ type: "text",
3281
+ text: error
3282
+ }],
3283
+ isError: true
3284
+ };
3285
+ }
3286
+ /**
3287
+ * Helper to create an image content block for CallToolResult
3288
+ */
3289
+ function imageContent(data, mimeType) {
3290
+ return { content: [{
3291
+ type: "image",
3292
+ data,
3293
+ mimeType
3294
+ }] };
3295
+ }
3296
+ /**
3297
+ * Abstract base class for tools.
3298
+ *
3299
+ * Tools are capabilities that the agent can invoke during execution.
3300
+ * Implement this class to create custom tools.
3301
+ *
3302
+ * All tool execute() methods must return MCP SDK-compliant CallToolResult:
3303
+ * - content: array of ContentBlock (TextContent, ImageContent, etc.)
3304
+ * - isError: optional boolean to indicate error
3305
+ * - structuredContent: optional structured data object
3306
+ */
3307
+ var BaseTool = class {
3308
+ /**
3309
+ * Risk level of the tool, used to determine if user approval is required.
3310
+ *
3311
+ * - 'safe': No risk, read-only operations (default)
3312
+ * - 'low': Low risk, minimal side effects
3313
+ * - 'medium': Medium risk, reversible changes
3314
+ * - 'high': High risk, file modifications
3315
+ * - 'critical': Critical risk, arbitrary command execution
3316
+ */
3317
+ riskLevel = "safe";
3318
+ };
3319
+
3320
+ //#endregion
3321
+ //#region src/tool/builtin/glob.ts
3322
+ /**
3323
+ * Maximum number of files to return
3324
+ */
3325
+ const MAX_FILES = 1e3;
3326
+ /**
3327
+ * Maximum depth for recursive search
3328
+ */
3329
+ const MAX_DEPTH = 20;
3330
+ /**
3331
+ * Convert glob pattern to RegExp
3332
+ *
3333
+ * Supports:
3334
+ * - `*` matches any sequence of characters except `/`
3335
+ * - `**` matches any sequence of characters including `/`
3336
+ * - `?` matches any single character except `/`
3337
+ * - `{a,b}` matches either `a` or `b`
3338
+ * - `[abc]` matches any character in the brackets
3339
+ */
3340
+ function globToRegex(pattern) {
3341
+ let regexStr = "";
3342
+ let i = 0;
3343
+ while (i < pattern.length) {
3344
+ const char = pattern[i];
3345
+ if (char === "*") if (pattern[i + 1] === "*") if (pattern[i + 2] === "/") {
3346
+ regexStr += "(?:.*\\/)?";
3347
+ i += 3;
3348
+ } else {
3349
+ regexStr += ".*";
3350
+ i += 2;
3351
+ }
3352
+ else {
3353
+ regexStr += "[^/]*";
3354
+ i++;
3355
+ }
3356
+ else if (char === "?") {
3357
+ regexStr += "[^/]";
3358
+ i++;
3359
+ } else if (char === "{") {
3360
+ const end = pattern.indexOf("}", i);
3361
+ if (end !== -1) {
3362
+ const options = pattern.slice(i + 1, end).split(",");
3363
+ regexStr += `(?:${options.map((o) => escapeRegex(o)).join("|")})`;
3364
+ i = end + 1;
3365
+ } else {
3366
+ regexStr += "\\{";
3367
+ i++;
3368
+ }
3369
+ } else if (char === "[") {
3370
+ const end = pattern.indexOf("]", i);
3371
+ if (end !== -1) {
3372
+ regexStr += pattern.slice(i, end + 1);
3373
+ i = end + 1;
3374
+ } else {
3375
+ regexStr += "\\[";
3376
+ i++;
3377
+ }
3378
+ } else if (char === ".") {
3379
+ regexStr += "\\.";
3380
+ i++;
3381
+ } else if (char === "/") {
3382
+ regexStr += "\\/";
3383
+ i++;
3384
+ } else if ("()[]{}^$+|\\".includes(char)) {
3385
+ regexStr += `\\${char}`;
3386
+ i++;
3387
+ } else {
3388
+ regexStr += char;
3389
+ i++;
3390
+ }
3391
+ }
3392
+ return /* @__PURE__ */ new RegExp(`^${regexStr}$`);
3393
+ }
3394
+ /**
3395
+ * Escape special regex characters
3396
+ */
3397
+ function escapeRegex(str) {
3398
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3399
+ }
3400
+ /**
3401
+ * Tool for finding files matching glob patterns.
3402
+ *
3403
+ * This tool provides fast file pattern matching that works with any codebase size,
3404
+ * returning matching file paths sorted by modification time.
3405
+ *
3406
+ * @example
3407
+ * ```typescript
3408
+ * const globTool = new GlobTool()
3409
+ * const result = await globTool.execute({
3410
+ * pattern: '**\/*.ts',
3411
+ * path: './src'
3412
+ * })
3413
+ * ```
3414
+ */
3415
+ var GlobTool = class extends BaseTool {
3416
+ name = "Glob";
3417
+ description = `Fast file pattern matching tool that works with any codebase size.
3418
+
3419
+ Usage notes:
3420
+ - Supports glob patterns like "**/*.js" or "src/**/*.ts"
3421
+ - Returns matching file paths sorted by modification time (newest first)
3422
+ - Use this tool when you need to find files by name patterns
3423
+ - You can call multiple tools in a single response for parallel searches
3424
+
3425
+ Supported patterns:
3426
+ - \`*\` matches any sequence of characters except path separator
3427
+ - \`**\` matches any sequence of characters including path separator
3428
+ - \`?\` matches any single character
3429
+ - \`{a,b}\` matches either a or b
3430
+ - \`[abc]\` matches any character in brackets`;
3431
+ parameters = {
3432
+ type: "object",
3433
+ properties: {
3434
+ pattern: {
3435
+ type: "string",
3436
+ description: "The glob pattern to match files against"
3437
+ },
3438
+ path: {
3439
+ type: "string",
3440
+ description: "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior."
3441
+ }
3442
+ },
3443
+ required: ["pattern"]
3444
+ };
3445
+ /** Current working directory for search */
3446
+ cwd;
3447
+ constructor(options) {
3448
+ super();
3449
+ this.cwd = options?.cwd ?? process.cwd();
3450
+ }
3451
+ /**
3452
+ * Set the current working directory
3453
+ */
3454
+ setCwd(cwd) {
3455
+ this.cwd = cwd;
3456
+ }
3457
+ /**
3458
+ * Get the current working directory
3459
+ */
3460
+ getCwd() {
3461
+ return this.cwd;
3462
+ }
3463
+ /**
3464
+ * Execute glob pattern matching
3465
+ *
3466
+ * @param args - Glob arguments
3467
+ * @returns MCP-compliant CallToolResult with matching files
3468
+ */
3469
+ async execute(args) {
3470
+ const { pattern, path: searchPath } = this.validateArgs(args);
3471
+ const baseDir = searchPath ? path.isAbsolute(searchPath) ? searchPath : path.resolve(this.cwd, searchPath) : this.cwd;
3472
+ try {
3473
+ if (!(await stat(baseDir)).isDirectory()) return errorContent(`Path is not a directory: ${baseDir}`);
3474
+ } catch (error) {
3475
+ if (error.code === "ENOENT") return errorContent(`Directory not found: ${baseDir}`);
3476
+ throw error;
3477
+ }
3478
+ let normalizedPattern = pattern;
3479
+ if (!pattern.startsWith("**/") && !pattern.startsWith("/") && !pattern.startsWith("./")) normalizedPattern = `**/${pattern}`;
3480
+ const regex = globToRegex(normalizedPattern);
3481
+ const files = [];
3482
+ await this.walkDirectory(baseDir, "", regex, files, 0);
3483
+ files.sort((a, b) => b.mtime - a.mtime);
3484
+ const truncated = files.length > MAX_FILES;
3485
+ const resultFiles = files.slice(0, MAX_FILES).map((f) => f.path);
3486
+ const result = {
3487
+ files: resultFiles,
3488
+ totalMatches: files.length,
3489
+ truncated
3490
+ };
3491
+ return {
3492
+ content: [{
3493
+ type: "text",
3494
+ text: resultFiles.length > 0 ? `Found ${files.length} file${files.length !== 1 ? "s" : ""}${truncated ? ` (showing first ${MAX_FILES})` : ""}:\n${resultFiles.join("\n")}` : `No files found matching pattern: ${pattern}`
3495
+ }],
3496
+ structuredContent: result
3497
+ };
3498
+ }
3499
+ /**
3500
+ * Validate and parse arguments
3501
+ */
3502
+ validateArgs(args) {
3503
+ const pattern = args.pattern;
3504
+ if (typeof pattern !== "string" || !pattern.trim()) throw new Error("Pattern is required and must be a non-empty string");
3505
+ let searchPath;
3506
+ if (args.path !== void 0 && args.path !== null && args.path !== "undefined" && args.path !== "null") {
3507
+ if (typeof args.path !== "string") throw new TypeError("Path must be a string");
3508
+ searchPath = args.path.trim() || void 0;
3509
+ }
3510
+ return {
3511
+ pattern: pattern.trim(),
3512
+ path: searchPath
3513
+ };
3514
+ }
3515
+ /**
3516
+ * Recursively walk directory and collect matching files
3517
+ */
3518
+ async walkDirectory(baseDir, relativePath, pattern, files, depth) {
3519
+ if (depth > MAX_DEPTH) return;
3520
+ const currentDir = relativePath ? path.join(baseDir, relativePath) : baseDir;
3521
+ let entries;
3522
+ try {
3523
+ entries = await readdir(currentDir, { withFileTypes: true });
3524
+ } catch {
3525
+ return;
3526
+ }
3527
+ for (const entry of entries) {
3528
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3529
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
3530
+ if (entry.isDirectory()) await this.walkDirectory(baseDir, entryRelativePath, pattern, files, depth + 1);
3531
+ else if (entry.isFile()) {
3532
+ if (pattern.test(entryRelativePath)) try {
3533
+ const stats = await stat(path.join(currentDir, entry.name));
3534
+ files.push({
3535
+ path: entryRelativePath,
3536
+ mtime: stats.mtimeMs
3537
+ });
3538
+ } catch {}
3539
+ }
3540
+ }
3541
+ }
3542
+ };
3543
+
3544
+ //#endregion
3545
+ //#region src/tool/builtin/grep.ts
3546
+ /**
3547
+ * Maximum output length before truncation (in characters)
3548
+ */
3549
+ const MAX_OUTPUT_LENGTH = 5e4;
3550
+ /**
3551
+ * Default command timeout (60 seconds)
3552
+ */
3553
+ const DEFAULT_TIMEOUT = 6e4;
3554
+ /**
3555
+ * Tool for searching files using ripgrep.
3556
+ *
3557
+ * A powerful search tool built on ripgrep that supports regex patterns,
3558
+ * multiple output modes, and various filtering options.
3559
+ *
3560
+ * @example
3561
+ * ```typescript
3562
+ * const grepTool = new GrepTool()
3563
+ * const result = await grepTool.execute({
3564
+ * pattern: 'function\\s+\\w+',
3565
+ * path: './src',
3566
+ * type: 'ts'
3567
+ * })
3568
+ * ```
3569
+ */
3570
+ var GrepTool = class extends BaseTool {
3571
+ name = "Grep";
3572
+ description = `A powerful search tool built on ripgrep.
3573
+
3574
+ Usage notes:
3575
+ - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
3576
+ - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
3577
+ - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
3578
+ - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
3579
+ - Multiline matching: By default patterns match within single lines only. For cross-line patterns, use multiline: true`;
3580
+ parameters = {
3581
+ type: "object",
3582
+ properties: {
3583
+ pattern: {
3584
+ type: "string",
3585
+ description: "The regular expression pattern to search for in file contents"
3586
+ },
3587
+ path: {
3588
+ type: "string",
3589
+ description: "File or directory to search in. Defaults to current working directory."
3590
+ },
3591
+ glob: {
3592
+ type: "string",
3593
+ description: "Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\")"
3594
+ },
3595
+ output_mode: {
3596
+ type: "string",
3597
+ enum: [
3598
+ "content",
3599
+ "files_with_matches",
3600
+ "count"
3601
+ ],
3602
+ description: "Output mode: \"content\" shows matching lines, \"files_with_matches\" shows file paths (default), \"count\" shows match counts."
3603
+ },
3604
+ "-B": {
3605
+ type: "number",
3606
+ description: "Number of lines to show before each match. Requires output_mode: \"content\"."
3607
+ },
3608
+ "-A": {
3609
+ type: "number",
3610
+ description: "Number of lines to show after each match. Requires output_mode: \"content\"."
3611
+ },
3612
+ "-C": {
3613
+ type: "number",
3614
+ description: "Number of lines to show before and after each match. Requires output_mode: \"content\"."
3615
+ },
3616
+ "-n": {
3617
+ type: "boolean",
3618
+ description: "Show line numbers in output. Requires output_mode: \"content\". Defaults to true."
3619
+ },
3620
+ "-i": {
3621
+ type: "boolean",
3622
+ description: "Case insensitive search"
3623
+ },
3624
+ type: {
3625
+ type: "string",
3626
+ description: "File type to search (e.g., js, py, rust, go, java). More efficient than glob for standard file types."
3627
+ },
3628
+ head_limit: {
3629
+ type: "number",
3630
+ description: "Limit output to first N lines/entries. Defaults to 0 (unlimited)."
3631
+ },
3632
+ offset: {
3633
+ type: "number",
3634
+ description: "Skip first N lines/entries before applying head_limit. Defaults to 0."
3635
+ },
3636
+ multiline: {
3637
+ type: "boolean",
3638
+ description: "Enable multiline mode where . matches newlines and patterns can span lines. Default: false."
3639
+ }
3640
+ },
3641
+ required: ["pattern"]
3642
+ };
3643
+ /** Current working directory for search */
3644
+ cwd;
3645
+ /** Path to ripgrep binary */
3646
+ rgPath;
3647
+ constructor(options) {
3648
+ super();
3649
+ this.cwd = options?.cwd ?? process.cwd();
3650
+ this.rgPath = options?.rgPath ?? "rg";
3651
+ }
3652
+ /**
3653
+ * Set the current working directory
3654
+ */
3655
+ setCwd(cwd) {
3656
+ this.cwd = cwd;
3657
+ }
3658
+ /**
3659
+ * Get the current working directory
3660
+ */
3661
+ getCwd() {
3662
+ return this.cwd;
3663
+ }
3664
+ /**
3665
+ * Execute grep search
3666
+ *
3667
+ * @param args - Grep arguments
3668
+ * @returns MCP-compliant CallToolResult with search results
3669
+ */
3670
+ async execute(args) {
3671
+ const validatedArgs = this.validateArgs(args);
3672
+ const rgArgs = this.buildRgArgs(validatedArgs);
3673
+ const result = await this.runRipgrep(rgArgs, validatedArgs);
3674
+ let text = result.output || "[No matches found]";
3675
+ if (result.timedOut) text += "\n[Search timed out]";
3676
+ if (result.matchCount !== void 0) text = `Found ${result.matchCount} match${result.matchCount !== 1 ? "es" : ""}\n${text}`;
3677
+ return {
3678
+ content: [{
3679
+ type: "text",
3680
+ text
3681
+ }],
3682
+ structuredContent: result,
3683
+ isError: result.exitCode !== 0 && result.exitCode !== 1
3684
+ };
3685
+ }
3686
+ /**
3687
+ * Validate and parse arguments
3688
+ */
3689
+ validateArgs(args) {
3690
+ const pattern = args.pattern;
3691
+ if (typeof pattern !== "string" || !pattern.trim()) throw new Error("Pattern is required and must be a non-empty string");
3692
+ const result = { pattern: pattern.trim() };
3693
+ if (args.path !== void 0 && args.path !== null && args.path !== "") {
3694
+ if (typeof args.path !== "string") throw new TypeError("Path must be a string");
3695
+ result.path = args.path.trim();
3696
+ }
3697
+ if (args.glob !== void 0 && args.glob !== null && args.glob !== "") {
3698
+ if (typeof args.glob !== "string") throw new TypeError("Glob must be a string");
3699
+ result.glob = args.glob.trim();
3700
+ }
3701
+ if (args.type !== void 0 && args.type !== null && args.type !== "") {
3702
+ if (typeof args.type !== "string") throw new TypeError("Type must be a string");
3703
+ result.type = args.type.trim();
3704
+ }
3705
+ if (args.output_mode !== void 0) {
3706
+ const validModes = [
3707
+ "content",
3708
+ "files_with_matches",
3709
+ "count"
3710
+ ];
3711
+ if (!validModes.includes(args.output_mode)) throw new Error(`Invalid output_mode. Must be one of: ${validModes.join(", ")}`);
3712
+ result.output_mode = args.output_mode;
3713
+ }
3714
+ for (const arg of [
3715
+ "-B",
3716
+ "-A",
3717
+ "-C",
3718
+ "head_limit",
3719
+ "offset"
3720
+ ]) if (args[arg] !== void 0 && args[arg] !== null) {
3721
+ if (typeof args[arg] !== "number") throw new TypeError(`${arg} must be a number`);
3722
+ result[arg] = Math.max(0, Math.floor(args[arg]));
3723
+ }
3724
+ for (const arg of [
3725
+ "-n",
3726
+ "-i",
3727
+ "multiline"
3728
+ ]) if (args[arg] !== void 0 && args[arg] !== null) result[arg] = Boolean(args[arg]);
3729
+ return result;
3730
+ }
3731
+ /**
3732
+ * Build ripgrep command arguments
3733
+ */
3734
+ buildRgArgs(args) {
3735
+ const rgArgs = [];
3736
+ const outputMode = args.output_mode ?? "files_with_matches";
3737
+ if (outputMode === "files_with_matches") rgArgs.push("-l");
3738
+ else if (outputMode === "count") rgArgs.push("-c");
3739
+ if (outputMode === "content") {
3740
+ if (args["-n"] !== false) rgArgs.push("-n");
3741
+ if (args["-B"] !== void 0 && args["-B"] > 0) rgArgs.push("-B", String(args["-B"]));
3742
+ if (args["-A"] !== void 0 && args["-A"] > 0) rgArgs.push("-A", String(args["-A"]));
3743
+ if (args["-C"] !== void 0 && args["-C"] > 0) rgArgs.push("-C", String(args["-C"]));
3744
+ }
3745
+ if (args["-i"]) rgArgs.push("-i");
3746
+ if (args.multiline) rgArgs.push("-U", "--multiline-dotall");
3747
+ if (args.type) rgArgs.push("--type", args.type);
3748
+ if (args.glob) rgArgs.push("--glob", args.glob);
3749
+ rgArgs.push("--color", "never");
3750
+ rgArgs.push("--no-heading");
3751
+ rgArgs.push("--regexp", args.pattern);
3752
+ if (args.path) rgArgs.push("--", args.path);
3753
+ return rgArgs;
3754
+ }
3755
+ /**
3756
+ * Run ripgrep command
3757
+ */
3758
+ runRipgrep(rgArgs, args) {
3759
+ return new Promise((resolve) => {
3760
+ let output = "";
3761
+ let timedOut = false;
3762
+ let truncated = false;
3763
+ const child = spawn(this.rgPath, rgArgs, {
3764
+ cwd: this.cwd,
3765
+ env: process.env,
3766
+ stdio: [
3767
+ "pipe",
3768
+ "pipe",
3769
+ "pipe"
3770
+ ]
3771
+ });
3772
+ const timeoutId = setTimeout(() => {
3773
+ timedOut = true;
3774
+ child.kill("SIGTERM");
3775
+ setTimeout(() => {
3776
+ if (!child.killed) child.kill("SIGKILL");
3777
+ }, 5e3);
3778
+ }, DEFAULT_TIMEOUT);
3779
+ child.stdout?.on("data", (data) => {
3780
+ const str = data.toString();
3781
+ if (output.length + str.length > MAX_OUTPUT_LENGTH) {
3782
+ output += str.slice(0, MAX_OUTPUT_LENGTH - output.length);
3783
+ truncated = true;
3784
+ } else output += str;
3785
+ });
3786
+ child.stderr?.on("data", (data) => {
3787
+ const str = data.toString();
3788
+ if (str.includes("error:")) output += `\n[stderr]: ${str}`;
3789
+ });
3790
+ child.on("close", (code) => {
3791
+ clearTimeout(timeoutId);
3792
+ let finalOutput = output;
3793
+ if (args.offset || args.head_limit) {
3794
+ const lines = output.split("\n").filter((line) => line.trim());
3795
+ const offset = args.offset ?? 0;
3796
+ const limit = args.head_limit ?? lines.length;
3797
+ finalOutput = lines.slice(offset, offset + limit).join("\n");
3798
+ }
3799
+ let matchCount;
3800
+ if (args.output_mode === "count") matchCount = finalOutput.split("\n").filter((line) => line.trim()).reduce((sum, line) => {
3801
+ const match = line.match(/:(\d+)$/);
3802
+ return sum + (match ? Number.parseInt(match[1], 10) : 0);
3803
+ }, 0);
3804
+ resolve({
3805
+ exitCode: code,
3806
+ output: truncated ? `${finalOutput}\n... [output truncated]` : finalOutput,
3807
+ truncated,
3808
+ timedOut,
3809
+ matchCount
3810
+ });
3811
+ });
3812
+ child.on("error", (error) => {
3813
+ clearTimeout(timeoutId);
3814
+ if (error?.code === "ENOENT") {
3815
+ this.runSystemGrep(args).then(resolve);
3816
+ return;
3817
+ }
3818
+ resolve({
3819
+ exitCode: 1,
3820
+ engine: "rg",
3821
+ output: `Failed to execute ripgrep: ${error.message}. Make sure 'rg' is installed and in PATH.`,
3822
+ truncated: false,
3823
+ timedOut: false
3824
+ });
3825
+ });
3826
+ });
3827
+ }
3828
+ runSystemGrep(args) {
3829
+ return new Promise((resolve) => {
3830
+ let output = "";
3831
+ let timedOut = false;
3832
+ let truncated = false;
3833
+ const grepArgs = [];
3834
+ grepArgs.push("-R", "-E", "-I");
3835
+ const mode = args.output_mode ?? "files_with_matches";
3836
+ if (mode === "files_with_matches") grepArgs.push("-l");
3837
+ else if (mode === "count") grepArgs.push("-c");
3838
+ else grepArgs.push("-n");
3839
+ if (mode === "content") {
3840
+ if (args["-B"] !== void 0 && args["-B"] > 0) grepArgs.push("-B", String(args["-B"]));
3841
+ if (args["-A"] !== void 0 && args["-A"] > 0) grepArgs.push("-A", String(args["-A"]));
3842
+ if (args["-C"] !== void 0 && args["-C"] > 0) grepArgs.push("-C", String(args["-C"]));
3843
+ }
3844
+ if (args["-i"]) grepArgs.push("-i");
3845
+ grepArgs.push(args.pattern);
3846
+ if (args.path) grepArgs.push(args.path);
3847
+ else grepArgs.push(".");
3848
+ const child = spawn("grep", grepArgs, {
3849
+ cwd: this.cwd,
3850
+ env: process.env,
3851
+ stdio: [
3852
+ "pipe",
3853
+ "pipe",
3854
+ "pipe"
3855
+ ]
3856
+ });
3857
+ const timeoutId = setTimeout(() => {
3858
+ timedOut = true;
3859
+ child.kill("SIGTERM");
3860
+ setTimeout(() => {
3861
+ if (!child.killed) child.kill("SIGKILL");
3862
+ }, 5e3);
3863
+ }, DEFAULT_TIMEOUT);
3864
+ child.stdout?.on("data", (data) => {
3865
+ const str = data.toString();
3866
+ if (output.length + str.length > MAX_OUTPUT_LENGTH) {
3867
+ output += str.slice(0, MAX_OUTPUT_LENGTH - output.length);
3868
+ truncated = true;
3869
+ } else output += str;
3870
+ });
3871
+ child.stderr?.on("data", (data) => {
3872
+ const str = data.toString();
3873
+ if (str.trim()) output += `\n[stderr]: ${str}`;
3874
+ });
3875
+ child.on("close", (code) => {
3876
+ clearTimeout(timeoutId);
3877
+ let finalOutput = output;
3878
+ if (args.offset || args.head_limit) {
3879
+ const lines = output.split("\n").filter((line) => line.trim());
3880
+ const offset = args.offset ?? 0;
3881
+ const limit = args.head_limit ?? lines.length;
3882
+ finalOutput = lines.slice(offset, offset + limit).join("\n");
3883
+ }
3884
+ let matchCount;
3885
+ if (mode === "count") matchCount = finalOutput.split("\n").filter((line) => line.trim()).reduce((sum, line) => {
3886
+ const last = line.split(":").pop();
3887
+ const n = last ? Number.parseInt(last, 10) : NaN;
3888
+ return sum + (Number.isFinite(n) ? n : 0);
3889
+ }, 0);
3890
+ resolve({
3891
+ exitCode: code,
3892
+ engine: "grep",
3893
+ output: truncated ? `${finalOutput}\n... [output truncated]` : finalOutput,
3894
+ truncated,
3895
+ timedOut,
3896
+ matchCount
3897
+ });
3898
+ });
3899
+ child.on("error", (error) => {
3900
+ clearTimeout(timeoutId);
3901
+ resolve({
3902
+ exitCode: 1,
3903
+ engine: "grep",
3904
+ output: `Failed to execute grep: ${error.message}.`,
3905
+ truncated: false,
3906
+ timedOut: false
3907
+ });
3908
+ });
3909
+ });
3910
+ }
3911
+ };
3912
+
3913
+ //#endregion
3914
+ //#region src/tool/builtin/read.ts
3915
+ /**
3916
+ * Default maximum number of lines to read
3917
+ */
3918
+ const DEFAULT_LINE_LIMIT = 2e3;
3919
+ /**
3920
+ * Maximum characters per line before truncation
3921
+ */
3922
+ const MAX_LINE_LENGTH = 2e3;
3923
+ /**
3924
+ * Known binary/image file extensions
3925
+ */
3926
+ const BINARY_EXTENSIONS = new Set([
3927
+ ".png",
3928
+ ".jpg",
3929
+ ".jpeg",
3930
+ ".gif",
3931
+ ".bmp",
3932
+ ".ico",
3933
+ ".webp",
3934
+ ".svg",
3935
+ ".pdf",
3936
+ ".zip",
3937
+ ".tar",
3938
+ ".gz",
3939
+ ".rar",
3940
+ ".7z",
3941
+ ".exe",
3942
+ ".dll",
3943
+ ".so",
3944
+ ".dylib",
3945
+ ".mp3",
3946
+ ".mp4",
3947
+ ".avi",
3948
+ ".mov",
3949
+ ".wav",
3950
+ ".woff",
3951
+ ".woff2",
3952
+ ".ttf",
3953
+ ".eot"
3954
+ ]);
3955
+ /**
3956
+ * MIME types for common file extensions
3957
+ */
3958
+ const MIME_TYPES = {
3959
+ ".png": "image/png",
3960
+ ".jpg": "image/jpeg",
3961
+ ".jpeg": "image/jpeg",
3962
+ ".gif": "image/gif",
3963
+ ".webp": "image/webp",
3964
+ ".svg": "image/svg+xml",
3965
+ ".pdf": "application/pdf",
3966
+ ".json": "application/json",
3967
+ ".js": "text/javascript",
3968
+ ".ts": "text/typescript",
3969
+ ".html": "text/html",
3970
+ ".css": "text/css",
3971
+ ".md": "text/markdown",
3972
+ ".txt": "text/plain"
3973
+ };
3974
+ /**
3975
+ * Tool for reading files from the filesystem.
3976
+ *
3977
+ * This tool reads files with line number formatting, supporting
3978
+ * offset/limit for large files and detecting binary content.
3979
+ *
3980
+ * @example
3981
+ * ```typescript
3982
+ * const readTool = new ReadTool()
3983
+ * const result = await readTool.execute({
3984
+ * file_path: '/path/to/file.ts',
3985
+ * offset: 100,
3986
+ * limit: 50
3987
+ * })
3988
+ * ```
3989
+ *
3990
+ * @example Restrict reads to a specific directory
3991
+ * ```typescript
3992
+ * const readTool = new ReadTool({
3993
+ * cwd: '/app/output',
3994
+ * restrictToDirectory: true
3995
+ * })
3996
+ * // All paths will be resolved relative to /app/output
3997
+ * // Absolute paths and path traversal (../) will be blocked
3998
+ * ```
3999
+ */
4000
+ var ReadTool = class extends BaseTool {
4001
+ name = "Read";
4002
+ /** Current working directory for resolving relative paths */
4003
+ _cwd;
4004
+ /** Allowed directory for file operations (if set, restricts reads to this directory) */
4005
+ _allowedDirectory;
4006
+ constructor(options) {
4007
+ super();
4008
+ this._cwd = options?.cwd ?? process.cwd();
4009
+ this._allowedDirectory = options?.allowedDirectory;
4010
+ }
4011
+ /**
4012
+ * Dynamic description that includes allowed directory info if configured
4013
+ */
4014
+ get description() {
4015
+ const baseDescription = `Reads a file from the local filesystem. You can access any file directly by using this tool.
4016
+
4017
+ Usage notes:
4018
+ - The file_path parameter must be an absolute path, not a relative path
4019
+ - By default, it reads up to ${DEFAULT_LINE_LIMIT} lines starting from the beginning
4020
+ - You can optionally specify a line offset and limit for large files
4021
+ - Any lines longer than ${MAX_LINE_LENGTH} characters will be truncated
4022
+ - Results are returned with line numbers (like cat -n format)
4023
+ - Can read images (PNG, JPG, etc.), PDFs, and Jupyter notebooks
4024
+ - You can call multiple tools in parallel to read multiple files at once`;
4025
+ if (this._allowedDirectory) return `${baseDescription}
4026
+ - IMPORTANT: Files can ONLY be read from within: ${this._allowedDirectory}
4027
+ - Use absolute paths starting with ${this._allowedDirectory}/ (e.g., ${this._allowedDirectory}/filename.html)`;
4028
+ return baseDescription;
4029
+ }
4030
+ /**
4031
+ * Dynamic parameters that include allowed directory info if configured
4032
+ */
4033
+ get parameters() {
4034
+ return {
4035
+ type: "object",
4036
+ properties: {
4037
+ file_path: {
4038
+ type: "string",
4039
+ description: this._allowedDirectory ? `The absolute path to the file to read (must be within ${this._allowedDirectory})` : "The absolute path to the file to read"
4040
+ },
4041
+ offset: {
4042
+ type: "number",
4043
+ description: "The line number to start reading from (1-based). Only provide if the file is too large to read at once."
4044
+ },
4045
+ limit: {
4046
+ type: "number",
4047
+ description: "The number of lines to read. Only provide if the file is too large to read at once."
4048
+ }
4049
+ },
4050
+ required: ["file_path"]
4051
+ };
4052
+ }
4053
+ /**
4054
+ * Set the current working directory
4055
+ */
4056
+ setCwd(cwd) {
4057
+ this._cwd = cwd;
4058
+ }
4059
+ /**
4060
+ * Get the current working directory
4061
+ */
4062
+ getCwd() {
4063
+ return this._cwd;
4064
+ }
4065
+ /**
4066
+ * Set the allowed directory for file operations
4067
+ */
4068
+ setAllowedDirectory(dir) {
4069
+ this._allowedDirectory = dir;
4070
+ }
4071
+ /**
4072
+ * Get the allowed directory for file operations
4073
+ */
4074
+ getAllowedDirectory() {
4075
+ return this._allowedDirectory;
4076
+ }
4077
+ /**
4078
+ * Execute file read
4079
+ *
4080
+ * @param args - Read arguments
4081
+ * @returns MCP-compliant CallToolResult with file content
4082
+ */
4083
+ async execute(args) {
4084
+ const { file_path, offset, limit } = this.validateArgs(args);
4085
+ const filePath = path.isAbsolute(file_path) ? file_path : path.resolve(this._cwd, file_path);
4086
+ if (this._allowedDirectory) {
4087
+ const normalizedAllowed = path.resolve(this._allowedDirectory);
4088
+ const normalizedFilePath = path.resolve(filePath);
4089
+ if (!normalizedFilePath.startsWith(normalizedAllowed + path.sep) && normalizedFilePath !== normalizedAllowed) return errorContent(`Access denied: ${file_path}\nFiles can only be read from within: ${this._allowedDirectory}\nPlease use a path like: ${this._allowedDirectory}/<filename>`);
4090
+ }
4091
+ let stats;
4092
+ try {
4093
+ stats = await stat(filePath);
4094
+ } catch (error) {
4095
+ if (error.code === "ENOENT") return errorContent(`File not found: ${filePath}`);
4096
+ throw error;
4097
+ }
4098
+ if (stats.isDirectory()) return errorContent(`Path is a directory, not a file: ${filePath}. Use ls command via Bash tool to read directories.`);
4099
+ const ext = path.extname(filePath).toLowerCase();
4100
+ const isBinary = BINARY_EXTENSIONS.has(ext);
4101
+ const mimeType = MIME_TYPES[ext];
4102
+ let result;
4103
+ if (isBinary) {
4104
+ result = await this.handleBinaryFile(filePath, stats.size, mimeType);
4105
+ if (mimeType?.startsWith("image/")) return {
4106
+ content: [{
4107
+ type: "image",
4108
+ data: (await readFile(filePath)).toString("base64"),
4109
+ mimeType
4110
+ }],
4111
+ structuredContent: result
4112
+ };
4113
+ } else if (ext === ".ipynb") result = await this.handleJupyterNotebook(filePath, stats.size, offset, limit);
4114
+ else result = await this.handleTextFile(filePath, stats.size, offset, limit);
4115
+ return {
4116
+ content: [{
4117
+ type: "text",
4118
+ text: result.content
4119
+ }],
4120
+ structuredContent: result
4121
+ };
4122
+ }
4123
+ /**
4124
+ * Validate and parse arguments
4125
+ */
4126
+ validateArgs(args) {
4127
+ const filePath = args.file_path;
4128
+ if (typeof filePath !== "string" || !filePath.trim()) throw new Error("file_path is required and must be a non-empty string");
4129
+ const result = { file_path: filePath.trim() };
4130
+ if (args.offset !== void 0 && args.offset !== null) {
4131
+ if (typeof args.offset !== "number") throw new TypeError("offset must be a number");
4132
+ result.offset = Math.max(1, Math.floor(args.offset));
4133
+ }
4134
+ if (args.limit !== void 0 && args.limit !== null) {
4135
+ if (typeof args.limit !== "number") throw new TypeError("limit must be a number");
4136
+ result.limit = Math.max(1, Math.floor(args.limit));
4137
+ }
4138
+ return result;
4139
+ }
4140
+ /**
4141
+ * Handle binary file (images, PDFs, etc.)
4142
+ */
4143
+ async handleBinaryFile(filePath, fileSize, mimeType) {
4144
+ const base64 = (await readFile(filePath)).toString("base64");
4145
+ return {
4146
+ content: `[Binary file: ${path.basename(filePath)}]\nSize: ${this.formatSize(fileSize)}\nMIME type: ${mimeType ?? "unknown"}\nBase64 encoded content:\n${base64}`,
4147
+ totalLines: 1,
4148
+ linesReturned: 1,
4149
+ startLine: 1,
4150
+ truncated: false,
4151
+ fileSize,
4152
+ isBinary: true,
4153
+ mimeType
4154
+ };
4155
+ }
4156
+ /**
4157
+ * Handle Jupyter notebook file
4158
+ */
4159
+ async handleJupyterNotebook(filePath, fileSize, offset, limit) {
4160
+ const content = await readFile(filePath, "utf-8");
4161
+ let notebook;
4162
+ try {
4163
+ notebook = JSON.parse(content);
4164
+ } catch {
4165
+ throw new Error(`Invalid Jupyter notebook format: ${filePath}`);
4166
+ }
4167
+ const cells = notebook.cells || [];
4168
+ const outputLines = [];
4169
+ for (let i = 0; i < cells.length; i++) {
4170
+ const cell = cells[i];
4171
+ const cellNum = i + 1;
4172
+ const cellType = cell.cell_type || "unknown";
4173
+ outputLines.push(`--- Cell ${cellNum} (${cellType}) ---`);
4174
+ const source = Array.isArray(cell.source) ? cell.source.join("") : cell.source || "";
4175
+ outputLines.push(...source.split("\n"));
4176
+ if (cell.outputs && cell.outputs.length > 0) {
4177
+ outputLines.push("--- Output ---");
4178
+ for (const output of cell.outputs) if (output.text) {
4179
+ const text = Array.isArray(output.text) ? output.text.join("") : output.text;
4180
+ outputLines.push(...text.split("\n"));
4181
+ } else if (output.data) {
4182
+ if (output.data["text/plain"]) {
4183
+ const text = Array.isArray(output.data["text/plain"]) ? output.data["text/plain"].join("") : output.data["text/plain"];
4184
+ outputLines.push(...text.split("\n"));
4185
+ }
4186
+ }
4187
+ }
4188
+ outputLines.push("");
4189
+ }
4190
+ return this.formatOutput(outputLines, fileSize, offset, limit);
4191
+ }
4192
+ /**
4193
+ * Handle text file
4194
+ */
4195
+ async handleTextFile(filePath, fileSize, offset, limit) {
4196
+ const content = await readFile(filePath, "utf-8");
4197
+ if (content.length === 0) return {
4198
+ content: "[File is empty]",
4199
+ totalLines: 0,
4200
+ linesReturned: 0,
4201
+ startLine: 1,
4202
+ truncated: false,
4203
+ fileSize,
4204
+ isBinary: false
4205
+ };
4206
+ const lines = content.split("\n");
4207
+ return this.formatOutput(lines, fileSize, offset, limit);
4208
+ }
4209
+ /**
4210
+ * Format output with line numbers and apply offset/limit
4211
+ */
4212
+ formatOutput(lines, fileSize, offset, limit) {
4213
+ const totalLines = lines.length;
4214
+ const startLine = offset ?? 1;
4215
+ const lineLimit = limit ?? DEFAULT_LINE_LIMIT;
4216
+ const startIndex = startLine - 1;
4217
+ const endIndex = Math.min(startIndex + lineLimit, totalLines);
4218
+ const selectedLines = lines.slice(startIndex, endIndex);
4219
+ let truncated = false;
4220
+ return {
4221
+ content: selectedLines.map((line, idx) => {
4222
+ const lineNum = startLine + idx;
4223
+ const lineNumStr = String(lineNum).padStart(6, " ");
4224
+ let displayLine = line;
4225
+ if (line.length > MAX_LINE_LENGTH) {
4226
+ displayLine = `${line.slice(0, MAX_LINE_LENGTH)}... [truncated]`;
4227
+ truncated = true;
4228
+ }
4229
+ return `${lineNumStr}|${displayLine}`;
4230
+ }).join("\n"),
4231
+ totalLines,
4232
+ linesReturned: selectedLines.length,
4233
+ startLine,
4234
+ truncated,
4235
+ fileSize,
4236
+ isBinary: false
4237
+ };
4238
+ }
4239
+ /**
4240
+ * Format file size for display
4241
+ */
4242
+ formatSize(bytes) {
4243
+ const units = [
4244
+ "B",
4245
+ "KB",
4246
+ "MB",
4247
+ "GB"
4248
+ ];
4249
+ let size = bytes;
4250
+ let unitIndex = 0;
4251
+ while (size >= 1024 && unitIndex < units.length - 1) {
4252
+ size /= 1024;
4253
+ unitIndex++;
4254
+ }
4255
+ return `${size.toFixed(unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
4256
+ }
4257
+ };
4258
+
4259
+ //#endregion
4260
+ //#region src/tool/builtin/edit.ts
4261
+ /**
4262
+ * Tool for performing exact string replacements in files.
4263
+ *
4264
+ * This tool finds and replaces text in files with careful handling
4265
+ * of unique matches and the option to replace all occurrences.
4266
+ *
4267
+ * @example
4268
+ * ```typescript
4269
+ * const editTool = new EditTool()
4270
+ * const result = await editTool.execute({
4271
+ * file_path: '/path/to/file.ts',
4272
+ * old_string: 'const foo = 1',
4273
+ * new_string: 'const foo = 2'
4274
+ * })
4275
+ * ```
4276
+ */
4277
+ var EditTool = class extends BaseTool {
4278
+ name = "Edit";
4279
+ riskLevel = "high";
4280
+ description = `Performs exact string replacements in files.
4281
+
4282
+ Usage notes:
4283
+ - When editing, preserve the exact indentation (tabs/spaces) from the original file
4284
+ - The edit will FAIL if old_string is not unique in the file unless replace_all is true
4285
+ - Use replace_all for replacing/renaming strings across the entire file
4286
+ - old_string and new_string must be different
4287
+ - ALWAYS prefer editing existing files over creating new ones`;
4288
+ parameters = {
4289
+ type: "object",
4290
+ properties: {
4291
+ file_path: {
4292
+ type: "string",
4293
+ description: "The absolute path to the file to modify"
4294
+ },
4295
+ old_string: {
4296
+ type: "string",
4297
+ description: "The text to replace"
4298
+ },
4299
+ new_string: {
4300
+ type: "string",
4301
+ description: "The text to replace it with (must be different from old_string)"
4302
+ },
4303
+ replace_all: {
4304
+ type: "boolean",
4305
+ description: "Replace all occurrences of old_string (default false)"
4306
+ }
4307
+ },
4308
+ required: [
4309
+ "file_path",
4310
+ "old_string",
4311
+ "new_string"
4312
+ ]
4313
+ };
4314
+ /** Current working directory for resolving relative paths */
4315
+ cwd;
4316
+ constructor(options) {
4317
+ super();
4318
+ this.cwd = options?.cwd ?? process.cwd();
4319
+ }
4320
+ /**
4321
+ * Set the current working directory
4322
+ */
4323
+ setCwd(cwd) {
4324
+ this.cwd = cwd;
4325
+ }
4326
+ /**
4327
+ * Get the current working directory
4328
+ */
4329
+ getCwd() {
4330
+ return this.cwd;
4331
+ }
4332
+ /**
4333
+ * Execute file edit
4334
+ *
4335
+ * @param args - Edit arguments
4336
+ * @returns MCP-compliant CallToolResult with edit details
4337
+ */
4338
+ async execute(args) {
4339
+ const { file_path, old_string, new_string, replace_all } = this.validateArgs(args);
4340
+ const filePath = path.isAbsolute(file_path) ? file_path : path.resolve(this.cwd, file_path);
4341
+ try {
4342
+ if ((await stat(filePath)).isDirectory()) return errorContent(`Path is a directory, not a file: ${filePath}`);
4343
+ } catch (error) {
4344
+ if (error.code === "ENOENT") return errorContent(`File not found: ${filePath}`);
4345
+ throw error;
4346
+ }
4347
+ const content = await readFile(filePath, "utf-8");
4348
+ const occurrences = this.countOccurrences(content, old_string);
4349
+ if (occurrences === 0) return errorContent(`old_string not found in file: ${filePath}\n\nSearched for:\n${this.truncateForError(old_string)}`);
4350
+ if (occurrences > 1 && !replace_all) return errorContent(`old_string is not unique in the file (found ${occurrences} occurrences). Either provide more context to make it unique, or set replace_all: true to replace all occurrences.`);
4351
+ let newContent;
4352
+ let replacements;
4353
+ if (replace_all) {
4354
+ newContent = content.split(old_string).join(new_string);
4355
+ replacements = occurrences;
4356
+ } else {
4357
+ const index = content.indexOf(old_string);
4358
+ newContent = content.slice(0, index) + new_string + content.slice(index + old_string.length);
4359
+ replacements = 1;
4360
+ }
4361
+ await writeFile(filePath, newContent, "utf-8");
4362
+ const result = {
4363
+ success: true,
4364
+ replacements,
4365
+ filePath,
4366
+ message: `Successfully replaced ${replacements} occurrence${replacements > 1 ? "s" : ""} in ${path.basename(filePath)}`
4367
+ };
4368
+ return {
4369
+ content: [{
4370
+ type: "text",
4371
+ text: result.message
4372
+ }],
4373
+ structuredContent: result
4374
+ };
4375
+ }
4376
+ /**
4377
+ * Validate and parse arguments
4378
+ */
4379
+ validateArgs(args) {
4380
+ const filePath = args.file_path;
4381
+ const oldString = args.old_string;
4382
+ const newString = args.new_string;
4383
+ if (typeof filePath !== "string" || !filePath.trim()) throw new Error("file_path is required and must be a non-empty string");
4384
+ if (typeof oldString !== "string") throw new TypeError("old_string is required and must be a string");
4385
+ if (oldString === "") throw new Error("old_string cannot be empty");
4386
+ if (typeof newString !== "string") throw new TypeError("new_string is required and must be a string");
4387
+ if (oldString === newString) throw new Error("new_string must be different from old_string");
4388
+ return {
4389
+ file_path: filePath.trim(),
4390
+ old_string: oldString,
4391
+ new_string: newString,
4392
+ replace_all: args.replace_all === true
4393
+ };
4394
+ }
4395
+ /**
4396
+ * Count occurrences of a substring in a string
4397
+ */
4398
+ countOccurrences(str, substr) {
4399
+ let count = 0;
4400
+ let pos = str.indexOf(substr);
4401
+ while (pos !== -1) {
4402
+ count++;
4403
+ pos = str.indexOf(substr, pos + substr.length);
4404
+ }
4405
+ return count;
4406
+ }
4407
+ /**
4408
+ * Truncate string for error messages
4409
+ */
4410
+ truncateForError(str, maxLength = 200) {
4411
+ if (str.length <= maxLength) return str;
4412
+ return `${str.slice(0, maxLength)}... [truncated, ${str.length} chars total]`;
4413
+ }
4414
+ };
4415
+
4416
+ //#endregion
4417
+ //#region src/tool/builtin/write.ts
4418
+ /**
4419
+ * Tool for writing files to the filesystem.
4420
+ *
4421
+ * This tool creates or overwrites files with the specified content,
4422
+ * automatically creating parent directories if needed.
4423
+ *
4424
+ * @example
4425
+ * ```typescript
4426
+ * const writeTool = new WriteTool()
4427
+ * const result = await writeTool.execute({
4428
+ * file_path: '/path/to/file.ts',
4429
+ * content: 'export const foo = 1'
4430
+ * })
4431
+ * ```
4432
+ *
4433
+ * @example Restrict writes to a specific directory
4434
+ * ```typescript
4435
+ * const writeTool = new WriteTool({
4436
+ * cwd: '/app/output',
4437
+ * restrictToDirectory: true
4438
+ * })
4439
+ * // All paths will be resolved relative to /app/output
4440
+ * // Absolute paths and path traversal (../) will be blocked
4441
+ * ```
4442
+ */
4443
+ var WriteTool = class extends BaseTool {
4444
+ name = "Write";
4445
+ riskLevel = "high";
4446
+ /** Current working directory for resolving relative paths */
4447
+ _cwd;
4448
+ /** Allowed directory for file operations (if set, restricts writes to this directory) */
4449
+ _allowedDirectory;
4450
+ constructor(options) {
4451
+ super();
4452
+ this._cwd = options?.cwd ?? process.cwd();
4453
+ this._allowedDirectory = options?.allowedDirectory;
4454
+ }
4455
+ /**
4456
+ * Dynamic description that includes allowed directory info if configured
4457
+ */
4458
+ get description() {
4459
+ const baseDescription = `Writes a file to the local filesystem.
4460
+
4461
+ Usage notes:
4462
+ - This tool will overwrite the existing file if there is one at the provided path
4463
+ - Parent directories will be created automatically if they don't exist
4464
+ - ALWAYS prefer editing existing files over writing new ones
4465
+ - NEVER proactively create documentation files (*.md) or README files unless explicitly requested`;
4466
+ if (this._allowedDirectory) return `${baseDescription}
4467
+ - IMPORTANT: Files can ONLY be written within: ${this._allowedDirectory}
4468
+ - Use absolute paths starting with ${this._allowedDirectory}/ (e.g., ${this._allowedDirectory}/filename.html)`;
4469
+ return baseDescription;
4470
+ }
4471
+ /**
4472
+ * Dynamic parameters that include allowed directory info if configured
4473
+ */
4474
+ get parameters() {
4475
+ return {
4476
+ type: "object",
4477
+ properties: {
4478
+ file_path: {
4479
+ type: "string",
4480
+ description: this._allowedDirectory ? `The absolute path to the file to write (must be within ${this._allowedDirectory})` : "The absolute path to the file to write (must be absolute, not relative)"
4481
+ },
4482
+ content: {
4483
+ type: "string",
4484
+ description: "The content to write to the file"
4485
+ }
4486
+ },
4487
+ required: ["file_path", "content"]
4488
+ };
4489
+ }
4490
+ /**
4491
+ * Set the current working directory
4492
+ */
4493
+ setCwd(cwd) {
4494
+ this._cwd = cwd;
4495
+ }
4496
+ /**
4497
+ * Get the current working directory
4498
+ */
4499
+ getCwd() {
4500
+ return this._cwd;
4501
+ }
4502
+ /**
4503
+ * Set the allowed directory for file operations
4504
+ */
4505
+ setAllowedDirectory(dir) {
4506
+ this._allowedDirectory = dir;
4507
+ }
4508
+ /**
4509
+ * Get the allowed directory for file operations
4510
+ */
4511
+ getAllowedDirectory() {
4512
+ return this._allowedDirectory;
4513
+ }
4514
+ /**
4515
+ * Execute file write
4516
+ *
4517
+ * @param args - Write arguments
4518
+ * @returns MCP-compliant CallToolResult with write details
4519
+ */
4520
+ async execute(args) {
4521
+ const { file_path, content } = this.validateArgs(args);
4522
+ const filePath = path.isAbsolute(file_path) ? file_path : path.resolve(this._cwd, file_path);
4523
+ if (this._allowedDirectory) {
4524
+ const normalizedAllowed = path.resolve(this._allowedDirectory);
4525
+ const normalizedFilePath = path.resolve(filePath);
4526
+ if (!normalizedFilePath.startsWith(normalizedAllowed + path.sep) && normalizedFilePath !== normalizedAllowed) return errorContent(`Access denied: ${file_path}\nFiles can only be written within: ${this._allowedDirectory}\nPlease use a path like: ${this._allowedDirectory}/<filename>`);
4527
+ }
4528
+ let overwritten = false;
4529
+ try {
4530
+ if ((await stat(filePath)).isDirectory()) return errorContent(`Path is a directory, not a file: ${filePath}`);
4531
+ overwritten = true;
4532
+ } catch (error) {
4533
+ if (error.code !== "ENOENT") throw error;
4534
+ }
4535
+ await mkdir(path.dirname(filePath), { recursive: true });
4536
+ await writeFile(filePath, content, "utf-8");
4537
+ const bytesWritten = Buffer.byteLength(content, "utf-8");
4538
+ const result = {
4539
+ success: true,
4540
+ filePath,
4541
+ bytesWritten,
4542
+ overwritten,
4543
+ message: overwritten ? `Successfully overwrote ${path.basename(filePath)} (${this.formatSize(bytesWritten)})` : `Successfully created ${path.basename(filePath)} (${this.formatSize(bytesWritten)})`
4544
+ };
4545
+ return {
4546
+ content: [{
4547
+ type: "text",
4548
+ text: result.message
4549
+ }],
4550
+ structuredContent: result
4551
+ };
4552
+ }
4553
+ /**
4554
+ * Validate and parse arguments
4555
+ */
4556
+ validateArgs(args) {
4557
+ const filePath = args.file_path;
4558
+ const content = args.content;
4559
+ if (typeof filePath !== "string" || !filePath.trim()) throw new Error("file_path is required and must be a non-empty string");
4560
+ if (typeof content !== "string") throw new TypeError("content is required and must be a string");
4561
+ return {
4562
+ file_path: filePath.trim(),
4563
+ content
4564
+ };
4565
+ }
4566
+ /**
4567
+ * Format file size for display
4568
+ */
4569
+ formatSize(bytes) {
4570
+ const units = [
4571
+ "B",
4572
+ "KB",
4573
+ "MB",
4574
+ "GB"
4575
+ ];
4576
+ let size = bytes;
4577
+ let unitIndex = 0;
4578
+ while (size >= 1024 && unitIndex < units.length - 1) {
4579
+ size /= 1024;
4580
+ unitIndex++;
4581
+ }
4582
+ return `${size.toFixed(unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
4583
+ }
4584
+ };
4585
+
4586
+ //#endregion
4587
+ //#region src/tool/builtin/webSearch.ts
4588
+ /**
4589
+ * Tool for searching the web using Serper API.
4590
+ *
4591
+ * This tool allows the agent to search the web and use the results
4592
+ * to inform responses with up-to-date information.
4593
+ *
4594
+ * @example
4595
+ * ```typescript
4596
+ * const webSearchTool = new WebSearchTool({ apiKey: 'your-serper-api-key' })
4597
+ * const result = await webSearchTool.execute({
4598
+ * query: 'latest TypeScript features 2025'
4599
+ * })
4600
+ * ```
4601
+ */
4602
+ var WebSearchTool = class extends BaseTool {
4603
+ name = "WebSearch";
4604
+ description = `Allows the agent to search the web and use the results to inform responses.
4605
+
4606
+ Usage notes:
4607
+ - Provides up-to-date information for current events and recent data
4608
+ - Returns search results with titles, links, and snippets
4609
+ - Use this tool for accessing information beyond the knowledge cutoff
4610
+ - After answering, include a "Sources:" section with relevant URLs as markdown hyperlinks`;
4611
+ parameters = {
4612
+ type: "object",
4613
+ properties: { query: {
4614
+ type: "string",
4615
+ minLength: 2,
4616
+ description: "The search query to use"
4617
+ } },
4618
+ required: ["query"]
4619
+ };
4620
+ /** Serper API key */
4621
+ apiKey;
4622
+ /** Serper API endpoint */
4623
+ apiEndpoint;
4624
+ /** Number of results to return */
4625
+ numResults;
4626
+ constructor(options) {
4627
+ super();
4628
+ this.apiKey = options?.apiKey ?? process.env.SERPER_API_KEY ?? "";
4629
+ this.apiEndpoint = options?.apiEndpoint ?? "https://google.serper.dev/search?format=json";
4630
+ this.numResults = options?.numResults ?? 10;
4631
+ }
4632
+ /**
4633
+ * Set API key
4634
+ */
4635
+ setApiKey(apiKey) {
4636
+ this.apiKey = apiKey;
4637
+ }
4638
+ /**
4639
+ * Execute web search
4640
+ *
4641
+ * @param args - WebSearch arguments
4642
+ * @returns MCP-compliant CallToolResult with search results
4643
+ */
4644
+ async execute(args) {
4645
+ const { query } = this.validateArgs(args);
4646
+ if (!this.apiKey) return errorContent("Serper API key is not configured. Set SERPER_API_KEY environment variable or pass apiKey in constructor.");
4647
+ try {
4648
+ const response = await fetch(this.apiEndpoint, {
4649
+ method: "POST",
4650
+ headers: {
4651
+ "X-API-KEY": this.apiKey,
4652
+ "Content-Type": "application/json"
4653
+ },
4654
+ body: JSON.stringify({
4655
+ q: query,
4656
+ num: this.numResults
4657
+ })
4658
+ });
4659
+ if (!response.ok) {
4660
+ const errorText = await response.text();
4661
+ return errorContent(`Serper API error (${response.status}): ${errorText}`);
4662
+ }
4663
+ const results = ((await response.json()).organic ?? []).map((item, index) => ({
4664
+ title: item.title,
4665
+ link: item.link,
4666
+ snippet: item.snippet,
4667
+ position: item.position ?? index + 1
4668
+ }));
4669
+ const markdown = this.formatMarkdown(query, results);
4670
+ const result = {
4671
+ success: true,
4672
+ query,
4673
+ results,
4674
+ totalResults: results.length,
4675
+ markdown
4676
+ };
4677
+ return {
4678
+ content: [{
4679
+ type: "text",
4680
+ text: markdown
4681
+ }],
4682
+ structuredContent: result
4683
+ };
4684
+ } catch (error) {
4685
+ return errorContent(`Failed to execute search: ${error instanceof Error ? error.message : String(error)}`);
4686
+ }
4687
+ }
4688
+ /**
4689
+ * Validate and parse arguments
4690
+ */
4691
+ validateArgs(args) {
4692
+ const query = args.query;
4693
+ if (typeof query !== "string" || query.trim().length < 2) throw new Error("query is required and must be at least 2 characters");
4694
+ return { query: query.trim() };
4695
+ }
4696
+ /**
4697
+ * Format search results as markdown
4698
+ */
4699
+ formatMarkdown(query, results) {
4700
+ if (results.length === 0) return `No results found for: "${query}"`;
4701
+ const lines = [`## Search Results for: "${query}"`, ""];
4702
+ for (const result of results) {
4703
+ lines.push(`### ${result.position}. [${result.title}](${result.link})`);
4704
+ lines.push("");
4705
+ lines.push(result.snippet);
4706
+ lines.push("");
4707
+ }
4708
+ lines.push("---");
4709
+ lines.push("");
4710
+ lines.push("**Sources:**");
4711
+ for (const result of results) lines.push(`- [${result.title}](${result.link})`);
4712
+ return lines.join("\n");
4713
+ }
4714
+ };
4715
+
4716
+ //#endregion
4717
+ //#region src/tool/registry.ts
4718
+ /**
4719
+ * Registry for managing tools.
4720
+ *
4721
+ * Provides methods to register, unregister, and look up tools,
4722
+ * as well as convert to OpenAI-compatible format.
4723
+ */
4724
+ var ToolRegistry = class {
4725
+ tools = /* @__PURE__ */ new Map();
4726
+ /**
4727
+ * Register a tool
4728
+ *
4729
+ * @param tool - Tool to register
4730
+ * @throws Error if tool with same name already exists
4731
+ */
4732
+ register(tool) {
4733
+ if (this.tools.has(tool.name)) throw new Error(`Tool "${tool.name}" already registered`);
4734
+ this.tools.set(tool.name, tool);
4735
+ }
4736
+ /**
4737
+ * Unregister a tool by name
4738
+ *
4739
+ * @param name - Name of tool to remove
4740
+ * @returns true if tool was found and removed
4741
+ */
4742
+ unregister(name) {
4743
+ return this.tools.delete(name);
4744
+ }
4745
+ /**
4746
+ * Get a tool by name
4747
+ *
4748
+ * @param name - Tool name
4749
+ * @returns Tool instance or undefined if not found
4750
+ */
4751
+ get(name) {
4752
+ return this.tools.get(name);
4753
+ }
4754
+ /**
4755
+ * List all registered tools
4756
+ *
4757
+ * @returns Array of all tools
4758
+ */
4759
+ list() {
4760
+ return Array.from(this.tools.values());
4761
+ }
4762
+ /**
4763
+ * Check if a tool is registered
4764
+ *
4765
+ * @param name - Tool name
4766
+ * @returns true if tool exists
4767
+ */
4768
+ has(name) {
4769
+ return this.tools.has(name);
4770
+ }
4771
+ /**
4772
+ * Get count of registered tools
4773
+ */
4774
+ get size() {
4775
+ return this.tools.size;
4776
+ }
4777
+ /**
4778
+ * Convert all tools to OpenAI-compatible format
4779
+ *
4780
+ * @returns Array of tools in OpenAI function calling format
4781
+ */
4782
+ toOpenAIFormat() {
4783
+ return this.list().map((tool) => ({
4784
+ type: "function",
4785
+ function: {
4786
+ name: tool.name,
4787
+ description: tool.description,
4788
+ parameters: tool.parameters
4789
+ }
4790
+ }));
4791
+ }
4792
+ };
4793
+
4794
+ //#endregion
4795
+ export { Agent, AgentAbortError, AgentMaxIterationsError, BaseModel, BaseSession, BaseSessionManager, BaseTool, EditTool, FileStateStore as FileCheckpointStore, FileStateStore, GlobTool, GrepTool, InMemoryStateStore as InMemoryCheckpointStore, InMemoryStateStore, InMemoryModelHealth, ModelError, ReadTool, RetryPolicy, StateKeys, StateStore, ToolRegistry, WebSearchTool, WriteTool, compose, compressSessionManually, createContextCompressionMiddleware, createInitialLoopState, createInitialLoopStateFromMessages, createModel, createOpenAIAdapter, ensureNotAborted, errorContent, fromLoopCheckpoint, imageContent, textContent, toLoopCheckpoint };