@qduc/term2 0.1.6 → 0.1.8

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 (146) hide show
  1. package/dist/cli.js +5 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/components/BottomArea.d.ts +1 -8
  4. package/dist/components/BottomArea.d.ts.map +1 -1
  5. package/dist/components/BottomArea.js.map +1 -1
  6. package/dist/components/InputBox.d.ts +6 -0
  7. package/dist/components/InputBox.d.ts.map +1 -1
  8. package/dist/components/InputBox.js +25 -3
  9. package/dist/components/InputBox.js.map +1 -1
  10. package/dist/components/InputBox.test.js +10 -1
  11. package/dist/components/InputBox.test.js.map +1 -1
  12. package/dist/contracts/conversation.d.ts +27 -0
  13. package/dist/contracts/conversation.d.ts.map +1 -0
  14. package/dist/contracts/conversation.js +2 -0
  15. package/dist/contracts/conversation.js.map +1 -0
  16. package/dist/hooks/use-conversation.d.ts +3 -14
  17. package/dist/hooks/use-conversation.d.ts.map +1 -1
  18. package/dist/hooks/use-conversation.js +15 -87
  19. package/dist/hooks/use-conversation.js.map +1 -1
  20. package/dist/lib/openai-agent-client.d.ts.map +1 -1
  21. package/dist/lib/openai-agent-client.js +2 -1
  22. package/dist/lib/openai-agent-client.js.map +1 -1
  23. package/dist/lib/openai-strict-tool-schema.d.ts +10 -0
  24. package/dist/lib/openai-strict-tool-schema.d.ts.map +1 -0
  25. package/dist/lib/openai-strict-tool-schema.js +39 -0
  26. package/dist/lib/openai-strict-tool-schema.js.map +1 -0
  27. package/dist/lib/openai-strict-tool-schema.test.d.ts +2 -0
  28. package/dist/lib/openai-strict-tool-schema.test.d.ts.map +1 -0
  29. package/dist/lib/openai-strict-tool-schema.test.js +26 -0
  30. package/dist/lib/openai-strict-tool-schema.test.js.map +1 -0
  31. package/dist/lib/tool-invoke.d.ts +13 -0
  32. package/dist/lib/tool-invoke.d.ts.map +1 -1
  33. package/dist/lib/tool-invoke.js +61 -3
  34. package/dist/lib/tool-invoke.js.map +1 -1
  35. package/dist/lib/tool-invoke.test.js +139 -1
  36. package/dist/lib/tool-invoke.test.js.map +1 -1
  37. package/dist/prompts/simple.md +4 -0
  38. package/dist/providers/openai-compatible/model.js +3 -3
  39. package/dist/providers/openai-compatible/model.js.map +1 -1
  40. package/dist/providers/openai-compatible/reasoning-content.test.js +70 -0
  41. package/dist/providers/openai-compatible/reasoning-content.test.js.map +1 -1
  42. package/dist/providers/openrouter/model.js +3 -3
  43. package/dist/providers/openrouter/model.js.map +1 -1
  44. package/dist/providers/openrouter/utils.d.ts +1 -0
  45. package/dist/providers/openrouter/utils.d.ts.map +1 -1
  46. package/dist/providers/openrouter/utils.js +3 -0
  47. package/dist/providers/openrouter/utils.js.map +1 -1
  48. package/dist/providers/openrouter.test.js +64 -0
  49. package/dist/providers/openrouter.test.js.map +1 -1
  50. package/dist/services/approval-presentation-policy.d.ts +17 -0
  51. package/dist/services/approval-presentation-policy.d.ts.map +1 -0
  52. package/dist/services/approval-presentation-policy.js +44 -0
  53. package/dist/services/approval-presentation-policy.js.map +1 -0
  54. package/dist/services/approval-presentation-policy.test.d.ts +2 -0
  55. package/dist/services/approval-presentation-policy.test.d.ts.map +1 -0
  56. package/dist/services/approval-presentation-policy.test.js +74 -0
  57. package/dist/services/approval-presentation-policy.test.js.map +1 -0
  58. package/dist/services/approval-state.d.ts +4 -4
  59. package/dist/services/approval-state.d.ts.map +1 -1
  60. package/dist/services/conversation-events.d.ts +12 -9
  61. package/dist/services/conversation-events.d.ts.map +1 -1
  62. package/dist/services/conversation-service.d.ts +7 -5
  63. package/dist/services/conversation-service.d.ts.map +1 -1
  64. package/dist/services/conversation-service.js +1 -1
  65. package/dist/services/conversation-service.js.map +1 -1
  66. package/dist/services/conversation-session.d.ts +4 -24
  67. package/dist/services/conversation-session.d.ts.map +1 -1
  68. package/dist/services/conversation-session.js +172 -229
  69. package/dist/services/conversation-session.js.map +1 -1
  70. package/dist/services/conversation-store.d.ts +6 -0
  71. package/dist/services/conversation-store.d.ts.map +1 -1
  72. package/dist/services/conversation-store.js +13 -0
  73. package/dist/services/conversation-store.js.map +1 -1
  74. package/dist/tools/ask-mentor.d.ts +1 -1
  75. package/dist/tools/ask-mentor.js +1 -1
  76. package/dist/tools/ask-mentor.js.map +1 -1
  77. package/dist/tools/ask-mentor.test.js +9 -3
  78. package/dist/tools/ask-mentor.test.js.map +1 -1
  79. package/dist/tools/find-files.d.ts +2 -2
  80. package/dist/tools/find-files.d.ts.map +1 -1
  81. package/dist/tools/find-files.js +2 -4
  82. package/dist/tools/find-files.js.map +1 -1
  83. package/dist/tools/find-files.test.js +7 -19
  84. package/dist/tools/find-files.test.js.map +1 -1
  85. package/dist/tools/grep.d.ts +1 -1
  86. package/dist/tools/grep.d.ts.map +1 -1
  87. package/dist/tools/grep.js +1 -5
  88. package/dist/tools/grep.js.map +1 -1
  89. package/dist/tools/read-file.d.ts +2 -2
  90. package/dist/tools/read-file.d.ts.map +1 -1
  91. package/dist/tools/read-file.js +2 -4
  92. package/dist/tools/read-file.js.map +1 -1
  93. package/dist/tools/read-file.test.js +22 -13
  94. package/dist/tools/read-file.test.js.map +1 -1
  95. package/dist/tools/search-replace.d.ts +1 -1
  96. package/dist/tools/search-replace.d.ts.map +1 -1
  97. package/dist/tools/search-replace.js +158 -1
  98. package/dist/tools/search-replace.js.map +1 -1
  99. package/dist/tools/search-replace.test.js +177 -0
  100. package/dist/tools/search-replace.test.js.map +1 -1
  101. package/dist/tools/search.d.ts +4 -4
  102. package/dist/tools/search.d.ts.map +1 -1
  103. package/dist/tools/search.js +5 -14
  104. package/dist/tools/search.js.map +1 -1
  105. package/dist/tools/shell.d.ts +2 -2
  106. package/dist/tools/shell.d.ts.map +1 -1
  107. package/dist/tools/shell.js +2 -4
  108. package/dist/tools/shell.js.map +1 -1
  109. package/dist/tools/tool-capabilities.d.ts +6 -0
  110. package/dist/tools/tool-capabilities.d.ts.map +1 -0
  111. package/dist/tools/tool-capabilities.js +17 -0
  112. package/dist/tools/tool-capabilities.js.map +1 -0
  113. package/dist/tools/tool-parameter-schema.test.d.ts +2 -0
  114. package/dist/tools/tool-parameter-schema.test.d.ts.map +1 -0
  115. package/dist/tools/tool-parameter-schema.test.js +88 -0
  116. package/dist/tools/tool-parameter-schema.test.js.map +1 -0
  117. package/dist/tools/types.d.ts +4 -2
  118. package/dist/tools/types.d.ts.map +1 -1
  119. package/dist/tools/utils.d.ts.map +1 -1
  120. package/dist/tools/utils.js +4 -1
  121. package/dist/tools/utils.js.map +1 -1
  122. package/dist/tools/utils.test.d.ts +2 -0
  123. package/dist/tools/utils.test.d.ts.map +1 -0
  124. package/dist/tools/utils.test.js +26 -0
  125. package/dist/tools/utils.test.js.map +1 -0
  126. package/dist/tools/web-fetch.d.ts +3 -3
  127. package/dist/tools/web-fetch.d.ts.map +1 -1
  128. package/dist/tools/web-fetch.js +3 -2
  129. package/dist/tools/web-fetch.js.map +1 -1
  130. package/dist/utils/conversation-event-handler.d.ts +16 -10
  131. package/dist/utils/conversation-event-handler.d.ts.map +1 -1
  132. package/dist/utils/conversation-event-handler.js +4 -0
  133. package/dist/utils/conversation-event-handler.js.map +1 -1
  134. package/dist/utils/streaming-session-factory.d.ts +9 -9
  135. package/dist/utils/streaming-session-factory.d.ts.map +1 -1
  136. package/dist/utils/streaming-session-factory.js +7 -2
  137. package/dist/utils/streaming-session-factory.js.map +1 -1
  138. package/dist/utils/synchronized-output.d.ts +35 -0
  139. package/dist/utils/synchronized-output.d.ts.map +1 -0
  140. package/dist/utils/synchronized-output.js +66 -0
  141. package/dist/utils/synchronized-output.js.map +1 -0
  142. package/dist/utils/synchronized-output.test.d.ts +2 -0
  143. package/dist/utils/synchronized-output.test.d.ts.map +1 -0
  144. package/dist/utils/synchronized-output.test.js +70 -0
  145. package/dist/utils/synchronized-output.test.js.map +1 -0
  146. package/package.json +1 -1
