deepagentsdk 0.9.2

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/package.json +95 -0
  4. package/src/agent.ts +1230 -0
  5. package/src/backends/composite.ts +273 -0
  6. package/src/backends/filesystem.ts +692 -0
  7. package/src/backends/index.ts +22 -0
  8. package/src/backends/local-sandbox.ts +175 -0
  9. package/src/backends/persistent.ts +593 -0
  10. package/src/backends/sandbox.ts +510 -0
  11. package/src/backends/state.ts +244 -0
  12. package/src/backends/utils.ts +287 -0
  13. package/src/checkpointer/file-saver.ts +98 -0
  14. package/src/checkpointer/index.ts +5 -0
  15. package/src/checkpointer/kv-saver.ts +82 -0
  16. package/src/checkpointer/memory-saver.ts +82 -0
  17. package/src/checkpointer/types.ts +125 -0
  18. package/src/cli/components/ApiKeyInput.tsx +300 -0
  19. package/src/cli/components/FilePreview.tsx +237 -0
  20. package/src/cli/components/Input.tsx +277 -0
  21. package/src/cli/components/Message.tsx +93 -0
  22. package/src/cli/components/ModelSelection.tsx +338 -0
  23. package/src/cli/components/SlashMenu.tsx +101 -0
  24. package/src/cli/components/StatusBar.tsx +89 -0
  25. package/src/cli/components/Subagent.tsx +91 -0
  26. package/src/cli/components/TodoList.tsx +133 -0
  27. package/src/cli/components/ToolApproval.tsx +70 -0
  28. package/src/cli/components/ToolCall.tsx +144 -0
  29. package/src/cli/components/ToolCallSummary.tsx +175 -0
  30. package/src/cli/components/Welcome.tsx +75 -0
  31. package/src/cli/components/index.ts +24 -0
  32. package/src/cli/hooks/index.ts +12 -0
  33. package/src/cli/hooks/useAgent.ts +933 -0
  34. package/src/cli/index.tsx +1066 -0
  35. package/src/cli/theme.ts +205 -0
  36. package/src/cli/utils/model-list.ts +365 -0
  37. package/src/constants/errors.ts +29 -0
  38. package/src/constants/limits.ts +195 -0
  39. package/src/index.ts +176 -0
  40. package/src/middleware/agent-memory.ts +330 -0
  41. package/src/prompts.ts +196 -0
  42. package/src/skills/index.ts +2 -0
  43. package/src/skills/load.ts +191 -0
  44. package/src/skills/types.ts +53 -0
  45. package/src/tools/execute.ts +167 -0
  46. package/src/tools/filesystem.ts +418 -0
  47. package/src/tools/index.ts +39 -0
  48. package/src/tools/subagent.ts +443 -0
  49. package/src/tools/todos.ts +101 -0
  50. package/src/tools/web.ts +567 -0
  51. package/src/types/backend.ts +177 -0
  52. package/src/types/core.ts +220 -0
  53. package/src/types/events.ts +429 -0
  54. package/src/types/index.ts +94 -0
  55. package/src/types/structured-output.ts +43 -0
  56. package/src/types/subagent.ts +96 -0
  57. package/src/types.ts +22 -0
  58. package/src/utils/approval.ts +213 -0
  59. package/src/utils/events.ts +416 -0
  60. package/src/utils/eviction.ts +181 -0
  61. package/src/utils/index.ts +34 -0
  62. package/src/utils/model-parser.ts +38 -0
  63. package/src/utils/patch-tool-calls.ts +233 -0
  64. package/src/utils/project-detection.ts +32 -0
  65. package/src/utils/summarization.ts +254 -0
