1use super::*;
4
5fn tool_output_text(output: &ToolOutputItem) -> String {
6 if !output.output.is_empty() {
7 return output.output.clone();
8 }
9
10 output
11 .spool_path
12 .as_deref()
13 .map(|path| format!("Output saved to {path}"))
14 .unwrap_or_default()
15}
16
17impl ResponseBuilder {
18 pub fn process_event<E: StreamEventEmitter>(&mut self, event: &ThreadEvent, emitter: &mut E) {
19 match event {
20 ThreadEvent::ThreadStarted(_) => {
21 emitter.response_created(self.response.clone());
22 self.response.status = ResponseStatus::InProgress;
23 emitter.response_in_progress(self.response.clone());
24 self.normalized.response_started = true;
25 }
26
27 ThreadEvent::TurnStarted(_) => {
28 }
31
32 ThreadEvent::TurnCompleted(evt) => {
33 if self.response.status.is_terminal() {
34 return;
35 }
36 self.response.usage = Some(OpenUsage::from_exec_usage(&evt.usage).into());
37 self.response.status = ResponseStatus::Completed;
38 self.response.complete();
39 emitter.response_completed(self.response.clone());
40 }
41
42 ThreadEvent::TurnFailed(evt) => {
43 if self.response.status.is_terminal() {
44 return;
45 }
46 self.response.fail(OpenResponseError::model_error(&evt.message));
47 emitter.response_failed(self.response.clone());
48 }
49
50 ThreadEvent::TurnBlocked(evt) => {
51 self.emit_custom_event(
52 emitter,
53 "vtcode.turn_blocked",
54 json!({
55 "completed_at": evt.completed_at,
56 "message": evt.message,
57 "last_tool": evt.last_tool,
58 "blocked_streak": evt.blocked_streak,
59 "blocked_total": evt.blocked_total,
60 "consecutive_cap": evt.consecutive_cap,
61 "total_cap": evt.total_cap,
62 "recovery_active": evt.recovery_active,
63 }),
64 );
65 }
66
67 ThreadEvent::AppSandboxDenied(evt) => {
68 self.emit_custom_event(
69 emitter,
70 "vtcode.app_sandbox_denied",
71 json!({
72 "tool": evt.tool,
73 "tier": evt.tier,
74 "uris": evt.uris,
75 }),
76 );
77 }
78
79 ThreadEvent::ThreadCompleted(evt) => {
80 self.emit_custom_event(
81 emitter,
82 "vtcode.thread_completed",
83 json!({
84 "completed_at": evt.completed_at,
85 "thread_id": evt.thread_id,
86 "session_id": evt.session_id,
87 "subtype": evt.subtype.as_str(),
88 "outcome_code": evt.outcome_code,
89 "result": evt.result,
90 "stop_reason": evt.stop_reason,
91 "usage": evt.usage,
92 "total_cost_usd": evt.total_cost_usd,
93 "num_turns": evt.num_turns,
94 }),
95 );
96 }
97
98 ThreadEvent::ThreadCompactBoundary(evt) => {
99 self.emit_custom_event(
100 emitter,
101 "vtcode.thread_compact_boundary",
102 json!({
103 "thread_id": evt.thread_id,
104 "trigger": evt.trigger.as_str(),
105 "mode": evt.mode.as_str(),
106 "original_message_count": evt.original_message_count,
107 "compacted_message_count": evt.compacted_message_count,
108 "history_artifact_path": evt.history_artifact_path,
109 }),
110 );
111 }
112
113 ThreadEvent::ContextReset(evt) => {
114 self.emit_custom_event(
115 emitter,
116 "vtcode.context_reset",
117 json!({
118 "thread_id": evt.thread_id,
119 "turn_id": evt.turn_id,
120 "trigger": evt.trigger,
121 "plan_preserved": evt.plan_preserved,
122 "previous_context_usage_percent": evt.previous_context_usage_percent,
123 "tool_budget_reset": evt.tool_budget_reset,
124 }),
125 );
126 }
127
128 ThreadEvent::ItemStarted(evt) => {
129 self.handle_item_started(&evt.item, emitter);
130 }
131
132 ThreadEvent::ItemUpdated(evt) => {
133 self.handle_item_updated(&evt.item, emitter);
134 }
135
136 ThreadEvent::ItemCompleted(evt) => {
137 self.handle_item_completed(&evt.item, emitter);
138 }
139 ThreadEvent::PlanDelta(_) => {
140 }
144
145 ThreadEvent::PlanApprovalRequested(evt) => {
146 self.emit_custom_event(
147 emitter,
148 "vtcode.plan_approval_requested",
149 json!({
150 "thread_id": evt.thread_id,
151 "turn_id": evt.turn_id,
152 "plan_file": evt.plan_file,
153 }),
154 );
155 }
156
157 ThreadEvent::PlanApprovalResolved(evt) => {
158 self.emit_custom_event(
159 emitter,
160 "vtcode.plan_approval_resolved",
161 json!({
162 "thread_id": evt.thread_id,
163 "turn_id": evt.turn_id,
164 "decision": evt.decision,
165 "automatic": evt.automatic,
166 }),
167 );
168 }
169
170 ThreadEvent::Error(evt) => {
171 if self.response.status.is_terminal() {
172 return;
173 }
174 self.response.fail(OpenResponseError::server_error(&evt.message));
175 emitter.response_failed(self.response.clone());
176 }
177
178 ThreadEvent::Unknown
180 | ThreadEvent::PermissionRequested(_)
181 | ThreadEvent::PermissionResolved(_)
182 | ThreadEvent::Interjected(_)
183 | ThreadEvent::MatrixUpdated(_) => {}
184 }
185 }
186
187 fn handle_item_started<E: StreamEventEmitter>(&mut self, item: &ThreadItem, emitter: &mut E) {
188 let output_index = self.next_output_index;
189 self.next_output_index += 1;
190 self.item_id_to_index.insert(item.id.clone(), output_index);
191
192 let output_item = self.convert_thread_item(item, ItemStatus::InProgress);
193
194 let initial_text = match &item.details {
197 ThreadItemDetails::AgentMessage(msg) => msg.text.clone(),
198 ThreadItemDetails::Plan(plan) => plan.text.clone(),
199 ThreadItemDetails::Reasoning(r) => r.text.clone(),
200 ThreadItemDetails::ToolOutput(output) => tool_output_text(output),
201 _ => String::new(),
202 };
203 let active_state = ActiveItemState {
204 output_index,
205 content_index: 0,
206 prev_text: initial_text,
207 };
208 self.active_items.insert(item.id.clone(), active_state);
209
210 self.response.add_output(output_item.clone());
211 emitter.output_item_added(&self.response.id, output_index, output_item.clone());
212
213 if let OutputItem::Message(ref msg) = output_item
215 && !msg.content.is_empty()
216 {
217 emitter.emit(ResponseStreamEvent::ContentPartAdded {
218 response_id: self.response.id.clone(),
219 item_id: item.id.clone(),
220 output_index,
221 content_index: 0,
222 part: msg.content[0].clone(),
223 });
224 }
225 }
226
227 fn emit_custom_event<E: StreamEventEmitter>(&self, emitter: &mut E, event_type: &str, data: serde_json::Value) {
228 emitter.emit(ResponseStreamEvent::CustomEvent {
229 response_id: self.response.id.clone(),
230 event_type: event_type.to_string(),
231 sequence_number: self.next_output_index as u64,
232 data,
233 });
234 }
235
236 fn handle_item_updated<E: StreamEventEmitter>(&mut self, item: &ThreadItem, emitter: &mut E) {
237 let state = if let Some(state) = self.active_items.get_mut(&item.id) {
239 state
240 } else {
241 self.handle_item_started(item, emitter);
243 match self.active_items.get_mut(&item.id) {
244 Some(s) => s,
245 None => return,
246 }
247 };
248
249 match &item.details {
250 ThreadItemDetails::AgentMessage(msg) => {
251 let delta = if let Some(suffix) = msg.text.strip_prefix(&state.prev_text) {
253 suffix
254 } else {
255 &msg.text
257 };
258
259 if !delta.is_empty() {
260 emitter.output_text_delta(
261 &self.response.id,
262 &item.id,
263 state.output_index,
264 state.content_index,
265 delta,
266 );
267 state.prev_text = msg.text.clone();
268 }
269 }
270
271 ThreadItemDetails::Reasoning(r) => {
272 let delta = if let Some(suffix) = r.text.strip_prefix(&state.prev_text) {
274 suffix
275 } else {
276 &r.text
278 };
279
280 if !delta.is_empty() {
281 emitter.reasoning_delta(&self.response.id, &item.id, state.output_index, delta);
282 state.prev_text = r.text.clone();
283 }
284 }
285
286 ThreadItemDetails::ToolOutput(output) => {
287 let current_text = tool_output_text(output);
288 let delta = if let Some(suffix) = current_text.strip_prefix(&state.prev_text) {
289 suffix
290 } else {
291 current_text.as_str()
292 };
293
294 if !delta.is_empty() {
295 emitter.output_text_delta(
296 &self.response.id,
297 &item.id,
298 state.output_index,
299 state.content_index,
300 delta,
301 );
302 state.prev_text = current_text;
303 }
304 }
305
306 _ => {
307 }
309 }
310 }
311
312 fn handle_item_completed<E: StreamEventEmitter>(&mut self, item: &ThreadItem, emitter: &mut E) {
313 let (was_started, output_index) = match self.item_id_to_index.get(&item.id) {
314 Some(&idx) => (true, idx),
315 None => {
316 let idx = self.next_output_index;
318 self.next_output_index += 1;
319 self.item_id_to_index.insert(item.id.clone(), idx);
320 (false, idx)
321 }
322 };
323
324 let status = self.determine_item_status(&item.details);
326 let output_item = self.convert_thread_item(item, status);
327
328 if !was_started {
330 emitter.output_item_added(&self.response.id, output_index, output_item.clone());
331
332 match &output_item {
334 OutputItem::Message(msg) => {
335 if !msg.content.is_empty() {
336 emitter.emit(ResponseStreamEvent::ContentPartAdded {
337 response_id: self.response.id.clone(),
338 item_id: item.id.clone(),
339 output_index,
340 content_index: 0,
341 part: msg.content[0].clone(),
342 });
343 }
344 }
345 OutputItem::Reasoning(r) => {
346 let text = r.content.clone().unwrap_or_default();
347 emitter.emit(ResponseStreamEvent::ContentPartAdded {
348 response_id: self.response.id.clone(),
349 item_id: item.id.clone(),
350 output_index,
351 content_index: 0,
352 part: ContentPart::output_text(text),
353 });
354 }
355 _ => {}
356 }
357 }
358
359 if output_index < self.response.output.len() {
361 self.response.output[output_index] = output_item.clone();
362 } else {
363 self.response.add_output(output_item.clone());
364 }
365
366 match &output_item {
368 OutputItem::Message(msg) => {
369 if let Some(ContentPart::OutputText(text_content)) = msg.content.first() {
371 emitter.emit(ResponseStreamEvent::OutputTextDone {
372 response_id: self.response.id.clone(),
373 item_id: item.id.clone(),
374 output_index,
375 content_index: 0,
376 text: text_content.text.clone(),
377 });
378 emitter.emit(ResponseStreamEvent::ContentPartDone {
379 response_id: self.response.id.clone(),
380 item_id: item.id.clone(),
381 output_index,
382 content_index: 0,
383 part: msg.content[0].clone(),
384 });
385 }
386 }
387 OutputItem::Reasoning(r) => {
388 emitter.emit(ResponseStreamEvent::ReasoningDone {
390 response_id: self.response.id.clone(),
391 item_id: item.id.clone(),
392 output_index,
393 item: output_item.clone(),
394 });
395 let text = r.content.clone().unwrap_or_default();
396 emitter.emit(ResponseStreamEvent::ContentPartDone {
397 response_id: self.response.id.clone(),
398 item_id: item.id.clone(),
399 output_index,
400 content_index: 0,
401 part: ContentPart::output_text(text),
402 });
403 }
404 OutputItem::FunctionCall(fc) => {
405 if let Ok(args_str) = serde_json::to_string(&fc.arguments) {
407 emitter.emit(ResponseStreamEvent::FunctionCallArgumentsDone {
408 response_id: self.response.id.clone(),
409 item_id: item.id.clone(),
410 output_index,
411 arguments: args_str,
412 });
413 }
414 }
415 OutputItem::FunctionCallOutput(fco) if !fco.output.is_empty() => {
416 emitter.emit(ResponseStreamEvent::OutputTextDone {
417 response_id: self.response.id.clone(),
418 item_id: item.id.clone(),
419 output_index,
420 content_index: 0,
421 text: fco.output.clone(),
422 });
423 }
424 _ => {}
425 }
426
427 self.active_items.remove(&item.id);
429
430 emitter.output_item_done(&self.response.id, output_index, output_item);
431 }
432
433 fn determine_item_status(&self, details: &ThreadItemDetails) -> ItemStatus {
434 match details {
435 ThreadItemDetails::CommandExecution(cmd) => match cmd.status {
436 CommandExecutionStatus::Completed => ItemStatus::Completed,
437 CommandExecutionStatus::Failed => ItemStatus::Failed,
438 CommandExecutionStatus::InProgress => ItemStatus::InProgress,
439 },
440 ThreadItemDetails::ToolInvocation(invocation) => match invocation.status {
441 vtcode_exec_events::ToolCallStatus::Completed => ItemStatus::Completed,
442 vtcode_exec_events::ToolCallStatus::Failed => ItemStatus::Failed,
443 vtcode_exec_events::ToolCallStatus::InProgress => ItemStatus::InProgress,
444 },
445 ThreadItemDetails::ToolOutput(output) => match output.status {
446 vtcode_exec_events::ToolCallStatus::Completed => ItemStatus::Completed,
447 vtcode_exec_events::ToolCallStatus::Failed => ItemStatus::Failed,
448 vtcode_exec_events::ToolCallStatus::InProgress => ItemStatus::InProgress,
449 },
450 ThreadItemDetails::FileChange(fc) => match fc.status {
451 PatchApplyStatus::Completed => ItemStatus::Completed,
452 PatchApplyStatus::Failed => ItemStatus::Failed,
453 },
454 ThreadItemDetails::McpToolCall(tc) => match tc.status {
455 Some(McpToolCallStatus::Completed) => ItemStatus::Completed,
456 Some(McpToolCallStatus::Failed) => ItemStatus::Failed,
457 Some(McpToolCallStatus::Started) | None => ItemStatus::InProgress,
458 },
459 ThreadItemDetails::Error(_) => ItemStatus::Failed,
460 _ => ItemStatus::Completed,
461 }
462 }
463
464 fn resolve_tool_call_correlation_id(&mut self, harness_call_id: &str, raw_tool_call_id: Option<&str>) -> String {
465 if let Some(existing) = self.tool_call_correlation_ids.get(harness_call_id) {
466 return existing.clone();
467 }
468
469 let correlation_id = match raw_tool_call_id {
470 Some(raw_id) if self.used_tool_call_ids.insert(raw_id.to_string()) => raw_id.to_string(),
471 _ => harness_call_id.to_string(),
472 };
473 self.tool_call_correlation_ids
474 .insert(harness_call_id.to_string(), correlation_id.clone());
475 correlation_id
476 }
477
478 fn convert_thread_item(&mut self, item: &ThreadItem, status: ItemStatus) -> OutputItem {
479 match &item.details {
480 ThreadItemDetails::Decision(decision) => OutputItem::Custom(CustomItem {
481 id: item.id.clone().into(),
482 status,
483 custom_type: "vtcode:decision".into(),
484 data: json!({"decision": decision, "context": item.context}),
485 }),
486 ThreadItemDetails::AgentMessage(msg) => OutputItem::Message(MessageItem {
487 id: item.id.clone().into(),
488 status,
489 role: MessageRole::Assistant,
490 content: vec![ContentPart::output_text(&msg.text)],
491 }),
492
493 ThreadItemDetails::Reasoning(r) => OutputItem::Reasoning(ReasoningItem {
494 id: item.id.clone().into(),
495 status,
496 summary: None,
497 content: Some(r.text.clone()),
498 encrypted_content: None,
499 }),
500
501 ThreadItemDetails::Plan(plan) => OutputItem::Custom(CustomItem {
502 id: item.id.clone().into(),
503 status,
504 custom_type: "vtcode:plan".to_string(),
505 data: json!({
506 "text": plan.text,
507 }),
508 }),
509
510 ThreadItemDetails::CommandExecution(cmd) => OutputItem::Custom(CustomItem {
511 id: item.id.clone().into(),
512 status,
513 custom_type: "vtcode:command_execution".to_string(),
514 data: json!({
515 "command": cmd.command,
516 "arguments": cmd.arguments,
517 "aggregated_output": cmd.aggregated_output,
518 "exit_code": cmd.exit_code,
519 "status": serde_json::to_value(&cmd.status).unwrap_or(serde_json::Value::Null),
520 }),
521 }),
522
523 ThreadItemDetails::ToolInvocation(invocation) => OutputItem::FunctionCall(FunctionCallItem {
524 id: item.id.clone().into(),
525 status,
526 name: invocation.tool_name.clone(),
527 arguments: invocation.arguments.clone().unwrap_or(json!({})),
528 call_id: Some(self.resolve_tool_call_correlation_id(&item.id, invocation.tool_call_id.as_deref())),
529 }),
530
531 ThreadItemDetails::ToolOutput(output) => {
532 OutputItem::FunctionCallOutput(crate::open_responses::FunctionCallOutputItem {
533 id: item.id.clone().into(),
534 status,
535 call_id: Some(
536 self.resolve_tool_call_correlation_id(&output.call_id, output.tool_call_id.as_deref()),
537 ),
538 output: tool_output_text(output),
539 })
540 }
541
542 ThreadItemDetails::FileChange(fc) => {
543 let changes: Vec<_> = fc
544 .changes
545 .iter()
546 .map(|c| {
547 json!({
548 "path": c.path,
549 "kind": format!("{:?}", c.kind).to_lowercase(),
550 })
551 })
552 .collect();
553
554 OutputItem::Custom(CustomItem {
555 id: item.id.clone().into(),
556 status,
557 custom_type: "vtcode:file_change".to_string(),
558 data: json!({
559 "changes": changes,
560 "status": format!("{:?}", fc.status).to_lowercase(),
561 }),
562 })
563 }
564
565 ThreadItemDetails::McpToolCall(tc) => OutputItem::FunctionCall(FunctionCallItem {
566 id: item.id.clone().into(),
567 status,
568 name: tc.tool_name.clone(),
569 arguments: tc.arguments.clone().unwrap_or(json!({})),
570 call_id: Some(item.id.clone()),
571 }),
572
573 ThreadItemDetails::WebSearch(ws) => OutputItem::Custom(CustomItem {
574 id: item.id.clone().into(),
575 status,
576 custom_type: "vtcode:web_search".to_string(),
577 data: json!({
578 "query": ws.query,
579 "provider": ws.provider,
580 "results": ws.results,
581 }),
582 }),
583
584 ThreadItemDetails::Harness(event) => OutputItem::Custom(CustomItem {
585 id: item.id.clone().into(),
586 status,
587 custom_type: "vtcode:harness_event".to_string(),
588 data: json!({
589 "event": serde_json::to_value(&event.event).unwrap_or(serde_json::Value::Null),
590 "message": event.message,
591 "command": event.command,
592 "path": event.path,
593 "exit_code": event.exit_code,
594 }),
595 }),
596
597 ThreadItemDetails::Error(err) => {
598 OutputItem::Custom(CustomItem {
600 id: item.id.clone().into(),
601 status: ItemStatus::Failed,
602 custom_type: "vtcode:error".to_string(),
603 data: json!({
604 "message": err.message,
605 }),
606 })
607 }
608 }
609 }
610}