@@ -7,6 +7,33 @@ import { extractReasoningDelta, extractTextDelta } from './stream-event-parsing.
7
7
  import { captureToolCallArguments, emitCommandMessagesFromItems } from './command-message-streaming.js';
8
8
  import { ApprovalState } from './approval-state.js';
9
9
  import { createInvalidToolCallDiagnostic } from './logging-contract.js';
10
+ const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
11
+ const getString = (record, key) => {
12
+ const value = record?.[key];
13
+ return typeof value === 'string' ? value : undefined;
14
+ };
15
+ const getMethod = (target, key) => {
16
+ const record = asRecord(target);
17
+ const candidate = record?.[key];
18
+ return typeof candidate === 'function' ? candidate : null;
19
+ };
20
+ const getCallIdFromObject = (value) => {
21
+ const record = asRecord(value);
22
+ const callId = getString(record, 'callId') ??
23
+ getString(record, 'call_id') ??
24
+ getString(record, 'tool_call_id') ??
25
+ getString(record, 'toolCallId') ??
26
+ getString(record, 'id');
27
+ if (callId) {
28
+ return callId;
29
+ }
30
+ const rawItem = asRecord(record?.rawItem);
31
+ return (getString(rawItem, 'callId') ??
32
+ getString(rawItem, 'call_id') ??
33
+ getString(rawItem, 'tool_call_id') ??
34
+ getString(rawItem, 'toolCallId') ??
35
+ getString(rawItem, 'id'));
36
+ };
10
37
  const getCommandFromArgs = (args) => {
11
38
  if (!args) {
12
39
  return '';
@@ -29,15 +56,16 @@ const getCommandFromArgs = (args) => {
29
56
  }
30
57
  }
31
58
  if (typeof args === 'object') {
59
+ const argsRecord = asRecord(args);
32
60
  // Handle shell tool's command parameter
33
- const cmdFromObject = 'command' in args ? String(args.command) : undefined;
61
+ const cmdFromObject = argsRecord?.command !== undefined ? String(argsRecord.command) : undefined;
34
62
  // Fallback for old 'commands' array format
35
- if ('commands' in args && Array.isArray(args.commands)) {
36
- return args.commands.join('\n');
63
+ if (Array.isArray(argsRecord?.commands)) {
64
+ return argsRecord.commands.join('\n');
37
65
  }
38
66
  let argsFromObject;
39
- if ('arguments' in args) {
40
- const rawArguments = args.arguments;
67
+ if (argsRecord?.arguments !== undefined) {
68
+ const rawArguments = argsRecord.arguments;
41
69
  if (typeof rawArguments === 'string') {
42
70
  try {
43
71
  argsFromObject = JSON.stringify(JSON.parse(rawArguments));
@@ -59,14 +87,17 @@ const getCommandFromArgs = (args) => {
59
87
  */
60
88
  const MAX_HALLUCINATION_RETRIES = 2;
61
89
  /**
62
- * Check if an error is a tool hallucination error (model called a non-existent tool)
90
+ * Check if an error is a recoverable model behavior error (hallucination, parsing error, etc.)
63
91
  */
64
- const isToolHallucinationError = (error) => {
92
+ const isRecoverableModelError = (error) => {
65
93
  if (!(error instanceof ModelBehaviorError)) {
66
94
  return false;
67
95
  }
68
96
  const message = error.message.toLowerCase();
69
- return message.includes('tool') && message.includes('not found');
97
+ return ((message.includes('tool') && message.includes('not found')) || // Hallucination
98
+ message.includes('model did not produce a final response') || // Give up/exhausted
99
+ message.includes('parsing tool arguments') || // Bad JSON
100
+ message.includes('valid json'));
70
101
  };
71
102
  const supportsConversationChaining = (providerId) => {
72
103
  const providerDef = getProvider(providerId);
@@ -82,45 +113,7 @@ export class ConversationSession {
82
113
  textDeltaCount = 0;
83
114
  reasoningDeltaCount = 0;
84
115
  toolCallArgumentsById = new Map();
85
- lastEventType = null;
86
- eventTypeCount = 0;
87
116
  emittedInvalidToolCallPackets = new Set();
88
- // private logStreamEvent = (eventType: string, eventData: any) => {
89
- // if (eventData.item) {
90
- // eventType = eventData.item.type;
91
- // eventData = eventData.item.rawItem;
92
- // // this.logStreamEvent(eventType, eventData);
93
- // }
94
- //
95
- // // Deduplicate consecutive identical event types
96
- // if (eventType !== this.lastEventType) {
97
- // if (this.lastEventType !== null && this.eventTypeCount > 0) {
98
- // this.logger.debug('Stream event summary', {
99
- // eventType: this.lastEventType,
100
- // count: this.eventTypeCount,
101
- // });
102
- // }
103
- // this.lastEventType = eventType;
104
- // this.eventTypeCount = 1;
105
- // // Log the first occurrence with details
106
- // this.logger.debug('Stream event', {
107
- // eventType,
108
- // ...eventData,
109
- // });
110
- // } else {
111
- // this.eventTypeCount++;
112
- // }
113
- // };
114
- flushStreamEventLog = () => {
115
- if (this.lastEventType !== null && this.eventTypeCount > 1) {
116
- this.logger.debug('Stream event summary', {
117
- eventType: this.lastEventType,
118
- count: this.eventTypeCount,
119
- });
120
- }
121
- this.lastEventType = null;
122
- this.eventTypeCount = 0;
123
- };
124
117
  constructor(id, { agentClient, deps }) {
125
118
  this.id = id;
126
119
  this.agentClient = agentClient;
@@ -133,27 +126,23 @@ export class ConversationSession {
133
126
  this.approvalState.clearPending();
134
127
  this.approvalState.consumeAborted();
135
128
  this.toolCallArgumentsById.clear();
136
- if (typeof this.agentClient.clearConversations === 'function') {
137
- this.agentClient.clearConversations();
138
- }
129
+ const clearConversations = getMethod(this.agentClient, 'clearConversations');
130
+ clearConversations?.call(this.agentClient);
139
131
  }
140
132
  setModel(model) {
141
133
  this.agentClient.setModel(model);
142
134
  }
143
135
  setReasoningEffort(effort) {
144
- if (typeof this.agentClient.setReasoningEffort === 'function') {
145
- this.agentClient.setReasoningEffort(effort);
146
- }
136
+ const setReasoningEffort = getMethod(this.agentClient, 'setReasoningEffort');
137
+ setReasoningEffort?.call(this.agentClient, effort);
147
138
  }
148
139
  setTemperature(temperature) {
149
- if (typeof this.agentClient.setTemperature === 'function') {
150
- this.agentClient.setTemperature(temperature);
151
- }
140
+ const setTemperature = getMethod(this.agentClient, 'setTemperature');
141
+ setTemperature?.call(this.agentClient, temperature);
152
142
  }
153
143
  setProvider(provider) {
154
- if (typeof this.agentClient.setProvider === 'function') {
155
- this.agentClient.setProvider(provider);
156
- }
144
+ const setProvider = getMethod(this.agentClient, 'setProvider');
145
+ setProvider?.call(this.agentClient, provider);
157
146
  }
158
147
  setRetryCallback(callback) {
159
148
  if (typeof this.agentClient.setRetryCallback === 'function') {
@@ -207,6 +196,7 @@ export class ConversationSession {
207
196
  message: text,
208
197
  });
209
198
  const { state, interruption, emittedCommandIds, toolCallArgumentsById } = abortedContext;
199
+ const interruptionRecord = asRecord(interruption);
210
200
  // Restore cached tool-call arguments captured before abort so continuation can attach them
211
201
  this.toolCallArgumentsById.clear();
212
202
  if (toolCallArgumentsById?.size) {
@@ -215,8 +205,8 @@ export class ConversationSession {
215
205
  }
216
206
  }
217
207
  // Add interceptor for this tool execution
218
- const toolName = interruption.name ?? 'unknown';
219
- const expectedCallId = interruption.rawItem?.callId ?? interruption.callId;
208
+ const toolName = getString(interruptionRecord, 'name') ?? 'unknown';
209
+ const expectedCallId = getCallIdFromObject(interruption);
220
210
  const rejectionMessage = `Tool execution was not approved. User provided new input instead: ${text}`;
221
211
  const removeInterceptor = this.agentClient.addToolInterceptor(async (name, _params, toolCallId) => {
222
212
  // Match both tool name and call ID for stricter matching
@@ -226,87 +216,35 @@ export class ConversationSession {
226
216
  }
227
217
  return null;
228
218
  });
229
- state.approve(interruption);
219
+ const approve = getMethod(state, 'approve');
220
+ approve?.call(state, interruption);
230
221
  try {
231
- const stream = await this.agentClient.continueRunStream(state, {
222
+ const continuedStream = (await this.agentClient.continueRunStream(state, {
232
223
  previousResponseId: this.previousResponseId,
233
- });
224
+ }));
234
225
  const acc = {
235
226
  finalOutput: '',
236
227
  reasoningOutput: '',
237
228
  emittedCommandIds: new Set(emittedCommandIds),
238
229
  latestUsage: undefined,
239
230
  };
240
- yield* this.#streamEvents(stream, acc, {
231
+ yield* this.#streamEvents(continuedStream, acc, {
241
232
  preserveExistingToolArgs: true,
242
233
  });
243
- this.previousResponseId = stream.lastResponseId;
244
- this.conversationStore.updateFromResult(stream);
234
+ this.previousResponseId = continuedStream.lastResponseId ?? null;
235
+ this.conversationStore.updateFromResult(continuedStream);
245
236
  // Check if another interruption occurred
246
- if (stream.interruptions && stream.interruptions.length > 0) {
237
+ if (continuedStream.interruptions && continuedStream.interruptions.length > 0) {
247
238
  this.logger.warn('Another interruption occurred after fake execution - handling as approval');
248
239
  // Let the normal flow handle this
249
- const result = this.#buildResult(stream, acc.finalOutput, acc.reasoningOutput, acc.emittedCommandIds, acc.latestUsage);
250
- // Re-emit the terminal event explicitly.
251
- if (result.type === 'approval_required') {
252
- const interruption = result.approval.rawInterruption;
253
- const callId = interruption?.rawItem?.callId ??
254
- interruption?.callId ??
255
- interruption?.call_id ??
256
- interruption?.tool_call_id ??
257
- interruption?.toolCallId ??
258
- interruption?.id;
259
- yield {
260
- type: 'approval_required',
261
- approval: {
262
- agentName: result.approval.agentName,
263
- toolName: result.approval.toolName,
264
- argumentsText: result.approval.argumentsText,
265
- ...(callId ? { callId: String(callId) } : {}),
266
- },
267
- };
268
- }
269
- else {
270
- yield {
271
- type: 'final',
272
- finalText: result.finalText,
273
- ...(result.reasoningText ? { reasoningText: result.reasoningText } : {}),
274
- ...(result.commandMessages?.length ? { commandMessages: result.commandMessages } : {}),
275
- ...(result.usage ? { usage: result.usage } : {}),
276
- };
277
- }
240
+ const result = this.#buildResult(continuedStream, acc.finalOutput, acc.reasoningOutput, acc.emittedCommandIds, acc.latestUsage);
241
+ yield this.#toTerminalEvent(result);
278
242
  return;
279
243
  }
280
244
  // Successfully resolved - agent should now have processed the fake rejection
281
245
  this.logger.debug('Fake execution completed, agent received rejection message');
282
- const result = this.#buildResult(stream, acc.finalOutput, acc.reasoningOutput, acc.emittedCommandIds, acc.latestUsage);
283
- if (result.type === 'approval_required') {
284
- const interruption = result.approval.rawInterruption;
285
- const callId = interruption?.rawItem?.callId ??
286
- interruption?.callId ??
287
- interruption?.call_id ??
288
- interruption?.tool_call_id ??
289
- interruption?.toolCallId ??
290
- interruption?.id;
291
- yield {
292
- type: 'approval_required',
293
- approval: {
294
- agentName: result.approval.agentName,
295
- toolName: result.approval.toolName,
296
- argumentsText: result.approval.argumentsText,
297
- ...(callId ? { callId: String(callId) } : {}),
298
- },
299
- };
300
- }
301
- else {
302
- yield {
303
- type: 'final',
304
- finalText: result.finalText,
305
- ...(result.reasoningText ? { reasoningText: result.reasoningText } : {}),
306
- ...(result.commandMessages?.length ? { commandMessages: result.commandMessages } : {}),
307
- ...(result.usage ? { usage: result.usage } : {}),
308
- };
309
- }
246
+ const result = this.#buildResult(continuedStream, acc.finalOutput, acc.reasoningOutput, acc.emittedCommandIds, acc.latestUsage);
247
+ yield this.#toTerminalEvent(result);
310
248
  return;
311
249
  }
312
250
  catch (error) {
@@ -321,13 +259,12 @@ export class ConversationSession {
321
259
  }
322
260
  }
323
261
  // Normal message flow
324
- const provider = typeof this.agentClient.getProvider === 'function'
325
- ? this.agentClient.getProvider()
326
- : 'openai';
262
+ const getProvider = getMethod(this.agentClient, 'getProvider');
263
+ const provider = getProvider ? getProvider.call(this.agentClient) : 'openai';
327
264
  const supportsChaining = supportsConversationChaining(provider);
328
- stream = await this.agentClient.startStream(supportsChaining ? text : this.conversationStore.getHistory(), {
265
+ stream = (await this.agentClient.startStream(supportsChaining ? text : this.conversationStore.getHistory(), {
329
266
  previousResponseId: this.previousResponseId,
330
- });
267
+ }));
331
268
  const acc = {
332
269
  finalOutput: '',
333
270
  reasoningOutput: '',
@@ -337,7 +274,7 @@ export class ConversationSession {
337
274
  yield* this.#streamEvents(stream, acc, {
338
275
  preserveExistingToolArgs: false,
339
276
  });
340
- this.previousResponseId = stream.lastResponseId;
277
+ this.previousResponseId = stream.lastResponseId ?? null;
341
278
  this.conversationStore.updateFromResult(stream);
342
279
  // Build terminal event (approval_required or final)
343
280
  const result = this.#buildResult(stream, acc.finalOutput || undefined, acc.reasoningOutput || undefined, acc.emittedCommandIds, acc.latestUsage);
@@ -350,59 +287,47 @@ export class ConversationSession {
350
287
  traceId: this.logger.getCorrelationId(),
351
288
  toolName: result.approval.toolName,
352
289
  });
353
- const interruption = result.approval.rawInterruption;
354
- const callId = interruption?.rawItem?.callId ??
355
- interruption?.callId ??
356
- interruption?.call_id ??
357
- interruption?.tool_call_id ??
358
- interruption?.toolCallId ??
359
- interruption?.id;
360
- yield {
361
- type: 'approval_required',
362
- approval: {
363
- agentName: result.approval.agentName,
364
- toolName: result.approval.toolName,
365
- argumentsText: result.approval.argumentsText,
366
- ...(callId ? { callId: String(callId) } : {}),
367
- },
368
- };
290
+ yield this.#toTerminalEvent(result);
369
291
  return;
370
292
  }
371
- yield {
372
- type: 'final',
373
- finalText: result.finalText,
374
- ...(result.reasoningText ? { reasoningText: result.reasoningText } : {}),
375
- ...(result.commandMessages?.length ? { commandMessages: result.commandMessages } : {}),
376
- ...(result.usage ? { usage: result.usage } : {}),
377
- };
293
+ yield this.#toTerminalEvent(result);
378
294
  }
379
295
  catch (error) {
380
- // Handle tool hallucination: model called a non-existent tool
381
- if (isToolHallucinationError(error) && hallucinationRetryCount < MAX_HALLUCINATION_RETRIES) {
382
- const toolName = error instanceof Error ? error.message.match(/Tool (\S+) not found/)?.[1] || 'unknown' : 'unknown';
383
- this.logger.warn('Tool hallucination detected, retrying', {
384
- eventType: 'retry.hallucination',
296
+ // Handle recoverable model errors (hallucination, parsing error, etc.)
297
+ if (isRecoverableModelError(error) && hallucinationRetryCount < MAX_HALLUCINATION_RETRIES) {
298
+ const message = error instanceof Error ? error.message : String(error);
299
+ const isHallucination = message.toLowerCase().includes('not found');
300
+ const isParsingError = message.toLowerCase().includes('parsing tool arguments') || message.toLowerCase().includes('valid json');
301
+ const toolName = isHallucination ? message.match(/Tool (\S+) not found/)?.[1] || 'unknown' : 'unknown';
302
+ this.logger.warn('Recoverable model error detected, retrying', {
303
+ eventType: 'retry.model_error',
385
304
  category: 'retry',
386
305
  phase: 'retry',
387
306
  toolName,
388
- retryType: 'hallucination',
307
+ retryType: isHallucination ? 'hallucination' : isParsingError ? 'parsing_error' : 'behavior',
389
308
  retryAttempt: hallucinationRetryCount + 1,
390
309
  attempt: hallucinationRetryCount + 1,
391
310
  maxRetries: MAX_HALLUCINATION_RETRIES,
392
311
  sessionId: this.id,
393
312
  traceId: this.logger.getCorrelationId(),
394
- errorMessage: error instanceof Error ? error.message : String(error),
313
+ errorMessage: message,
395
314
  });
396
315
  yield {
397
316
  type: 'retry',
398
- toolName,
317
+ toolName: isHallucination ? toolName : 'model',
399
318
  attempt: hallucinationRetryCount + 1,
400
319
  maxRetries: MAX_HALLUCINATION_RETRIES,
401
- errorMessage: error instanceof Error ? error.message : String(error),
320
+ errorMessage: message,
402
321
  };
403
322
  if (stream) {
404
- // Update conversation store with partial results (successful tool calls)
323
+ // Update conversation store with partial results (including the parse error output if it was yielded)
405
324
  this.conversationStore.updateFromResult(stream);
325
+ // If stream produced no usable history, inject error context so the
326
+ // model has explicit feedback about the failure on retry.
327
+ const streamHistory = Array.isArray(stream.history) ? stream.history : [];
328
+ if (streamHistory.length === 0) {
329
+ this.conversationStore.addErrorContext(`[System: Previous attempt failed with error: ${message}. Please retry with corrected output.]`);
330
+ }
406
331
  // Retry from current state without re-adding user message
407
332
  yield* this.run(text, {
408
333
  hallucinationRetryCount: hallucinationRetryCount + 1,
@@ -445,6 +370,7 @@ export class ConversationSession {
445
370
  return;
446
371
  }
447
372
  const { state, interruption, emittedCommandIds: previouslyEmittedIds, toolCallArgumentsById, } = pendingApprovalContext;
373
+ const interruptionRecord = asRecord(interruption);
448
374
  let removeInterceptor = null;
449
375
  if (answer === 'y') {
450
376
  this.logger.info('Tool approval granted', {
@@ -454,11 +380,12 @@ export class ConversationSession {
454
380
  sessionId: this.id,
455
381
  traceId: this.logger.getCorrelationId(),
456
382
  });
457
- state.approve(interruption);
383
+ const approve = getMethod(state, 'approve');
384
+ approve?.call(state, interruption);
458
385
  }
459
386
  else {
460
- const toolName = interruption.name ?? 'unknown';
461
- const expectedCallId = interruption.rawItem?.callId ?? interruption.callId;
387
+ const toolName = getString(interruptionRecord, 'name') ?? 'unknown';
388
+ const expectedCallId = getCallIdFromObject(interruption);
462
389
  const rejectionMessage = rejectionReason
463
390
  ? `Tool execution was not approved. User's reason: ${rejectionReason}`
464
391
  : 'Tool execution was not approved.';
@@ -471,13 +398,15 @@ export class ConversationSession {
471
398
  return null;
472
399
  });
473
400
  // Approve to continue but interceptor will return rejection message
474
- state.approve(interruption);
401
+ const approve = getMethod(state, 'approve');
402
+ approve?.call(state, interruption);
475
403
  // Store interceptor cleanup for after stream
476
404
  this.approvalState.setPendingRemoveInterceptor(removeInterceptor);
477
405
  }
478
406
  else {
479
407
  // Fallback for clients without tool interceptors
480
- state.reject(interruption);
408
+ const reject = getMethod(state, 'reject');
409
+ reject?.call(state, interruption);
481
410
  }
482
411
  this.logger.info('Tool approval rejected', {
483
412
  eventType: 'approval.rejected',
@@ -496,9 +425,9 @@ export class ConversationSession {
496
425
  }
497
426
  }
498
427
  try {
499
- const stream = await this.agentClient.continueRunStream(state, {
428
+ const stream = (await this.agentClient.continueRunStream(state, {
500
429
  previousResponseId: this.previousResponseId,
501
- });
430
+ }));
502
431
  const acc = {
503
432
  finalOutput: '',
504
433
  reasoningOutput: '',
@@ -508,7 +437,7 @@ export class ConversationSession {
508
437
  yield* this.#streamEvents(stream, acc, {
509
438
  preserveExistingToolArgs: true,
510
439
  });
511
- this.previousResponseId = stream.lastResponseId;
440
+ this.previousResponseId = stream.lastResponseId ?? null;
512
441
  this.conversationStore.updateFromResult(stream);
513
442
  // Merge previously emitted command IDs with newly emitted ones
514
443
  // This prevents duplicates when result.history contains commands from the initial stream
@@ -523,31 +452,10 @@ export class ConversationSession {
523
452
  traceId: this.logger.getCorrelationId(),
524
453
  toolName: result.approval.toolName,
525
454
  });
526
- const interruption = result.approval.rawInterruption;
527
- const callId = interruption?.rawItem?.callId ??
528
- interruption?.callId ??
529
- interruption?.call_id ??
530
- interruption?.tool_call_id ??
531
- interruption?.toolCallId ??
532
- interruption?.id;
533
- yield {
534
- type: 'approval_required',
535
- approval: {
536
- agentName: result.approval.agentName,
537
- toolName: result.approval.toolName,
538
- argumentsText: result.approval.argumentsText,
539
- ...(callId ? { callId: String(callId) } : {}),
540
- },
541
- };
455
+ yield this.#toTerminalEvent(result);
542
456
  return;
543
457
  }
544
- yield {
545
- type: 'final',
546
- finalText: result.finalText,
547
- ...(result.reasoningText ? { reasoningText: result.reasoningText } : {}),
548
- ...(result.commandMessages?.length ? { commandMessages: result.commandMessages } : {}),
549
- ...(result.usage ? { usage: result.usage } : {}),
550
- };
458
+ yield this.#toTerminalEvent(result);
551
459
  }
552
460
  catch (error) {
553
461
  yield {
@@ -595,6 +503,7 @@ export class ConversationSession {
595
503
  toolName: event.approval.toolName,
596
504
  argumentsText: event.approval.argumentsText,
597
505
  rawInterruption,
506
+ callId: event.approval.callId,
598
507
  },
599
508
  };
600
509
  }
@@ -677,6 +586,7 @@ export class ConversationSession {
677
586
  toolName: event.approval.toolName,
678
587
  argumentsText: event.approval.argumentsText,
679
588
  rawInterruption,
589
+ callId: event.approval.callId,
680
590
  },
681
591
  };
682
592
  }
@@ -764,27 +674,37 @@ export class ConversationSession {
764
674
  fullText: acc.reasoningOutput,
765
675
  };
766
676
  };
767
- for await (const event of stream) {
768
- // Extract usage if present in any of the common locations
769
- const usage = extractUsage(event);
677
+ for await (const rawEvent of stream) {
678
+ const event = asRecord(rawEvent);
679
+ const eventData = asRecord(event?.data);
680
+ const eventType = getString(event, 'type');
681
+ // Extract usage if present in any of the common locations.
682
+ // Check both the top-level event and event.data, since raw_model_stream_event
683
+ // nests response data (including usage) under .data (e.g. data.response.usage).
684
+ const usage = extractUsage(rawEvent) ?? (eventData ? extractUsage(eventData) : undefined);
770
685
  if (usage) {
771
686
  acc.latestUsage = usage;
772
687
  this.logger.debug('Usage extracted from stream event', {
773
688
  sessionId: this.id,
774
689
  source: 'stream_event',
775
- eventType: event?.type ?? event?.data?.type ?? 'unknown',
690
+ eventType: eventType ?? getString(eventData, 'type') ?? 'unknown',
776
691
  usage,
777
692
  });
693
+ // Emit usage update event for real-time token tracking
694
+ yield {
695
+ type: 'usage_update',
696
+ usage,
697
+ };
778
698
  }
779
699
  // Log event type with deduplication for ordering understanding
780
- const delta1 = extractTextDelta(event);
700
+ const delta1 = extractTextDelta(rawEvent);
781
701
  if (delta1) {
782
702
  const e = emitText(delta1);
783
703
  if (e)
784
704
  yield e;
785
705
  }
786
- if (event?.data) {
787
- const delta2 = extractTextDelta(event.data);
706
+ if (eventData) {
707
+ const delta2 = extractTextDelta(eventData);
788
708
  if (delta2) {
789
709
  const e = emitText(delta2);
790
710
  if (e)
@@ -792,7 +712,7 @@ export class ConversationSession {
792
712
  }
793
713
  }
794
714
  // Handle reasoning items
795
- const reasoningDelta = extractReasoningDelta(event);
715
+ const reasoningDelta = extractReasoningDelta(rawEvent);
796
716
  if (reasoningDelta) {
797
717
  const e = emitReasoning(reasoningDelta);
798
718
  if (e)
@@ -802,15 +722,21 @@ export class ConversationSession {
802
722
  toolCallArgumentsById,
803
723
  emittedCommandIds: acc.emittedCommandIds,
804
724
  });
805
- if (event?.type === 'run_item_stream_event') {
806
- captureToolCallArguments(event.item, toolCallArgumentsById);
725
+ if (eventType === 'run_item_stream_event') {
726
+ const eventItem = event?.item;
727
+ const eventItemRecord = asRecord(eventItem);
728
+ captureToolCallArguments(eventItem, toolCallArgumentsById);
807
729
  // Emit tool_started event when a function_call is detected
808
- const rawItem = event.item?.rawItem ?? event.item;
809
- if (rawItem?.type === 'function_call') {
810
- const callId = rawItem.callId ?? rawItem.call_id ?? rawItem.tool_call_id ?? rawItem.toolCallId ?? rawItem.id;
730
+ const rawItem = asRecord(eventItemRecord?.rawItem) ?? eventItemRecord;
731
+ if (getString(rawItem, 'type') === 'function_call') {
732
+ const callId = getString(rawItem, 'callId') ??
733
+ getString(rawItem, 'call_id') ??
734
+ getString(rawItem, 'tool_call_id') ??
735
+ getString(rawItem, 'toolCallId') ??
736
+ getString(rawItem, 'id');
811
737
  if (callId) {
812
- const toolName = rawItem.name ?? event.item?.name;
813
- const args = rawItem.arguments ?? rawItem.args ?? event.item?.arguments ?? event.item?.args;
738
+ const toolName = getString(rawItem, 'name') ?? getString(eventItemRecord, 'name');
739
+ const args = rawItem?.arguments ?? rawItem?.args ?? eventItemRecord?.arguments ?? eventItemRecord?.args;
814
740
  // Providers sometimes surface arguments as a JSON string.
815
741
  // Normalize here so downstream UI (pending/running display)
816
742
  // can reliably render parameters.
@@ -871,13 +797,14 @@ export class ConversationSession {
871
797
  });
872
798
  }
873
799
  }
874
- for (const e of maybeEmitCommandMessagesFromItems([event.item])) {
800
+ for (const e of maybeEmitCommandMessagesFromItems([eventItem])) {
875
801
  yield e;
876
802
  }
877
803
  }
878
- else if (event?.type === 'tool_call_output_item' || event?.rawItem?.type === 'function_call_output') {
879
- captureToolCallArguments(event, toolCallArgumentsById);
880
- for (const e of maybeEmitCommandMessagesFromItems([event])) {
804
+ else if (eventType === 'tool_call_output_item' ||
805
+ getString(asRecord(event?.rawItem), 'type') === 'function_call_output') {
806
+ captureToolCallArguments(rawEvent, toolCallArgumentsById);
807
+ for (const e of maybeEmitCommandMessagesFromItems([rawEvent])) {
881
808
  yield e;
882
809
  }
883
810
  }
@@ -924,11 +851,30 @@ export class ConversationSession {
924
851
  usage: Boolean(completedResultRecord?.usage),
925
852
  usageMetadata: Boolean(completedResultRecord?.usageMetadata),
926
853
  usage_metadata: Boolean(completedResultRecord?.usage_metadata),
927
- responseUsage: Boolean(completedResultRecord?.response?.usage),
854
+ responseUsage: Boolean(asRecord(completedResultRecord?.response)?.usage),
928
855
  },
929
856
  });
930
857
  }
931
- this.flushStreamEventLog();
858
+ }
859
+ #toTerminalEvent(result) {
860
+ if (result.type === 'approval_required') {
861
+ return {
862
+ type: 'approval_required',
863
+ approval: {
864
+ agentName: result.approval.agentName,
865
+ toolName: result.approval.toolName,
866
+ argumentsText: result.approval.argumentsText,
867
+ ...(result.approval.callId ? { callId: result.approval.callId } : {}),
868
+ },
869
+ };
870
+ }
871
+ return {
872
+ type: 'final',
873
+ finalText: result.finalText,
874
+ ...(result.reasoningText ? { reasoningText: result.reasoningText } : {}),
875
+ ...(result.commandMessages?.length ? { commandMessages: result.commandMessages } : {}),
876
+ ...(result.usage ? { usage: result.usage } : {}),
877
+ };
932
878
  }
933
879
  #buildResult(result, finalOutputOverride, reasoningOutputOverride, emittedCommandIds, usage) {
934
880
  if (result.interruptions && result.interruptions.length > 0) {
@@ -940,29 +886,26 @@ export class ConversationSession {
940
886
  toolCallArgumentsById: new Map(this.toolCallArgumentsById),
941
887
  });
942
888
  let argumentsText = '';
943
- const toolName = interruption.name;
889
+ const interruptionRecord = asRecord(interruption);
890
+ const toolName = getString(interruptionRecord, 'name');
944
891
  // For shell_call (built-in shell tool), extract commands from action
945
892
  // For function tools (bash, shell), extract from arguments
946
- if (interruption.type === 'shell_call') {
947
- if (interruption.action?.commands) {
948
- argumentsText = Array.isArray(interruption.action.commands)
949
- ? interruption.action.commands.join('\n')
950
- : String(interruption.action.commands);
893
+ if (getString(interruptionRecord, 'type') === 'shell_call') {
894
+ const action = asRecord(interruptionRecord?.action);
895
+ const actionCommands = action?.commands;
896
+ if (actionCommands) {
897
+ argumentsText = Array.isArray(actionCommands) ? actionCommands.join('\n') : String(actionCommands);
951
898
  }
952
899
  }
953
900
  else {
954
- argumentsText = getCommandFromArgs(interruption.arguments);
901
+ argumentsText = getCommandFromArgs(interruptionRecord?.arguments);
955
902
  }
956
- const callId = interruption?.rawItem?.callId ??
957
- interruption?.callId ??
958
- interruption?.call_id ??
959
- interruption?.tool_call_id ??
960
- interruption?.toolCallId ??
961
- interruption?.id;
903
+ const agent = asRecord(interruptionRecord?.agent);
904
+ const callId = getCallIdFromObject(interruption);
962
905
  return {
963
906
  type: 'approval_required',
964
907
  approval: {
965
- agentName: interruption.agent?.name ?? 'Agent',
908
+ agentName: getString(agent, 'name') ?? 'Agent',
966
909
  toolName: toolName ?? 'Unknown Tool',
967
910
  argumentsText,
968
911
  rawInterruption: interruption,