@@ -0,0 +1,933 @@
1
+ /**
2
+ * Hook for managing agent streaming and events.
3
+ */
4
+ import { useState, useCallback, useRef } from "react";
5
+ import type {
6
+ DeepAgentState,
7
+ DeepAgentEvent,
8
+ TodoItem,
9
+ ModelMessage,
10
+ SummarizationConfig,
11
+ InterruptOnConfig,
12
+ } from "../../types.js";
13
+ import type { BaseCheckpointSaver } from "../../checkpointer/types.js";
14
+ import { createDeepAgent } from "../../agent.js";
15
+ import { parseModelString } from "../../utils/model-parser.js";
16
+ import type { SandboxBackendProtocol } from "../../types.js";
17
+ import type { ToolCallData } from "../components/Message.js";
18
+ import { useEffect } from "react";
19
+
20
+ export type AgentStatus =
21
+ | "idle"
22
+ | "thinking"
23
+ | "streaming"
24
+ | "tool-call"
25
+ | "subagent"
26
+ | "done"
27
+ | "error";
28
+
29
+ export interface AgentEventLog {
30
+ id: string;
31
+ type: DeepAgentEvent["type"] | "text-segment";
32
+ event: DeepAgentEvent | { type: "text-segment"; text: string };
33
+ timestamp: Date;
34
+ }
35
+
36
+ export interface UseAgentOptions {
37
+ model: string;
38
+ maxSteps: number;
39
+ systemPrompt?: string;
40
+ backend: SandboxBackendProtocol;
41
+ /** Enable Anthropic prompt caching */
42
+ enablePromptCaching?: boolean;
43
+ /** Token limit before evicting large tool results */
44
+ toolResultEvictionLimit?: number;
45
+ /** Summarization configuration */
46
+ summarization?: SummarizationConfig;
47
+ /**
48
+ * Default interruptOn config for CLI.
49
+ * Default: { execute: true, write_file: true, edit_file: true }
50
+ */
51
+ interruptOn?: InterruptOnConfig;
52
+ /** Session ID for checkpoint persistence */
53
+ sessionId?: string;
54
+ /** Checkpoint saver for session persistence */
55
+ checkpointer?: BaseCheckpointSaver;
56
+ }
57
+
58
+ export interface UseAgentReturn {
59
+ /** Current agent status */
60
+ status: AgentStatus;
61
+ /** Current streaming text */
62
+ streamingText: string;
63
+ /** Final text from the last completed generation */
64
+ lastCompletedText: string;
65
+ /** Event log for rendering */
66
+ events: AgentEventLog[];
67
+ /** Current state (todos, files) */
68
+ state: DeepAgentState;
69
+ /** Conversation history */
70
+ messages: ModelMessage[];
71
+ /** Tool calls from the current/last generation */
72
+ toolCalls: ToolCallData[];
73
+ /** Current error if any */
74
+ error: Error | null;
75
+ /** Send a prompt to the agent, returns the final text and tool calls */
76
+ sendPrompt: (prompt: string) => Promise<{ text: string; toolCalls: ToolCallData[] }>;
77
+ /** Abort current generation */
78
+ abort: () => void;
79
+ /** Clear events, messages, and reset */
80
+ clear: () => void;
81
+ /** Clear only the streaming text (after saving to messages) */
82
+ clearStreamingText: () => void;
83
+ /** Clear only the events (after saving to messages) */
84
+ clearEvents: () => void;
85
+ /** Update model */
86
+ setModel: (model: string) => void;
87
+ /** Current model */
88
+ currentModel: string;
89
+ /** Feature flags */
90
+ features: {
91
+ promptCaching: boolean;
92
+ eviction: boolean;
93
+ summarization: boolean;
94
+ };
95
+ /** Toggle prompt caching */
96
+ setPromptCaching: (enabled: boolean) => void;
97
+ /** Toggle eviction */
98
+ setEviction: (enabled: boolean) => void;
99
+ /** Toggle summarization */
100
+ setSummarization: (enabled: boolean) => void;
101
+ /** Current approval request if any */
102
+ pendingApproval: {
103
+ approvalId: string;
104
+ toolName: string;
105
+ args: unknown;
106
+ } | null;
107
+ /** Respond to approval request */
108
+ respondToApproval: (approved: boolean) => void;
109
+ /** Whether auto-approve mode is enabled */
110
+ autoApproveEnabled: boolean;
111
+ /** Toggle auto-approve mode */
112
+ setAutoApprove: (enabled: boolean) => void;
113
+ }
114
+
115
+ let eventCounter = 0;
116
+
117
+ function createEventId(): string {
118
+ return `event-${++eventCounter}`;
119
+ }
120
+
121
+ // Default interruptOn config for CLI - safe defaults
122
+ // Based on LangChain DeepAgents approval pattern
123
+ const DEFAULT_CLI_INTERRUPT_ON: InterruptOnConfig = {
124
+ execute: true,
125
+ write_file: true,
126
+ edit_file: true,
127
+ web_search: true,
128
+ fetch_url: true,
129
+ // Note: http_request does NOT require approval per LangChain pattern
130
+ };
131
+
132
+ export function useAgent(options: UseAgentOptions): UseAgentReturn {
133
+ const [status, setStatus] = useState<AgentStatus>("idle");
134
+ const [streamingText, setStreamingText] = useState("");
135
+ const [lastCompletedText, setLastCompletedText] = useState("");
136
+ const [events, setEvents] = useState<AgentEventLog[]>([]);
137
+ const [state, setState] = useState<DeepAgentState>({
138
+ todos: [],
139
+ files: {},
140
+ });
141
+ const [messages, setMessages] = useState<ModelMessage[]>([]);
142
+ const [toolCalls, setToolCalls] = useState<ToolCallData[]>([]);
143
+ const [error, setError] = useState<Error | null>(null);
144
+ const [currentModel, setCurrentModel] = useState(options.model);
145
+
146
+ // Load session on mount if sessionId and checkpointer are provided
147
+ useEffect(() => {
148
+ const loadSession = async () => {
149
+ if (!options.sessionId || !options.checkpointer) return;
150
+
151
+ const checkpoint = await options.checkpointer.load(options.sessionId);
152
+ if (checkpoint) {
153
+ setState(checkpoint.state);
154
+ setMessages(checkpoint.messages);
155
+ messagesRef.current = checkpoint.messages;
156
+ // Show restore message via addEvent
157
+ addEvent({
158
+ type: "checkpoint-loaded",
159
+ threadId: options.sessionId,
160
+ step: checkpoint.step,
161
+ messagesCount: checkpoint.messages.length,
162
+ });
163
+ }
164
+ };
165
+
166
+ loadSession().catch(console.error);
167
+ // eslint-disable-next-line react-hooks/exhaustive-deps
168
+ }, [options.sessionId]);
169
+
170
+ // Feature flag states (can be toggled at runtime)
171
+ const [promptCachingEnabled, setPromptCachingEnabled] = useState(options.enablePromptCaching ?? false);
172
+ const [evictionLimit, setEvictionLimit] = useState(options.toolResultEvictionLimit ?? 0);
173
+ const [summarizationEnabled, setSummarizationEnabled] = useState(options.summarization?.enabled ?? false);
174
+ const [summarizationConfig, setSummarizationConfig] = useState(options.summarization);
175
+
176
+ // Auto-approve mode state
177
+ const [autoApproveEnabled, setAutoApproveEnabled] = useState(false);
178
+
179
+ // Pending approval state
180
+ const [pendingApproval, setPendingApproval] = useState<{
181
+ approvalId: string;
182
+ toolName: string;
183
+ args: unknown;
184
+ } | null>(null);
185
+ const approvalResolverRef = useRef<((approved: boolean) => void) | null>(null);
186
+
187
+ const abortControllerRef = useRef<AbortController | null>(null);
188
+ // Use a ref to track accumulated text during streaming (current segment, gets flushed)
189
+ const accumulatedTextRef = useRef("");
190
+ // Use a ref to track total text for return value (never reset mid-generation)
191
+ const totalTextRef = useRef("");
192
+ // Use a ref to track messages during streaming (to pass to agent)
193
+ const messagesRef = useRef<ModelMessage[]>([]);
194
+ // Use a ref to track tool calls during streaming
195
+ const toolCallsRef = useRef<ToolCallData[]>([]);
196
+ // Map to track pending tool calls by ID
197
+ const pendingToolCallsRef = useRef<Map<string, ToolCallData>>(new Map());
198
+
199
+ // Track feature flags (derived from state)
200
+ const features = {
201
+ promptCaching: promptCachingEnabled,
202
+ eviction: evictionLimit > 0,
203
+ summarization: summarizationEnabled,
204
+ };
205
+
206
+ const agentRef = useRef(
207
+ createDeepAgent({
208
+ model: parseModelString(currentModel),
209
+ maxSteps: options.maxSteps,
210
+ systemPrompt: options.systemPrompt,
211
+ backend: options.backend,
212
+ enablePromptCaching: promptCachingEnabled,
213
+ toolResultEvictionLimit: evictionLimit,
214
+ summarization: summarizationConfig,
215
+ interruptOn: autoApproveEnabled ? undefined : (options.interruptOn ?? DEFAULT_CLI_INTERRUPT_ON),
216
+ checkpointer: options.checkpointer,
217
+ })
218
+ );
219
+
220
+ const addEvent = useCallback((event: DeepAgentEvent | { type: "text-segment"; text: string }) => {
221
+ setEvents((prev) => [
222
+ ...prev,
223
+ {
224
+ id: createEventId(),
225
+ type: event.type,
226
+ event,
227
+ timestamp: new Date(),
228
+ },
229
+ ]);
230
+ }, []);
231
+
232
+ // Flush accumulated text as a text-segment event
233
+ const flushTextSegment = useCallback(() => {
234
+ if (accumulatedTextRef.current.trim()) {
235
+ addEvent({
236
+ type: "text-segment",
237
+ text: accumulatedTextRef.current,
238
+ });
239
+ accumulatedTextRef.current = "";
240
+ setStreamingText("");
241
+ }
242
+ }, [addEvent]);
243
+
244
+ // ============================================================================
245
+ // Event Handler Context
246
+ // ============================================================================
247
+
248
+ /**
249
+ * Context object shared by all event handlers.
250
+ * Contains all refs and setters needed for event processing.
251
+ */
252
+ interface EventHandlerContext {
253
+ setStatus: (status: AgentStatus) => void;
254
+ setState: React.Dispatch<React.SetStateAction<DeepAgentState>>;
255
+ setMessages: React.Dispatch<React.SetStateAction<ModelMessage[]>>;
256
+ setToolCalls: React.Dispatch<React.SetStateAction<ToolCallData[]>>;
257
+ setError: React.Dispatch<React.SetStateAction<Error | null>>;
258
+ addEvent: (event: DeepAgentEvent | { type: "text-segment"; text: string }) => void;
259
+ flushTextSegment: () => void;
260
+ accumulatedTextRef: React.MutableRefObject<string>;
261
+ totalTextRef: React.MutableRefObject<string>;
262
+ toolCallsRef: React.MutableRefObject<ToolCallData[]>;
263
+ pendingToolCallsRef: React.MutableRefObject<Map<string, ToolCallData>>;
264
+ messagesRef: React.MutableRefObject<ModelMessage[]>;
265
+ }
266
+
267
+ // ============================================================================
268
+ // Helper Functions for Common Patterns
269
+ // ============================================================================
270
+
271
+ /**
272
+ * Common pattern: flush text, set status to "tool-call", add event
273
+ */
274
+ const handleToolEvent = (
275
+ event: DeepAgentEvent,
276
+ ctx: EventHandlerContext
277
+ ) => {
278
+ ctx.flushTextSegment();
279
+ ctx.setStatus("tool-call");
280
+ ctx.addEvent(event);
281
+ };
282
+
283
+ // ============================================================================
284
+ // Event Handlers
285
+ // ============================================================================
286
+
287
+ /**
288
+ * Handle text streaming events.
289
+ * Accumulates text and updates streaming display.
290
+ */
291
+ const handleTextEvent = (
292
+ event: DeepAgentEvent,
293
+ ctx: EventHandlerContext
294
+ ) => {
295
+ if (event.type !== "text") return;
296
+ ctx.setStatus("streaming");
297
+ ctx.accumulatedTextRef.current += event.text;
298
+ ctx.totalTextRef.current += event.text;
299
+ setStreamingText(ctx.accumulatedTextRef.current);
300
+ };
301
+
302
+ /**
303
+ * Handle step-start events.
304
+ * Marks beginning of a new reasoning step.
305
+ */
306
+ const handleStepStartEvent = (
307
+ event: DeepAgentEvent,
308
+ ctx: EventHandlerContext
309
+ ) => {
310
+ if (event.type !== "step-start") return;
311
+ // Don't flush here - steps are just markers, text comes after tool results
312
+ if (event.stepNumber > 1) {
313
+ ctx.addEvent(event);
314
+ }
315
+ };
316
+
317
+ /**
318
+ * Handle tool-call events.
319
+ * Tracks pending tool calls until results arrive.
320
+ */
321
+ const handleToolCallEvent = (
322
+ event: DeepAgentEvent,
323
+ ctx: EventHandlerContext
324
+ ) => {
325
+ if (event.type !== "tool-call") return;
326
+ ctx.flushTextSegment();
327
+ ctx.setStatus("tool-call");
328
+ const pendingToolCall: ToolCallData = {
329
+ toolName: event.toolName,
330
+ args: event.args,
331
+ status: "success", // Will be updated on result
332
+ };
333
+ ctx.pendingToolCallsRef.current.set(event.toolCallId, pendingToolCall);
334
+ ctx.addEvent(event);
335
+ };
336
+
337
+ /**
338
+ * Handle tool-result events.
339
+ * Updates pending tool call with result and moves to completed list.
340
+ */
341
+ const handleToolResultEvent = (
342
+ event: DeepAgentEvent,
343
+ ctx: EventHandlerContext
344
+ ) => {
345
+ if (event.type !== "tool-result") return;
346
+ const completedToolCall = ctx.pendingToolCallsRef.current.get(
347
+ event.toolCallId
348
+ );
349
+ if (completedToolCall) {
350
+ completedToolCall.result = event.result;
351
+ // Move from pending to completed
352
+ ctx.toolCallsRef.current.push(completedToolCall);
353
+ ctx.setToolCalls([...ctx.toolCallsRef.current]);
354
+ ctx.pendingToolCallsRef.current.delete(event.toolCallId);
355
+ }
356
+ ctx.addEvent(event);
357
+ };
358
+
359
+ /**
360
+ * Handle todos-changed events.
361
+ * Updates the todos list in state.
362
+ */
363
+ const handleTodosChangedEvent = (
364
+ event: DeepAgentEvent,
365
+ ctx: EventHandlerContext
366
+ ) => {
367
+ if (event.type !== "todos-changed") return;
368
+ ctx.flushTextSegment();
369
+ ctx.setStatus("tool-call");
370
+ ctx.setState((prev) => ({ ...prev, todos: event.todos }));
371
+ ctx.addEvent(event);
372
+ };
373
+
374
+ /**
375
+ * Handle file-write-start events.
376
+ */
377
+ const handleFileWriteStartEvent = (
378
+ event: DeepAgentEvent,
379
+ ctx: EventHandlerContext
380
+ ) => {
381
+ if (event.type !== "file-write-start") return;
382
+ handleToolEvent(event, ctx);
383
+ };
384
+
385
+ /**
386
+ * Handle file-written events.
387
+ */
388
+ const handleFileWrittenEvent = (
389
+ event: DeepAgentEvent,
390
+ ctx: EventHandlerContext
391
+ ) => {
392
+ if (event.type !== "file-written") return;
393
+ ctx.setStatus("tool-call");
394
+ ctx.addEvent(event);
395
+ };
396
+
397
+ /**
398
+ * Handle file-edited events.
399
+ */
400
+ const handleFileEditedEvent = (
401
+ event: DeepAgentEvent,
402
+ ctx: EventHandlerContext
403
+ ) => {
404
+ if (event.type !== "file-edited") return;
405
+ ctx.setStatus("tool-call");
406
+ ctx.addEvent(event);
407
+ };
408
+
409
+ /**
410
+ * Handle file-read events.
411
+ */
412
+ const handleFileReadEvent = (
413
+ event: DeepAgentEvent,
414
+ ctx: EventHandlerContext
415
+ ) => {
416
+ if (event.type !== "file-read") return;
417
+ handleToolEvent(event, ctx);
418
+ };
419
+
420
+ /**
421
+ * Handle ls events.
422
+ */
423
+ const handleLsEvent = (event: DeepAgentEvent, ctx: EventHandlerContext) => {
424
+ if (event.type !== "ls") return;
425
+ handleToolEvent(event, ctx);
426
+ };
427
+
428
+ /**
429
+ * Handle glob events.
430
+ */
431
+ const handleGlobEvent = (
432
+ event: DeepAgentEvent,
433
+ ctx: EventHandlerContext
434
+ ) => {
435
+ if (event.type !== "glob") return;
436
+ handleToolEvent(event, ctx);
437
+ };
438
+
439
+ /**
440
+ * Handle grep events.
441
+ */
442
+ const handleGrepEvent = (
443
+ event: DeepAgentEvent,
444
+ ctx: EventHandlerContext
445
+ ) => {
446
+ if (event.type !== "grep") return;
447
+ handleToolEvent(event, ctx);
448
+ };
449
+
450
+ /**
451
+ * Handle web-search-start events.
452
+ */
453
+ const handleWebSearchStartEvent = (
454
+ event: DeepAgentEvent,
455
+ ctx: EventHandlerContext
456
+ ) => {
457
+ if (event.type !== "web-search-start") return;
458
+ handleToolEvent(event, ctx);
459
+ };
460
+
461
+ /**
462
+ * Handle web-search-finish events.
463
+ */
464
+ const handleWebSearchFinishEvent = (
465
+ event: DeepAgentEvent,
466
+ ctx: EventHandlerContext
467
+ ) => {
468
+ if (event.type !== "web-search-finish") return;
469
+ ctx.setStatus("tool-call");
470
+ ctx.addEvent(event);
471
+ };
472
+
473
+ /**
474
+ * Handle http-request-start events.
475
+ */
476
+ const handleHttpRequestStartEvent = (
477
+ event: DeepAgentEvent,
478
+ ctx: EventHandlerContext
479
+ ) => {
480
+ if (event.type !== "http-request-start") return;
481
+ handleToolEvent(event, ctx);
482
+ };
483
+
484
+ /**
485
+ * Handle http-request-finish events.
486
+ */
487
+ const handleHttpRequestFinishEvent = (
488
+ event: DeepAgentEvent,
489
+ ctx: EventHandlerContext
490
+ ) => {
491
+ if (event.type !== "http-request-finish") return;
492
+ ctx.setStatus("tool-call");
493
+ ctx.addEvent(event);
494
+ };
495
+
496
+ /**
497
+ * Handle fetch-url-start events.
498
+ */
499
+ const handleFetchUrlStartEvent = (
500
+ event: DeepAgentEvent,
501
+ ctx: EventHandlerContext
502
+ ) => {
503
+ if (event.type !== "fetch-url-start") return;
504
+ handleToolEvent(event, ctx);
505
+ };
506
+
507
+ /**
508
+ * Handle fetch-url-finish events.
509
+ */
510
+ const handleFetchUrlFinishEvent = (
511
+ event: DeepAgentEvent,
512
+ ctx: EventHandlerContext
513
+ ) => {
514
+ if (event.type !== "fetch-url-finish") return;
515
+ ctx.setStatus("tool-call");
516
+ ctx.addEvent(event);
517
+ };
518
+
519
+ /**
520
+ * Handle subagent-start events.
521
+ */
522
+ const handleSubagentStartEvent = (
523
+ event: DeepAgentEvent,
524
+ ctx: EventHandlerContext
525
+ ) => {
526
+ if (event.type !== "subagent-start") return;
527
+ ctx.setStatus("subagent");
528
+ ctx.addEvent(event);
529
+ };
530
+
531
+ /**
532
+ * Handle subagent-finish events.
533
+ */
534
+ const handleSubagentFinishEvent = (
535
+ event: DeepAgentEvent,
536
+ ctx: EventHandlerContext
537
+ ) => {
538
+ if (event.type !== "subagent-finish") return;
539
+ ctx.addEvent(event);
540
+ };
541
+
542
+ /**
543
+ * Handle approval-requested events.
544
+ * Already handled in onApprovalRequest callback, so no-op here.
545
+ */
546
+ const handleApprovalRequestedEvent = (
547
+ event: DeepAgentEvent,
548
+ ctx: EventHandlerContext
549
+ ) => {
550
+ // Approval request is handled in onApprovalRequest callback
551
+ // Event is already emitted there, no need to duplicate
552
+ };
553
+
554
+ /**
555
+ * Handle approval-response events.
556
+ * Already handled in respondToApproval callback, so no-op here.
557
+ */
558
+ const handleApprovalResponseEvent = (
559
+ event: DeepAgentEvent,
560
+ ctx: EventHandlerContext
561
+ ) => {
562
+ // Approval response is handled in respondToApproval callback
563
+ // Event is already emitted there, no need to duplicate
564
+ };
565
+
566
+ /**
567
+ * Handle done events.
568
+ * Flushes remaining text and updates final state.
569
+ */
570
+ const handleDoneEvent = (
571
+ event: DeepAgentEvent,
572
+ ctx: EventHandlerContext
573
+ ) => {
574
+ if (event.type !== "done") return;
575
+ // Flush any remaining text as a final text-segment
576
+ ctx.flushTextSegment();
577
+ ctx.setStatus("done");
578
+ ctx.setState(event.state);
579
+ // Update messages with the new conversation history
580
+ if (event.messages) {
581
+ ctx.setMessages(event.messages);
582
+ ctx.messagesRef.current = event.messages;
583
+ }
584
+ ctx.addEvent(event);
585
+ };
586
+
587
+ /**
588
+ * Handle error events.
589
+ * Flushes remaining text and marks pending tool calls as failed.
590
+ */
591
+ const handleErrorEvent = (
592
+ event: DeepAgentEvent,
593
+ ctx: EventHandlerContext
594
+ ) => {
595
+ if (event.type !== "error") return;
596
+ // Flush any remaining text before showing error
597
+ ctx.flushTextSegment();
598
+ ctx.setStatus("error");
599
+ ctx.setError(event.error);
600
+ // Mark any pending tool calls as failed
601
+ for (const [id, tc] of ctx.pendingToolCallsRef.current) {
602
+ tc.status = "error";
603
+ ctx.toolCallsRef.current.push(tc);
604
+ }
605
+ ctx.pendingToolCallsRef.current.clear();
606
+ ctx.setToolCalls([...ctx.toolCallsRef.current]);
607
+ ctx.addEvent(event);
608
+ };
609
+
610
+ /**
611
+ * Type-safe event handler function.
612
+ */
613
+ type EventHandler = (event: DeepAgentEvent, ctx: EventHandlerContext) => void;
614
+
615
+ /**
616
+ * Event handler map.
617
+ * Maps event types to their handler functions.
618
+ */
619
+ const EVENT_HANDLERS: Record<string, EventHandler> = {
620
+ "text": handleTextEvent,
621
+ "step-start": handleStepStartEvent,
622
+ "tool-call": handleToolCallEvent,
623
+ "tool-result": handleToolResultEvent,
624
+ "todos-changed": handleTodosChangedEvent,
625
+ "file-write-start": handleFileWriteStartEvent,
626
+ "file-written": handleFileWrittenEvent,
627
+ "file-edited": handleFileEditedEvent,
628
+ "file-read": handleFileReadEvent,
629
+ "ls": handleLsEvent,
630
+ "glob": handleGlobEvent,
631
+ "grep": handleGrepEvent,
632
+ "web-search-start": handleWebSearchStartEvent,
633
+ "web-search-finish": handleWebSearchFinishEvent,
634
+ "http-request-start": handleHttpRequestStartEvent,
635
+ "http-request-finish": handleHttpRequestFinishEvent,
636
+ "fetch-url-start": handleFetchUrlStartEvent,
637
+ "fetch-url-finish": handleFetchUrlFinishEvent,
638
+ "subagent-start": handleSubagentStartEvent,
639
+ "subagent-finish": handleSubagentFinishEvent,
640
+ "approval-requested": handleApprovalRequestedEvent,
641
+ "approval-response": handleApprovalResponseEvent,
642
+ "done": handleDoneEvent,
643
+ "error": handleErrorEvent,
644
+ };
645
+
646
+ // ============================================================================
647
+ // Main sendPrompt Function
648
+ // ============================================================================
649
+
650
+ const sendPrompt = useCallback(
651
+ async (prompt: string): Promise<{ text: string; toolCalls: ToolCallData[] }> => {
652
+ // Reset for new generation - but keep events for history
653
+ setStatus("thinking");
654
+ setStreamingText("");
655
+ // Don't clear events - they serve as conversation history
656
+ setToolCalls([]);
657
+ setError(null);
658
+ accumulatedTextRef.current = "";
659
+ totalTextRef.current = "";
660
+ toolCallsRef.current = [];
661
+ pendingToolCallsRef.current.clear();
662
+
663
+ // Add user message to events for history
664
+ addEvent({ type: "user-message", content: prompt });
665
+
666
+ // Sync messages ref with current state
667
+ messagesRef.current = messages;
668
+
669
+ // Create new abort controller
670
+ abortControllerRef.current = new AbortController();
671
+
672
+ // Build the input messages array for streamWithEvents
673
+ // Append the new prompt to the conversation history (if any exists)
674
+ const inputMessages = messagesRef.current.length > 0
675
+ ? [
676
+ ...messagesRef.current,
677
+ { role: "user", content: prompt } as ModelMessage,
678
+ ]
679
+ : [{ role: "user", content: prompt } as ModelMessage]; // First message: just the prompt
680
+
681
+ try {
682
+ for await (const event of agentRef.current.streamWithEvents({
683
+ messages: inputMessages, // Always use messages parameter (built with or without history)
684
+ state,
685
+ threadId: options.sessionId,
686
+ abortSignal: abortControllerRef.current.signal,
687
+ // Approval callback - auto-approve or prompt user
688
+ onApprovalRequest: async (request) => {
689
+ // If auto-approve is enabled, immediately approve
690
+ if (autoApproveEnabled) {
691
+ addEvent({
692
+ type: "approval-requested",
693
+ ...request,
694
+ });
695
+ addEvent({
696
+ type: "approval-response",
697
+ approvalId: request.approvalId,
698
+ approved: true
699
+ });
700
+ return true;
701
+ }
702
+
703
+ // Otherwise, show approval UI and wait for user response
704
+ setPendingApproval({
705
+ approvalId: request.approvalId,
706
+ toolName: request.toolName,
707
+ args: request.args,
708
+ });
709
+ addEvent({ type: "approval-requested", ...request });
710
+
711
+ // Return a promise that resolves when user responds
712
+ return new Promise<boolean>((resolve) => {
713
+ approvalResolverRef.current = resolve;
714
+ });
715
+ },
716
+ })) {
717
+ // Create event handler context
718
+ const eventHandlerContext: EventHandlerContext = {
719
+ setStatus,
720
+ setState,
721
+ setMessages,
722
+ setToolCalls,
723
+ setError,
724
+ addEvent,
725
+ flushTextSegment,
726
+ accumulatedTextRef,
727
+ totalTextRef,
728
+ toolCallsRef,
729
+ pendingToolCallsRef,
730
+ messagesRef,
731
+ };
732
+
733
+ // Get the handler for this event type
734
+ const handler = EVENT_HANDLERS[event.type];
735
+ if (handler) {
736
+ handler(event, eventHandlerContext);
737
+ }
738
+ }
739
+
740
+ // Save the final text and tool calls before resetting
741
+ const finalText = totalTextRef.current;
742
+ const finalToolCalls = [...toolCallsRef.current];
743
+ setLastCompletedText(finalText);
744
+ setStatus("idle");
745
+ return { text: finalText, toolCalls: finalToolCalls };
746
+ } catch (err) {
747
+ if ((err as Error).name === "AbortError") {
748
+ // Flush remaining text before aborting
749
+ flushTextSegment();
750
+ setStatus("idle");
751
+ return { text: totalTextRef.current, toolCalls: toolCallsRef.current };
752
+ } else {
753
+ // Flush remaining text before showing error
754
+ flushTextSegment();
755
+ setStatus("error");
756
+ setError(err as Error);
757
+ return { text: "", toolCalls: [] };
758
+ }
759
+ } finally {
760
+ abortControllerRef.current = null;
761
+ }
762
+ },
763
+ [state, messages, addEvent, flushTextSegment, autoApproveEnabled]
764
+ );
765
+
766
+
767
+ const abort = useCallback(() => {
768
+ if (abortControllerRef.current) {
769
+ abortControllerRef.current.abort();
770
+ setStatus("idle");
771
+ }
772
+ }, []);
773
+
774
+ const clear = useCallback(() => {
775
+ setEvents([]);
776
+ setStreamingText("");
777
+ setLastCompletedText("");
778
+ setMessages([]);
779
+ setToolCalls([]);
780
+ messagesRef.current = [];
781
+ toolCallsRef.current = [];
782
+ pendingToolCallsRef.current.clear();
783
+ setError(null);
784
+ setStatus("idle");
785
+ }, []);
786
+
787
+ const clearStreamingText = useCallback(() => {
788
+ setStreamingText("");
789
+ setEvents([]);
790
+ }, []);
791
+
792
+ const clearEvents = useCallback(() => {
793
+ setEvents([]);
794
+ }, []);
795
+
796
+ // Helper to recreate the agent with current settings
797
+ const recreateAgent = useCallback(
798
+ (overrides: {
799
+ model?: string;
800
+ promptCaching?: boolean;
801
+ evictionLimit?: number;
802
+ summarization?: SummarizationConfig;
803
+ interruptOn?: InterruptOnConfig | null; // null means explicitly no interruptOn (auto-approve)
804
+ } = {}) => {
805
+ const newModel = overrides.model ?? currentModel;
806
+ const newPromptCaching = overrides.promptCaching ?? promptCachingEnabled;
807
+ const newEvictionLimit = overrides.evictionLimit ?? evictionLimit;
808
+ const newSummarization = overrides.summarization ?? summarizationConfig;
809
+
810
+ // Handle interruptOn: null = explicit no approval, undefined = use current state
811
+ let newInterruptOn: InterruptOnConfig | undefined;
812
+ if (overrides.interruptOn === null) {
813
+ // Explicitly disable approval (auto-approve mode)
814
+ newInterruptOn = undefined;
815
+ } else if (overrides.interruptOn !== undefined) {
816
+ // Use the provided config
817
+ newInterruptOn = overrides.interruptOn;
818
+ } else {
819
+ // Use current state to determine
820
+ newInterruptOn = autoApproveEnabled ? undefined : (options.interruptOn ?? DEFAULT_CLI_INTERRUPT_ON);
821
+ }
822
+
823
+ agentRef.current = createDeepAgent({
824
+ model: parseModelString(newModel),
825
+ maxSteps: options.maxSteps,
826
+ systemPrompt: options.systemPrompt,
827
+ backend: options.backend,
828
+ enablePromptCaching: newPromptCaching,
829
+ toolResultEvictionLimit: newEvictionLimit,
830
+ summarization: newSummarization,
831
+ interruptOn: newInterruptOn,
832
+ });
833
+ },
834
+ [currentModel, promptCachingEnabled, evictionLimit, summarizationConfig, autoApproveEnabled, options.maxSteps, options.systemPrompt, options.backend, options.interruptOn]
835
+ );
836
+
837
+ const setModel = useCallback(
838
+ (model: string) => {
839
+ setCurrentModel(model);
840
+ recreateAgent({ model });
841
+ },
842
+ [recreateAgent]
843
+ );
844
+
845
+ const setPromptCaching = useCallback(
846
+ (enabled: boolean) => {
847
+ setPromptCachingEnabled(enabled);
848
+ recreateAgent({ promptCaching: enabled });
849
+ },
850
+ [recreateAgent]
851
+ );
852
+
853
+ const setEviction = useCallback(
854
+ (enabled: boolean) => {
855
+ const newLimit = enabled ? (options.toolResultEvictionLimit || 20000) : 0;
856
+ setEvictionLimit(newLimit);
857
+ recreateAgent({ evictionLimit: newLimit });
858
+ },
859
+ [recreateAgent, options.toolResultEvictionLimit]
860
+ );
861
+
862
+ const setSummarization = useCallback(
863
+ (enabled: boolean) => {
864
+ setSummarizationEnabled(enabled);
865
+ const newConfig = enabled
866
+ ? { enabled: true, tokenThreshold: options.summarization?.tokenThreshold, keepMessages: options.summarization?.keepMessages }
867
+ : undefined;
868
+ setSummarizationConfig(newConfig);
869
+ recreateAgent({ summarization: newConfig });
870
+ },
871
+ [recreateAgent, options.summarization]
872
+ );
873
+
874
+ // Respond to approval request
875
+ const respondToApproval = useCallback((approved: boolean) => {
876
+ if (approvalResolverRef.current) {
877
+ approvalResolverRef.current(approved);
878
+ approvalResolverRef.current = null;
879
+ const currentApproval = pendingApproval;
880
+ setPendingApproval(null);
881
+ if (currentApproval) {
882
+ addEvent({
883
+ type: "approval-response",
884
+ approvalId: currentApproval.approvalId,
885
+ approved
886
+ });
887
+ }
888
+ }
889
+ }, [addEvent]);
890
+
891
+ // Toggle auto-approve and recreate agent
892
+ const setAutoApprove = useCallback((enabled: boolean) => {
893
+ setAutoApproveEnabled(enabled);
894
+
895
+ // When enabling auto-approve, immediately approve any pending request
896
+ if (enabled && approvalResolverRef.current) {
897
+ respondToApproval(true);
898
+ }
899
+
900
+ // Recreate agent with/without interruptOn config
901
+ // Use null to explicitly disable approval, or the config to enable it
902
+ recreateAgent({
903
+ interruptOn: enabled ? null : (options.interruptOn ?? DEFAULT_CLI_INTERRUPT_ON)
904
+ });
905
+ }, [recreateAgent, options.interruptOn, respondToApproval]);
906
+
907
+ return {
908
+ status,
909
+ streamingText,
910
+ lastCompletedText,
911
+ events,
912
+ state,
913
+ messages,
914
+ toolCalls,
915
+ error,
916
+ sendPrompt,
917
+ abort,
918
+ clear,
919
+ clearStreamingText,
920
+ clearEvents,
921
+ setModel,
922
+ currentModel,
923
+ features,
924
+ setPromptCaching,
925
+ setEviction,
926
+ setSummarization,
927
+ pendingApproval,
928
+ respondToApproval,
929
+ autoApproveEnabled,
930
+ setAutoApprove,
931
+ };
932
+ }
933
+