Skip to content

Commit c7a52dd

Browse files
authored
feat: preserve typed tool status in API streams (#83)
1 parent d39c96a commit c7a52dd

5 files changed

Lines changed: 60 additions & 9 deletions

File tree

docs/public-api-surface.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub struct OutboundMessage {
130130
| `AgentThought` | `chat_id`, `thought`, `background_job_id` |
131131
| `AgentUsage` | `chat_id`, `model`, `prompt_tokens`, `completion_tokens`, `total_tokens`, `background_job_id` |
132132
| `ToolCallStarted` | `chat_id`, `tool_name`, `args`, `background_job_id` |
133-
| `ToolCallFinished` | `chat_id`, `tool_name`, `result`, `background_job_id` |
133+
| `ToolCallFinished` | `chat_id`, `tool_name`, `result`, `is_error`, `background_job_id` |
134134
| `ToolProgress` | `chat_id`, `channel`, `tool_name`, `tool_call_id`, `message`, `background_job_id` |
135135
| `CronTrigger` | `job_id`, `message` |
136136
| `ExecutionRunFinished` | `chat_id`, `channel`, `provider_id`, `session_id`, `exit_code`, `duration_ms`, `stdout_len`, `stderr_len`, `artifact_count`, `git_head`, `description` |

src/agent/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3988,6 +3988,7 @@ impl AgentLogic {
39883988
chat_id: inbound.chat_id.clone(),
39893989
tool_name: tool_name.clone(),
39903990
result: tool_result_text.clone(),
3991+
is_error,
39913992
background_job_id: crate::bus::get_background_job_id(&inbound.metadata),
39923993
};
39933994
let _ = outbound_tx.send(BusMessage::Telemetry(tfin.clone())).await;
@@ -4098,6 +4099,7 @@ impl AgentLogic {
40984099
chat_id: inbound.chat_id.clone(),
40994100
tool_name: tn,
41004101
result: tool_result_text.clone(),
4102+
is_error,
41014103
background_job_id: crate::bus::get_background_job_id(&inbound.metadata),
41024104
};
41034105
let _ = outbound_tx.send(BusMessage::Telemetry(tfin.clone())).await;

src/bus.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ pub enum TelemetryEvent {
181181
chat_id: String,
182182
tool_name: String,
183183
result: String,
184+
/// Authoritative executor status. Defaults to success so telemetry
185+
/// persisted before this field was added still deserializes.
186+
#[serde(default)]
187+
is_error: bool,
184188
background_job_id: Option<String>,
185189
},
186190
/// Mid–tool-call status (e.g. uv-managed Python env setup); not a tool result.
@@ -535,7 +539,7 @@ fn redact_chat_id(chat_id: &str) -> String {
535539

536540
#[cfg(test)]
537541
mod tests {
538-
use super::{clarification_session_key, InboundMessage, LogEvent, LogLevel};
542+
use super::{clarification_session_key, InboundMessage, LogEvent, LogLevel, TelemetryEvent};
539543
use crate::tool_runtime::ToolExecCtx;
540544

541545
#[test]
@@ -559,6 +563,28 @@ mod tests {
559563
assert_eq!(inbound.clarification_session_key(), "api:x:");
560564
}
561565

566+
#[test]
567+
fn legacy_tool_completion_defaults_to_success_status() {
568+
let encoded = serde_json::json!({
569+
"ToolCallFinished": {
570+
"chat_id": "chat-1",
571+
"tool_name": "read_file",
572+
"result": "Error: literal file content",
573+
"background_job_id": null
574+
}
575+
});
576+
let event: TelemetryEvent =
577+
serde_json::from_value(encoded).expect("deserialize legacy telemetry");
578+
579+
assert!(matches!(
580+
event,
581+
TelemetryEvent::ToolCallFinished {
582+
is_error: false,
583+
..
584+
}
585+
));
586+
}
587+
562588
#[test]
563589
fn format_line_masks_email_chat_ids() {
564590
let line = LogEvent {

src/channels/api.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ enum StreamEvent {
9292
ToolCallFinished {
9393
tool_name: String,
9494
result: String,
95+
is_error: bool,
9596
},
9697
AgentThought {
9798
thought: String,
@@ -652,14 +653,16 @@ impl ApiChannel {
652653
chat_id,
653654
tool_name,
654655
result,
656+
is_error,
655657
..
656658
} => {
657659
if let Some(pending) = self.pending_requests.get(&chat_id) {
658660
if let PendingRequest::Stream(pending) = pending.value() {
659-
if let Err(e) = pending
660-
.stream_tx
661-
.try_send(StreamEvent::ToolCallFinished { tool_name, result })
662-
{
661+
if let Err(e) = pending.stream_tx.try_send(StreamEvent::ToolCallFinished {
662+
tool_name,
663+
result,
664+
is_error,
665+
}) {
663666
error!("Failed to send tool_call_finished to stream: {}", e);
664667
}
665668
}
@@ -2567,7 +2570,7 @@ fn log_api(logger_tx: &LoggerHandle, event: LogEvent) {
25672570
mod tests {
25682571
use super::{
25692572
bearer_token_matches, build_router, is_loopback_bind, validate_bind_security, ApiState,
2570-
PendingRequest, EMBEDDED_UI_ASSETS,
2573+
PendingRequest, StreamEvent, EMBEDDED_UI_ASSETS,
25712574
};
25722575
use crate::bus::{BusMessage, OutboundMessage};
25732576
use crate::channels::api_store::ResponseStore;
@@ -2589,6 +2592,24 @@ mod tests {
25892592
use tokio::sync::{mpsc, oneshot};
25902593
use tower::ServiceExt;
25912594

2595+
#[test]
2596+
fn streamed_tool_completion_preserves_typed_error_status() {
2597+
for (result, is_error) in [
2598+
("Error: this is file content, not a failure", false),
2599+
("native failure without a text prefix", true),
2600+
] {
2601+
let event = StreamEvent::ToolCallFinished {
2602+
tool_name: "read_file".to_string(),
2603+
result: result.to_string(),
2604+
is_error,
2605+
};
2606+
let encoded = serde_json::to_value(event).expect("serialize stream event");
2607+
2608+
assert_eq!(encoded["type"], "tool_call_finished");
2609+
assert_eq!(encoded["is_error"], is_error);
2610+
}
2611+
}
2612+
25922613
struct LocalTempDir {
25932614
path: std::path::PathBuf,
25942615
}

src/logging.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -712,13 +712,15 @@ fn telemetry_to_log_event(telemetry: &TelemetryEvent) -> LogEvent {
712712
chat_id,
713713
tool_name,
714714
result,
715+
is_error,
715716
..
716717
} => LogEvent::debug(
717718
"Telemetry",
718719
&format!(
719-
"ToolCallFinished tool={} result_len={}",
720+
"ToolCallFinished tool={} result_len={} is_error={}",
720721
tool_name,
721-
result.len()
722+
result.len(),
723+
is_error
722724
),
723725
)
724726
.with_chat_id(chat_id),

0 commit comments

Comments
 (0)