mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00:00
Make tool-result telemetry limits configurable (#39779)
## What changed - Add `otel.tool_result.max_bytes`, defaulting to 2048 bytes, to control the text included in `codex.tool_result` logs independently of model-visible output limits. - Apply truncation centrally when emitting telemetry, preserve UTF-8 boundaries, and report whether the logged output was truncated. - Record typed tool names and namespaces, agent names, and process-local result sequence numbers while keeping tool arguments and output out of trace events. ## Testing - Cover configuration propagation, UTF-8-safe truncation, raw tool-output logging, and log/trace event routing and fields. GitOrigin-RevId: 89ca76e55f1a09b729e7770ce01f2e4d98f9a22d
This commit is contained in:
1 parent
5663754f62
commit
854cbb2fd4
39 files changed
+583
-310
No files matched your search
@@ -548,6 +548,9 @@ pub enum OtelExporterKind {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||||
#[schemars(deny_unknown_fields)]
|
#[schemars(deny_unknown_fields)]
|
||||||
pub struct OtelConfigToml {
|
pub struct OtelConfigToml {
|
||||||
|
/// Byte limit for tool-result log output; independent of model-visible output.
|
||||||
|
#[serde(default)]
|
||||||
|
pub tool_result: codex_protocol::config_types::ToolResultLogConfig,
|
||||||
/// Log user prompt in traces
|
/// Log user prompt in traces
|
||||||
pub log_user_prompt: Option<bool>,
|
pub log_user_prompt: Option<bool>,
|
||||||
|
|
||||||
@@ -573,6 +576,7 @@ pub struct OtelConfigToml {
|
|||||||
/// Effective OTEL settings after defaults are applied.
|
/// Effective OTEL settings after defaults are applied.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct OtelConfig {
|
pub struct OtelConfig {
|
||||||
|
pub tool_result: codex_protocol::config_types::ToolResultLogConfig,
|
||||||
pub log_user_prompt: bool,
|
pub log_user_prompt: bool,
|
||||||
pub environment: String,
|
pub environment: String,
|
||||||
pub exporter: OtelExporterKind,
|
pub exporter: OtelExporterKind,
|
||||||
@@ -585,6 +589,7 @@ pub struct OtelConfig {
|
|||||||
impl Default for OtelConfig {
|
impl Default for OtelConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
OtelConfig {
|
OtelConfig {
|
||||||
|
tool_result: Default::default(),
|
||||||
log_user_prompt: false,
|
log_user_prompt: false,
|
||||||
environment: DEFAULT_OTEL_ENVIRONMENT.to_owned(),
|
environment: DEFAULT_OTEL_ENVIRONMENT.to_owned(),
|
||||||
exporter: OtelExporterKind::None,
|
exporter: OtelExporterKind::None,
|
||||||
|
|||||||
@@ -2561,6 +2561,17 @@
|
|||||||
"description": "Attributes to add to every exported trace span.",
|
"description": "Attributes to add to every exported trace span.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"tool_result": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ToolResultLogConfig"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": {
|
||||||
|
"max_bytes": 2048
|
||||||
|
},
|
||||||
|
"description": "Byte limit for tool-result log output; independent of model-visible output."
|
||||||
|
},
|
||||||
"trace_exporter": {
|
"trace_exporter": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -3410,6 +3421,19 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ToolResultLogConfig": {
|
||||||
|
"description": "Limit for the text included in `codex.tool_result` log records. This does not affect model-visible output. Raising it can expose more tool data to logs.",
|
||||||
|
"properties": {
|
||||||
|
"max_bytes": {
|
||||||
|
"default": 2048,
|
||||||
|
"description": "Maximum UTF-8 bytes before the truncation notice. Defaults to 2048.",
|
||||||
|
"format": "uint",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ToolSuggestConfig": {
|
"ToolSuggestConfig": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -9554,6 +9554,7 @@ async fn trace_exporter_defaults_to_none_when_log_exporter_is_set() -> std::io::
|
|||||||
let fixture = create_test_fixture()?;
|
let fixture = create_test_fixture()?;
|
||||||
let mut cfg = fixture.cfg.clone();
|
let mut cfg = fixture.cfg.clone();
|
||||||
cfg.otel = Some(OtelConfigToml {
|
cfg.otel = Some(OtelConfigToml {
|
||||||
|
tool_result: toml::from_str("max_bytes = 8192").expect("tool-result logging config"),
|
||||||
exporter: Some(OtelExporterKind::OtlpHttp {
|
exporter: Some(OtelExporterKind::OtlpHttp {
|
||||||
endpoint: "http://localhost:14318/v1/logs".to_string(),
|
endpoint: "http://localhost:14318/v1/logs".to_string(),
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
@@ -9574,6 +9575,7 @@ async fn trace_exporter_defaults_to_none_when_log_exporter_is_set() -> std::io::
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(config.otel.tool_result.max_bytes, 8192);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
config.otel.exporter,
|
config.otel.exporter,
|
||||||
OtelExporterKind::OtlpHttp { .. }
|
OtelExporterKind::OtlpHttp { .. }
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub(crate) fn resolve_config(
|
|||||||
let tracestate = resolve_tracestate(config.tracestate, startup_warnings);
|
let tracestate = resolve_tracestate(config.tracestate, startup_warnings);
|
||||||
|
|
||||||
OtelConfig {
|
OtelConfig {
|
||||||
|
tool_result: config.tool_result,
|
||||||
log_user_prompt,
|
log_user_prompt,
|
||||||
environment,
|
environment,
|
||||||
exporter,
|
exporter,
|
||||||
|
|||||||
@@ -1067,7 +1067,8 @@ impl Session {
|
|||||||
terminal_type.clone(),
|
terminal_type.clone(),
|
||||||
session_configuration.session_source.clone(),
|
session_configuration.session_source.clone(),
|
||||||
)
|
)
|
||||||
.with_auth_env(auth_env_telemetry.to_otel_metadata());
|
.with_auth_env(auth_env_telemetry.to_otel_metadata())
|
||||||
|
.with_tool_result_log_config(config.otel.tool_result);
|
||||||
if let Some(service_name) = session_configuration.metrics_service_name.as_deref() {
|
if let Some(service_name) = session_configuration.metrics_service_name.as_deref() {
|
||||||
session_telemetry = session_telemetry.with_metrics_service_name(service_name);
|
session_telemetry = session_telemetry.with_metrics_service_name(service_name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use crate::mcp_tool_call::request_mcp_tool_user_approval;
|
|||||||
use crate::sandboxing::SandboxPermissions;
|
use crate::sandboxing::SandboxPermissions;
|
||||||
use crate::session::session::Session;
|
use crate::session::session::Session;
|
||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::turn_context::TurnContext;
|
||||||
use crate::tools::flat_tool_name;
|
|
||||||
use crate::tools::hook_names::HookToolName;
|
use crate::tools::hook_names::HookToolName;
|
||||||
use crate::tools::runtimes::apply_patch::ApplyPatchApprovalKey;
|
use crate::tools::runtimes::apply_patch::ApplyPatchApprovalKey;
|
||||||
use crate::tools::runtimes::unified_exec::UnifiedExecApprovalKey;
|
use crate::tools::runtimes::unified_exec::UnifiedExecApprovalKey;
|
||||||
@@ -778,9 +777,8 @@ fn record_resolution(ctx: &ApprovalContext, resolution: &ApprovalResolution) {
|
|||||||
ApprovalResolutionSource::Guardian => ToolDecisionSource::AutomatedReviewer,
|
ApprovalResolutionSource::Guardian => ToolDecisionSource::AutomatedReviewer,
|
||||||
ApprovalResolutionSource::User => ToolDecisionSource::User,
|
ApprovalResolutionSource::User => ToolDecisionSource::User,
|
||||||
};
|
};
|
||||||
let tool_name = flat_tool_name(&ctx.tool_name);
|
|
||||||
ctx.review_context.turn().session_telemetry.tool_decision(
|
ctx.review_context.turn().session_telemetry.tool_decision(
|
||||||
tool_name.as_ref(),
|
&ctx.tool_name,
|
||||||
&ctx.call_id,
|
&ctx.call_id,
|
||||||
&resolution.decision,
|
&resolution.decision,
|
||||||
Some(source),
|
Some(source),
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ use crate::original_image_detail::sanitize_original_image_detail;
|
|||||||
use crate::session::session::Session;
|
use crate::session::session::Session;
|
||||||
use crate::session::step_context::StepContext;
|
use crate::session::step_context::StepContext;
|
||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::turn_context::TurnContext;
|
||||||
use crate::tools::TELEMETRY_PREVIEW_MAX_BYTES;
|
|
||||||
use crate::tools::TELEMETRY_PREVIEW_MAX_LINES;
|
|
||||||
use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE;
|
|
||||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||||
use crate::unified_exec::format_output_omission_marker;
|
use crate::unified_exec::format_output_omission_marker;
|
||||||
use crate::unified_exec::resolve_max_tokens;
|
use crate::unified_exec::resolve_max_tokens;
|
||||||
@@ -21,7 +18,6 @@ use codex_utils_output_truncation::TruncationPolicy;
|
|||||||
use codex_utils_output_truncation::approx_token_count;
|
use codex_utils_output_truncation::approx_token_count;
|
||||||
use codex_utils_output_truncation::formatted_truncate_text;
|
use codex_utils_output_truncation::formatted_truncate_text;
|
||||||
use codex_utils_output_truncation::truncate_text;
|
use codex_utils_output_truncation::truncate_text;
|
||||||
use codex_utils_string::take_bytes_at_char_boundary;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
@@ -80,13 +76,16 @@ pub struct McpToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for McpToolOutput {
|
impl ToolOutput for McpToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
let payload = self.response_payload();
|
// Logging has its own budget; do not first apply the model-context budget.
|
||||||
let preview = payload.body.to_text().unwrap_or_else(|| {
|
let output = self.result.log_output();
|
||||||
serde_json::to_string(&self.result.content)
|
let wall_time_seconds = self.wall_time.as_secs_f64();
|
||||||
.unwrap_or_else(|err| format!("failed to serialize mcp result: {err}"))
|
let header = format!("Wall time: {wall_time_seconds:.4} seconds\nOutput:");
|
||||||
});
|
if output.is_empty() {
|
||||||
telemetry_preview(&preview)
|
header
|
||||||
|
} else {
|
||||||
|
format!("{header}\n{output}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -152,7 +151,7 @@ pub struct ToolSearchOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for ToolSearchOutput {
|
impl ToolOutput for ToolSearchOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
let tools = self
|
let tools = self
|
||||||
.tools
|
.tools
|
||||||
.iter()
|
.iter()
|
||||||
@@ -162,7 +161,7 @@ impl ToolOutput for ToolSearchOutput {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
telemetry_preview(&JsonValue::Array(tools).to_string())
|
JsonValue::Array(tools).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -219,10 +218,8 @@ impl FunctionToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for FunctionToolOutput {
|
impl ToolOutput for FunctionToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
telemetry_preview(
|
function_call_output_content_items_to_text(&self.body).unwrap_or_default()
|
||||||
&function_call_output_content_items_to_text(&self.body).unwrap_or_default(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -249,8 +246,8 @@ impl ApplyPatchToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for ApplyPatchToolOutput {
|
impl ToolOutput for ApplyPatchToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
telemetry_preview(&self.text)
|
self.text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -282,8 +279,8 @@ pub struct AbortedToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for AbortedToolOutput {
|
impl ToolOutput for AbortedToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
telemetry_preview(&self.message)
|
self.message.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -328,8 +325,16 @@ pub struct ExecCommandToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for ExecCommandToolOutput {
|
impl ToolOutput for ExecCommandToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
telemetry_preview(&self.response_text())
|
// The telemetry budget must not inherit the model's output-token limit.
|
||||||
|
let mut output = String::from_utf8_lossy(&self.raw_output).into_owned();
|
||||||
|
if let Some(omitted_bytes) = self.output_omitted_bytes {
|
||||||
|
let marker = format_output_omission_marker(omitted_bytes.get());
|
||||||
|
if !output.contains(&marker) {
|
||||||
|
output = format!("{marker}\n{output}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format!("{}\n{output}", self.response_header())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -447,7 +452,7 @@ impl ExecCommandToolOutput {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn response_text(&self) -> String {
|
fn response_header(&self) -> String {
|
||||||
let mut sections = Vec::new();
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
if !self.chunk_id.is_empty() {
|
if !self.chunk_id.is_empty() {
|
||||||
@@ -470,7 +475,11 @@ impl ExecCommandToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sections.push("Output:".to_string());
|
sections.push("Output:".to_string());
|
||||||
let header = sections.join("\n");
|
sections.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_text(&self) -> String {
|
||||||
|
let header = self.response_header();
|
||||||
let output_budget = (self.truncation_policy * 1.2)
|
let output_budget = (self.truncation_policy * 1.2)
|
||||||
.byte_budget()
|
.byte_budget()
|
||||||
.saturating_sub(header.len().saturating_add(/*rhs*/ 1));
|
.saturating_sub(header.len().saturating_add(/*rhs*/ 1));
|
||||||
@@ -524,46 +533,6 @@ fn function_tool_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn telemetry_preview(content: &str) -> String {
|
|
||||||
let truncated_slice = take_bytes_at_char_boundary(content, TELEMETRY_PREVIEW_MAX_BYTES);
|
|
||||||
let truncated_by_bytes = truncated_slice.len() < content.len();
|
|
||||||
|
|
||||||
let mut preview = String::new();
|
|
||||||
let mut lines_iter = truncated_slice.lines();
|
|
||||||
for idx in 0..TELEMETRY_PREVIEW_MAX_LINES {
|
|
||||||
match lines_iter.next() {
|
|
||||||
Some(line) => {
|
|
||||||
if idx > 0 {
|
|
||||||
preview.push('\n');
|
|
||||||
}
|
|
||||||
preview.push_str(line);
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let truncated_by_lines = lines_iter.next().is_some();
|
|
||||||
|
|
||||||
if !truncated_by_bytes && !truncated_by_lines {
|
|
||||||
return content.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if preview.len() < truncated_slice.len()
|
|
||||||
&& truncated_slice
|
|
||||||
.as_bytes()
|
|
||||||
.get(preview.len())
|
|
||||||
.is_some_and(|byte| *byte == b'\n')
|
|
||||||
{
|
|
||||||
preview.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if !preview.is_empty() && !preview.ends_with('\n') {
|
|
||||||
preview.push('\n');
|
|
||||||
}
|
|
||||||
preview.push_str(TELEMETRY_PREVIEW_TRUNCATION_NOTICE);
|
|
||||||
|
|
||||||
preview
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "context_tests.rs"]
|
#[path = "context_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -153,6 +153,13 @@ fn mcp_tool_output_response_item_truncates_large_structured_content() {
|
|||||||
truncation_policy: TruncationPolicy::Bytes(128),
|
truncation_policy: TruncationPolicy::Bytes(128),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output.log_output(),
|
||||||
|
format!(
|
||||||
|
"Wall time: 1.2500 seconds\nOutput:\n{}",
|
||||||
|
json!({"items": "large structured value ".repeat(1_000)})
|
||||||
|
)
|
||||||
|
);
|
||||||
let response = output.to_response_item(
|
let response = output.to_response_item(
|
||||||
"mcp-call-large",
|
"mcp-call-large",
|
||||||
&ToolPayload::Function {
|
&ToolPayload::Function {
|
||||||
@@ -384,51 +391,19 @@ fn log_preview_uses_content_items_when_plain_text_is_missing() {
|
|||||||
Some(true),
|
Some(true),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(output.log_preview(), "preview");
|
assert_eq!(output.log_output(), "preview");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
function_call_output_content_items_to_text(&output.body),
|
function_call_output_content_items_to_text(&output.body),
|
||||||
Some("preview".to_string())
|
Some("preview".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn telemetry_preview_returns_original_within_limits() {
|
|
||||||
let content = "short output";
|
|
||||||
assert_eq!(telemetry_preview(content), content);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn telemetry_preview_truncates_by_bytes() {
|
|
||||||
let content = "x".repeat(TELEMETRY_PREVIEW_MAX_BYTES + 8);
|
|
||||||
let preview = telemetry_preview(&content);
|
|
||||||
|
|
||||||
assert!(preview.contains(TELEMETRY_PREVIEW_TRUNCATION_NOTICE));
|
|
||||||
assert!(
|
|
||||||
preview.len()
|
|
||||||
<= TELEMETRY_PREVIEW_MAX_BYTES + TELEMETRY_PREVIEW_TRUNCATION_NOTICE.len() + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn telemetry_preview_truncates_by_lines() {
|
|
||||||
let content = (0..(TELEMETRY_PREVIEW_MAX_LINES + 5))
|
|
||||||
.map(|idx| format!("line {idx}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
let preview = telemetry_preview(&content);
|
|
||||||
let lines: Vec<&str> = preview.lines().collect();
|
|
||||||
|
|
||||||
assert!(lines.len() <= TELEMETRY_PREVIEW_MAX_LINES + 1);
|
|
||||||
assert_eq!(lines.last(), Some(&TELEMETRY_PREVIEW_TRUNCATION_NOTICE));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exec_command_tool_output_formats_truncated_response() {
|
fn exec_command_tool_output_formats_truncated_response() {
|
||||||
let payload = ToolPayload::Function {
|
let payload = ToolPayload::Function {
|
||||||
arguments: "{}".to_string(),
|
arguments: "{}".to_string(),
|
||||||
};
|
};
|
||||||
let response = ExecCommandToolOutput {
|
let output = ExecCommandToolOutput {
|
||||||
event_call_id: "call-42".to_string(),
|
event_call_id: "call-42".to_string(),
|
||||||
chunk_id: "abc123".to_string(),
|
chunk_id: "abc123".to_string(),
|
||||||
wall_time: std::time::Duration::from_millis(1250),
|
wall_time: std::time::Duration::from_millis(1250),
|
||||||
@@ -440,8 +415,12 @@ fn exec_command_tool_output_formats_truncated_response() {
|
|||||||
original_token_count: Some(10),
|
original_token_count: Some(10),
|
||||||
output_omitted_bytes: None,
|
output_omitted_bytes: None,
|
||||||
hook_command: None,
|
hook_command: None,
|
||||||
}
|
};
|
||||||
.to_response_item("call-42", &payload);
|
assert_eq!(
|
||||||
|
output.log_output(),
|
||||||
|
"Chunk ID: abc123\nWall time: 1.2500 seconds\nProcess exited with code 0\nOriginal token count: 10\nOutput:\ntoken one token two token three token four token five"
|
||||||
|
);
|
||||||
|
let response = output.to_response_item("call-42", &payload);
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||||
@@ -525,7 +504,7 @@ fn exec_command_tool_output_preserves_omission_metadata_when_truncated() {
|
|||||||
"z".repeat(/*n*/ 100)
|
"z".repeat(/*n*/ 100)
|
||||||
)
|
)
|
||||||
.into_bytes();
|
.into_bytes();
|
||||||
let response = ExecCommandToolOutput {
|
let mut output = ExecCommandToolOutput {
|
||||||
event_call_id: "call-omitted".to_string(),
|
event_call_id: "call-omitted".to_string(),
|
||||||
chunk_id: "abc123".to_string(),
|
chunk_id: "abc123".to_string(),
|
||||||
wall_time: std::time::Duration::from_millis(/*millis*/ 1250),
|
wall_time: std::time::Duration::from_millis(/*millis*/ 1250),
|
||||||
@@ -537,8 +516,23 @@ fn exec_command_tool_output_preserves_omission_metadata_when_truncated() {
|
|||||||
original_token_count: Some(42_000),
|
original_token_count: Some(42_000),
|
||||||
output_omitted_bytes: NonZeroUsize::new(/*n*/ 123_456),
|
output_omitted_bytes: NonZeroUsize::new(/*n*/ 123_456),
|
||||||
hook_command: None,
|
hook_command: None,
|
||||||
}
|
};
|
||||||
.to_response_item("call-omitted", &payload);
|
let expected_header = "Chunk ID: abc123\nWall time: 1.2500 seconds\nProcess exited with code 0\nOriginal token count: 42000\nOutput:\n";
|
||||||
|
assert_eq!(
|
||||||
|
output.log_output(),
|
||||||
|
format!(
|
||||||
|
"{expected_header}{}",
|
||||||
|
String::from_utf8_lossy(&output.raw_output)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let response = output.to_response_item("call-omitted", &payload);
|
||||||
|
|
||||||
|
// Collection may report omitted bytes without including the marker in its text.
|
||||||
|
output.raw_output = b"remaining output".to_vec();
|
||||||
|
assert_eq!(
|
||||||
|
output.log_output(),
|
||||||
|
format!("{expected_header}{marker}\nremaining output")
|
||||||
|
);
|
||||||
|
|
||||||
let ResponseInputItem::FunctionCallOutput { output, .. } = response else {
|
let ResponseInputItem::FunctionCallOutput { output, .. } = response else {
|
||||||
panic!("expected FunctionCallOutput");
|
panic!("expected FunctionCallOutput");
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const TOOL_NAME: &str = "curr_time";
|
|||||||
struct CurrentTimeOutput(CurrentTimeReminder);
|
struct CurrentTimeOutput(CurrentTimeReminder);
|
||||||
|
|
||||||
impl ToolOutput for CurrentTimeOutput {
|
impl ToolOutput for CurrentTimeOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
self.0.body()
|
self.0.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl GetContextRemainingOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for GetContextRemainingOutput {
|
impl ToolOutput for GetContextRemainingOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
self.fragment()
|
self.fragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ pub(crate) struct CloseAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for CloseAgentResult {
|
impl ToolOutput for CloseAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "close_agent")
|
tool_output_json_text(self, "close_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ pub(crate) struct ResumeAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for ResumeAgentResult {
|
impl ToolOutput for ResumeAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "resume_agent")
|
tool_output_json_text(self, "resume_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ pub(crate) struct SendInputResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for SendInputResult {
|
impl ToolOutput for SendInputResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "send_input")
|
tool_output_json_text(self, "send_input")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ pub(crate) struct SpawnAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for SpawnAgentResult {
|
impl ToolOutput for SpawnAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "spawn_agent")
|
tool_output_json_text(self, "spawn_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ pub(crate) struct WaitAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for WaitAgentResult {
|
impl ToolOutput for WaitAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "wait_agent")
|
tool_output_json_text(self, "wait_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ pub(crate) struct InterruptAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for InterruptAgentResult {
|
impl ToolOutput for InterruptAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "interrupt_agent")
|
tool_output_json_text(self, "interrupt_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub(crate) struct ListAgentsResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for ListAgentsResult {
|
impl ToolOutput for ListAgentsResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "list_agents")
|
tool_output_json_text(self, "list_agents")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ pub(crate) enum SpawnAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for SpawnAgentResult {
|
impl ToolOutput for SpawnAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "spawn_agent")
|
tool_output_json_text(self, "spawn_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ impl WaitAgentResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for WaitAgentResult {
|
impl ToolOutput for WaitAgentResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
tool_output_json_text(self, "wait_agent")
|
tool_output_json_text(self, "wait_agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub struct PlanToolOutput;
|
|||||||
const PLAN_UPDATED_MESSAGE: &str = "Plan updated";
|
const PLAN_UPDATED_MESSAGE: &str = "Plan updated";
|
||||||
|
|
||||||
impl ToolOutput for PlanToolOutput {
|
impl ToolOutput for PlanToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
PLAN_UPDATED_MESSAGE.to_string()
|
PLAN_UPDATED_MESSAGE.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ pub struct ViewImageOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for ViewImageOutput {
|
impl ToolOutput for ViewImageOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
format!("<image data URL omitted: {} bytes>", self.image_url.len())
|
format!("<image data URL omitted: {} bytes>", self.image_url.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ mod tests {
|
|||||||
unified_image_budget: false,
|
unified_image_budget: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(output.log_preview(), "<image data URL omitted: 25 bytes>");
|
assert_eq!(output.log_output(), "<image data URL omitted: 25 bytes>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -33,12 +33,6 @@ use codex_utils_output_truncation::truncate_text;
|
|||||||
pub(crate) use executed_tool_calls::ExecutedToolCallRecorder;
|
pub(crate) use executed_tool_calls::ExecutedToolCallRecorder;
|
||||||
pub use router::ToolRouter;
|
pub use router::ToolRouter;
|
||||||
|
|
||||||
// Telemetry preview limits: keep log events smaller than model budgets.
|
|
||||||
pub(crate) const TELEMETRY_PREVIEW_MAX_BYTES: usize = 2 * 1024; // 2 KiB
|
|
||||||
pub(crate) const TELEMETRY_PREVIEW_MAX_LINES: usize = 64; // lines
|
|
||||||
pub(crate) const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str =
|
|
||||||
"[... telemetry preview truncated ...]";
|
|
||||||
|
|
||||||
/// Legacy boundaries such as hook payloads, telemetry tags, and Responses tool
|
/// Legacy boundaries such as hook payloads, telemetry tags, and Responses tool
|
||||||
/// names still require a single flattened string. Keep comparisons and sorting
|
/// names still require a single flattened string. Keep comparisons and sorting
|
||||||
/// on `ToolName` itself; use this only when crossing those boundaries.
|
/// on `ToolName` itself; use this only when crossing those boundaries.
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ const ABANDONED_NETWORK_APPROVAL_MESSAGE: &str =
|
|||||||
pub(crate) struct NetworkApprovalSpec {
|
pub(crate) struct NetworkApprovalSpec {
|
||||||
pub network: Option<NetworkProxy>,
|
pub network: Option<NetworkProxy>,
|
||||||
pub trigger: GuardianNetworkAccessTrigger,
|
pub trigger: GuardianNetworkAccessTrigger,
|
||||||
|
/// Preserve the typed identity independently of Guardian's display-name payload.
|
||||||
|
pub tool_name: ToolName,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub environment_id: String,
|
pub environment_id: String,
|
||||||
pub permission_profile: PermissionProfile,
|
pub permission_profile: PermissionProfile,
|
||||||
@@ -229,6 +231,7 @@ struct ActiveNetworkApprovalCall {
|
|||||||
registration_id: String,
|
registration_id: String,
|
||||||
turn_id: String,
|
turn_id: String,
|
||||||
trigger: GuardianNetworkAccessTrigger,
|
trigger: GuardianNetworkAccessTrigger,
|
||||||
|
tool_name: ToolName,
|
||||||
command: String,
|
command: String,
|
||||||
environment_id: String,
|
environment_id: String,
|
||||||
permission_profile: PermissionProfile,
|
permission_profile: PermissionProfile,
|
||||||
@@ -730,8 +733,8 @@ impl NetworkApprovalService {
|
|||||||
|call| call.trigger.call_id.clone(),
|
|call| call.trigger.call_id.clone(),
|
||||||
);
|
);
|
||||||
let telemetry_tool_name = owner_call.as_ref().map_or_else(
|
let telemetry_tool_name = owner_call.as_ref().map_or_else(
|
||||||
|| "network_access".to_string(),
|
|| ToolName::plain("network_access"),
|
||||||
|call| call.trigger.tool_name.clone(),
|
|call| call.tool_name.clone(),
|
||||||
);
|
);
|
||||||
let action = ApprovalAction::NetworkAccess {
|
let action = ApprovalAction::NetworkAccess {
|
||||||
id: guardian_approval_id,
|
id: guardian_approval_id,
|
||||||
@@ -750,7 +753,7 @@ impl NetworkApprovalService {
|
|||||||
let approval_context = ApprovalContext {
|
let approval_context = ApprovalContext {
|
||||||
review_context: GuardianReviewContext::from(&turn_context),
|
review_context: GuardianReviewContext::from(&turn_context),
|
||||||
call_id: approval_call_id,
|
call_id: approval_call_id,
|
||||||
tool_name: ToolName::plain(telemetry_tool_name.clone()),
|
tool_name: telemetry_tool_name.clone(),
|
||||||
strict_auto_review,
|
strict_auto_review,
|
||||||
approval_reason: Some(prompt_reason),
|
approval_reason: Some(prompt_reason),
|
||||||
retry_reason: Some(policy_denial_message.clone()),
|
retry_reason: Some(policy_denial_message.clone()),
|
||||||
@@ -1026,6 +1029,7 @@ pub(crate) async fn begin_network_approval(
|
|||||||
let NetworkApprovalSpec {
|
let NetworkApprovalSpec {
|
||||||
network,
|
network,
|
||||||
trigger,
|
trigger,
|
||||||
|
tool_name,
|
||||||
command,
|
command,
|
||||||
environment_id,
|
environment_id,
|
||||||
permission_profile,
|
permission_profile,
|
||||||
@@ -1057,6 +1061,7 @@ pub(crate) async fn begin_network_approval(
|
|||||||
registration_id: registration_id.clone(),
|
registration_id: registration_id.clone(),
|
||||||
turn_id: turn_id.to_string(),
|
turn_id: turn_id.to_string(),
|
||||||
trigger,
|
trigger,
|
||||||
|
tool_name,
|
||||||
command,
|
command,
|
||||||
environment_id,
|
environment_id,
|
||||||
permission_profile,
|
permission_profile,
|
||||||
|
|||||||
@@ -478,6 +478,7 @@ async fn register_call_with_default_shell_trigger(
|
|||||||
.register_call(ActiveNetworkApprovalCall {
|
.register_call(ActiveNetworkApprovalCall {
|
||||||
registration_id: registration_id.to_string(),
|
registration_id: registration_id.to_string(),
|
||||||
turn_id: "turn-1".to_string(),
|
turn_id: "turn-1".to_string(),
|
||||||
|
tool_name: ToolName::plain("exec_command"),
|
||||||
trigger: GuardianNetworkAccessTrigger {
|
trigger: GuardianNetworkAccessTrigger {
|
||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: "exec_command".to_string(),
|
tool_name: "exec_command".to_string(),
|
||||||
@@ -500,9 +501,10 @@ async fn register_call_with_default_shell_trigger(
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn active_call_preserves_triggering_command_context() {
|
async fn active_call_preserves_triggering_command_context() {
|
||||||
let service = NetworkApprovalService::default();
|
let service = NetworkApprovalService::default();
|
||||||
|
let tool_name = ToolName::namespaced("mcp__example", "exec_command");
|
||||||
let expected = GuardianNetworkAccessTrigger {
|
let expected = GuardianNetworkAccessTrigger {
|
||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: "exec_command".to_string(),
|
tool_name: tool_name.to_string(),
|
||||||
command: vec!["curl".to_string(), "https://example.com".to_string()],
|
command: vec!["curl".to_string(), "https://example.com".to_string()],
|
||||||
cwd: PathUri::parse("file:///C:/repo").expect("valid Windows path URI"),
|
cwd: PathUri::parse("file:///C:/repo").expect("valid Windows path URI"),
|
||||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||||
@@ -516,6 +518,7 @@ async fn active_call_preserves_triggering_command_context() {
|
|||||||
registration_id: "registration-1".to_string(),
|
registration_id: "registration-1".to_string(),
|
||||||
turn_id: "turn-1".to_string(),
|
turn_id: "turn-1".to_string(),
|
||||||
trigger: expected.clone(),
|
trigger: expected.clone(),
|
||||||
|
tool_name: tool_name.clone(),
|
||||||
command: "curl https://example.com".to_string(),
|
command: "curl https://example.com".to_string(),
|
||||||
environment_id: "remote".to_string(),
|
environment_id: "remote".to_string(),
|
||||||
permission_profile: PermissionProfile::workspace_write(),
|
permission_profile: PermissionProfile::workspace_write(),
|
||||||
@@ -529,6 +532,7 @@ async fn active_call_preserves_triggering_command_context() {
|
|||||||
.expect("single active call should resolve");
|
.expect("single active call should resolve");
|
||||||
|
|
||||||
assert_eq!(&call.trigger, &expected);
|
assert_eq!(&call.trigger, &expected);
|
||||||
|
assert_eq!(call.tool_name, tool_name);
|
||||||
assert_eq!(call.command, "curl https://example.com");
|
assert_eq!(call.command, "curl https://example.com");
|
||||||
assert_eq!(call.environment_id, "remote");
|
assert_eq!(call.environment_id, "remote");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ impl ToolOrchestrator {
|
|||||||
already_approved = true;
|
already_approved = true;
|
||||||
} else {
|
} else {
|
||||||
otel.tool_decision(
|
otel.tool_decision(
|
||||||
&otel_tn,
|
&tool_ctx.tool_name,
|
||||||
otel_ci,
|
otel_ci,
|
||||||
&ReviewDecision::Approved,
|
&ReviewDecision::Approved,
|
||||||
Some(ToolDecisionSource::Config),
|
Some(ToolDecisionSource::Config),
|
||||||
|
|||||||
@@ -215,8 +215,8 @@ struct PostToolUseFeedbackOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for PostToolUseFeedbackOutput {
|
impl ToolOutput for PostToolUseFeedbackOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
self.original.log_preview()
|
self.original.log_output()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -482,7 +482,6 @@ impl ToolRegistry {
|
|||||||
terminal_outcome_reached: Option<Arc<AtomicBool>>,
|
terminal_outcome_reached: Option<Arc<AtomicBool>>,
|
||||||
) -> Result<AnyToolResult, FunctionCallError> {
|
) -> Result<AnyToolResult, FunctionCallError> {
|
||||||
let tool_name = invocation.tool_name.clone();
|
let tool_name = invocation.tool_name.clone();
|
||||||
let tool_name_flat = flat_tool_name(&tool_name);
|
|
||||||
let call_id_owned = invocation.call_id.clone();
|
let call_id_owned = invocation.call_id.clone();
|
||||||
let otel = invocation.turn.session_telemetry.clone();
|
let otel = invocation.turn.session_telemetry.clone();
|
||||||
let permission_profile = invocation.turn.permission_profile();
|
let permission_profile = invocation.turn.permission_profile();
|
||||||
@@ -520,7 +519,7 @@ impl ToolRegistry {
|
|||||||
let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
|
let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
|
||||||
let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
|
let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
|
||||||
otel.tool_result_with_tags(
|
otel.tool_result_with_tags(
|
||||||
tool_name_flat.as_ref(),
|
&tool_name,
|
||||||
&call_id_owned,
|
&call_id_owned,
|
||||||
log_payload.as_ref(),
|
log_payload.as_ref(),
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
@@ -550,7 +549,7 @@ impl ToolRegistry {
|
|||||||
let message = format!("tool {tool_name} invoked with incompatible payload");
|
let message = format!("tool {tool_name} invoked with incompatible payload");
|
||||||
let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
|
let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
|
||||||
otel.tool_result_with_tags(
|
otel.tool_result_with_tags(
|
||||||
tool_name_flat.as_ref(),
|
&tool_name,
|
||||||
&call_id_owned,
|
&call_id_owned,
|
||||||
log_payload.as_ref(),
|
log_payload.as_ref(),
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
@@ -639,37 +638,26 @@ impl ToolRegistry {
|
|||||||
tool_result_tags.push(("command_category", category));
|
tool_result_tags.push(("command_category", category));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response_cell = tokio::sync::Mutex::new(None);
|
|
||||||
let invocation_for_tool = invocation.clone();
|
|
||||||
let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
|
let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
|
||||||
|
|
||||||
let result = otel
|
let result = otel
|
||||||
.log_tool_result_with_tags(
|
.log_tool_result_with_tags(
|
||||||
tool_name_flat.as_ref(),
|
&tool_name,
|
||||||
&call_id_owned,
|
&call_id_owned,
|
||||||
log_payload.as_ref(),
|
log_payload.as_ref(),
|
||||||
&tool_result_tags,
|
&tool_result_tags,
|
||||||
&extra_trace_fields,
|
&extra_trace_fields,
|
||||||
|| {
|
|| handle_any_tool(tool.as_ref(), invocation.clone()),
|
||||||
let tool = tool.clone();
|
|result| {
|
||||||
let response_cell = &response_cell;
|
(
|
||||||
async move {
|
result.result.log_output(),
|
||||||
match handle_any_tool(tool.as_ref(), invocation_for_tool).await {
|
result.result.success_for_logging(),
|
||||||
Ok(result) => {
|
)
|
||||||
let preview = result.result.log_preview();
|
|
||||||
let success = result.result.success_for_logging();
|
|
||||||
let mut guard = response_cell.lock().await;
|
|
||||||
*guard = Some(result);
|
|
||||||
Ok((preview, success))
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let success = match &result {
|
let success = match &result {
|
||||||
Ok((_, success)) => *success,
|
Ok(result) => result.result.success_for_logging(),
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
};
|
};
|
||||||
if let Some(analytics) = control_tool_analytics.as_mut() {
|
if let Some(analytics) = control_tool_analytics.as_mut() {
|
||||||
@@ -681,9 +669,9 @@ impl ToolRegistry {
|
|||||||
}
|
}
|
||||||
emit_metric_for_tool_read(&invocation, success);
|
emit_metric_for_tool_read(&invocation, success);
|
||||||
let post_tool_use_payload = if success {
|
let post_tool_use_payload = if success {
|
||||||
let guard = response_cell.lock().await;
|
result
|
||||||
guard
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
.ok()
|
||||||
.and_then(|result| result.post_tool_use_payload.clone())
|
.and_then(|result| result.post_tool_use_payload.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -715,17 +703,7 @@ impl ToolRegistry {
|
|||||||
|
|
||||||
// A PostToolUse block rejects the result, not the already-completed tool execution.
|
// A PostToolUse block rejects the result, not the already-completed tool execution.
|
||||||
let lifecycle_outcome = match &result {
|
let lifecycle_outcome = match &result {
|
||||||
Ok(_) => {
|
Ok(_) => ToolCallOutcome::Completed { success },
|
||||||
let guard = response_cell.lock().await;
|
|
||||||
match guard.as_ref() {
|
|
||||||
Some(result) => ToolCallOutcome::Completed {
|
|
||||||
success: result.result.success_for_logging(),
|
|
||||||
},
|
|
||||||
None => ToolCallOutcome::Failed {
|
|
||||||
handler_executed: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => ToolCallOutcome::Failed {
|
Err(_) => ToolCallOutcome::Failed {
|
||||||
handler_executed: true,
|
handler_executed: true,
|
||||||
},
|
},
|
||||||
@@ -738,11 +716,7 @@ impl ToolRegistry {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(mut result) => {
|
||||||
let mut guard = response_cell.lock().await;
|
|
||||||
let mut result = guard.take().ok_or_else(|| {
|
|
||||||
FunctionCallError::Fatal("tool produced no output".to_string())
|
|
||||||
})?;
|
|
||||||
if let Some(outcome) = post_tool_use_outcome {
|
if let Some(outcome) = post_tool_use_outcome {
|
||||||
if outcome.should_block {
|
if outcome.should_block {
|
||||||
let message = outcome.feedback_message.unwrap_or_else(|| {
|
let message = outcome.feedback_message.unwrap_or_else(|| {
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
|
|||||||
managed_network_for_sandbox_permissions(req.network.as_ref(), sandbox_permissions)?;
|
managed_network_for_sandbox_permissions(req.network.as_ref(), sandbox_permissions)?;
|
||||||
Some(NetworkApprovalSpec {
|
Some(NetworkApprovalSpec {
|
||||||
network: Some(network.clone()),
|
network: Some(network.clone()),
|
||||||
|
tool_name: ctx.tool_name.clone(),
|
||||||
trigger: GuardianNetworkAccessTrigger {
|
trigger: GuardianNetworkAccessTrigger {
|
||||||
call_id: ctx.call_id.clone(),
|
call_id: ctx.call_id.clone(),
|
||||||
tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
|
tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use codex_features::Feature;
|
|||||||
use codex_otel::SessionTelemetry;
|
use codex_otel::SessionTelemetry;
|
||||||
use codex_otel::TelemetryAuthMode;
|
use codex_otel::TelemetryAuthMode;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::ToolName;
|
||||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||||
use codex_protocol::approvals::NetworkPolicyRuleAction;
|
use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||||
use codex_protocol::config_types::ServiceTier;
|
use codex_protocol::config_types::ServiceTier;
|
||||||
@@ -1080,6 +1081,9 @@ fn tool_decision_assertion<'a>(
|
|||||||
if !lower.contains("tool_name=exec_command") {
|
if !lower.contains("tool_name=exec_command") {
|
||||||
return Err("missing tool_name for exec_command".to_string());
|
return Err("missing tool_name for exec_command".to_string());
|
||||||
}
|
}
|
||||||
|
if !lower.contains("tool_namespace=functions") {
|
||||||
|
return Err("missing default tool namespace".to_string());
|
||||||
|
}
|
||||||
if !lower.contains(&format!("decision={expected_decision}")) {
|
if !lower.contains(&format!("decision={expected_decision}")) {
|
||||||
return Err(format!("unexpected decision for {call_id}"));
|
return Err(format!("unexpected decision for {call_id}"));
|
||||||
}
|
}
|
||||||
@@ -1154,7 +1158,7 @@ fn network_policy_decisions_omit_source_and_destination() {
|
|||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
telemetry.tool_decision(
|
telemetry.tool_decision(
|
||||||
"exec_command",
|
&ToolName::namespaced("mcp__example", "exec_command"),
|
||||||
call_id,
|
call_id,
|
||||||
&ReviewDecision::NetworkPolicyAmendment {
|
&ReviewDecision::NetworkPolicyAmendment {
|
||||||
network_policy_amendment: NetworkPolicyAmendment {
|
network_policy_amendment: NetworkPolicyAmendment {
|
||||||
@@ -1177,6 +1181,9 @@ fn network_policy_decisions_omit_source_and_destination() {
|
|||||||
if !line.contains("tool_name=exec_command") {
|
if !line.contains("tool_name=exec_command") {
|
||||||
return Err("missing triggering network tool name".to_string());
|
return Err("missing triggering network tool name".to_string());
|
||||||
}
|
}
|
||||||
|
if !line.contains("tool_namespace=mcp__example") {
|
||||||
|
return Err("missing triggering network tool namespace".to_string());
|
||||||
|
}
|
||||||
if !line.contains(&format!("decision={expected_decision}")) {
|
if !line.contains(&format!("decision={expected_decision}")) {
|
||||||
return Err(format!("unexpected network tool decision for {call_id}"));
|
return Err(format!("unexpected network tool decision for {call_id}"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,7 +641,7 @@ struct GeneratedImageOutput {
|
|||||||
|
|
||||||
impl ToolOutput for GeneratedImageOutput {
|
impl ToolOutput for GeneratedImageOutput {
|
||||||
/// Avoids copying image bytes into tool-call telemetry.
|
/// Avoids copying image bytes into tool-call telemetry.
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
"[generated image]".to_string()
|
"[generated image]".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ impl SearchOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for SearchOutput {
|
impl ToolOutput for SearchOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
"[standalone web search output]".to_string()
|
"[standalone web search output]".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,16 @@ use crate::metrics::runtime_metrics::RuntimeMetricsSummary;
|
|||||||
use crate::metrics::timer::Timer;
|
use crate::metrics::timer::Timer;
|
||||||
use crate::provider::OtelProvider;
|
use crate::provider::OtelProvider;
|
||||||
use crate::sanitize_metric_tag_value;
|
use crate::sanitize_metric_tag_value;
|
||||||
|
use crate::tool_result::ToolResultEvent;
|
||||||
|
use crate::tool_result::emit_tool_result;
|
||||||
use codex_api::AgentIdentityTelemetry;
|
use codex_api::AgentIdentityTelemetry;
|
||||||
use codex_api::ApiError;
|
use codex_api::ApiError;
|
||||||
use codex_api::ResponseEvent;
|
use codex_api::ResponseEvent;
|
||||||
|
use codex_protocol::AgentPath;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::ToolName;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
|
use codex_protocol::config_types::ToolResultLogConfig;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
@@ -50,7 +55,6 @@ use eventsource_stream::EventStreamError as StreamError;
|
|||||||
use opentelemetry_sdk::metrics::data::ResourceMetrics;
|
use opentelemetry_sdk::metrics::data::ResourceMetrics;
|
||||||
use reqwest::Error;
|
use reqwest::Error;
|
||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -87,6 +91,9 @@ pub struct AuthEnvTelemetryMetadata {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SessionTelemetryMetadata {
|
pub struct SessionTelemetryMetadata {
|
||||||
pub(crate) conversation_id: ThreadId,
|
pub(crate) conversation_id: ThreadId,
|
||||||
|
// Full canonical path when available; legacy nickname or thread ID otherwise.
|
||||||
|
// Keep this potentially user-chosen label on detailed logs, not trace-safe events.
|
||||||
|
pub(crate) agent_name: String,
|
||||||
pub(crate) auth_mode: Option<String>,
|
pub(crate) auth_mode: Option<String>,
|
||||||
pub(crate) auth_env: AuthEnvTelemetryMetadata,
|
pub(crate) auth_env: AuthEnvTelemetryMetadata,
|
||||||
pub(crate) account_id: Option<String>,
|
pub(crate) account_id: Option<String>,
|
||||||
@@ -105,12 +112,18 @@ pub struct SessionTelemetryMetadata {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SessionTelemetry {
|
pub struct SessionTelemetry {
|
||||||
|
tool_result_log_config: ToolResultLogConfig,
|
||||||
pub(crate) metadata: SessionTelemetryMetadata,
|
pub(crate) metadata: SessionTelemetryMetadata,
|
||||||
pub(crate) metrics: Option<MetricsClient>,
|
pub(crate) metrics: Option<MetricsClient>,
|
||||||
pub(crate) metrics_use_metadata_tags: bool,
|
pub(crate) metrics_use_metadata_tags: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionTelemetry {
|
impl SessionTelemetry {
|
||||||
|
pub fn with_tool_result_log_config(mut self, config: ToolResultLogConfig) -> Self {
|
||||||
|
self.tool_result_log_config = config;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_auth_env(mut self, auth_env: AuthEnvTelemetryMetadata) -> Self {
|
pub fn with_auth_env(mut self, auth_env: AuthEnvTelemetryMetadata) -> Self {
|
||||||
self.metadata.auth_env = auth_env;
|
self.metadata.auth_env = auth_env;
|
||||||
self
|
self
|
||||||
@@ -425,9 +438,22 @@ impl SessionTelemetry {
|
|||||||
terminal_type: String,
|
terminal_type: String,
|
||||||
session_source: SessionSource,
|
session_source: SessionSource,
|
||||||
) -> SessionTelemetry {
|
) -> SessionTelemetry {
|
||||||
|
let agent_name = session_source
|
||||||
|
.get_agent_path()
|
||||||
|
.map(|path| path.to_string())
|
||||||
|
.or_else(|| session_source.get_nickname())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
if session_source.is_non_root_agent() {
|
||||||
|
conversation_id.to_string()
|
||||||
|
} else {
|
||||||
|
AgentPath::ROOT.to_string()
|
||||||
|
}
|
||||||
|
});
|
||||||
Self {
|
Self {
|
||||||
|
tool_result_log_config: ToolResultLogConfig::default(),
|
||||||
metadata: SessionTelemetryMetadata {
|
metadata: SessionTelemetryMetadata {
|
||||||
conversation_id,
|
conversation_id,
|
||||||
|
agent_name,
|
||||||
auth_mode: auth_mode.map(|m| m.to_string()),
|
auth_mode: auth_mode.map(|m| m.to_string()),
|
||||||
auth_env: AuthEnvTelemetryMetadata::default(),
|
auth_env: AuthEnvTelemetryMetadata::default(),
|
||||||
account_id,
|
account_id,
|
||||||
@@ -1008,16 +1034,18 @@ impl SessionTelemetry {
|
|||||||
|
|
||||||
pub fn tool_decision(
|
pub fn tool_decision(
|
||||||
&self,
|
&self,
|
||||||
tool_name: &str,
|
tool_name: &ToolName,
|
||||||
call_id: &str,
|
call_id: &str,
|
||||||
decision: &ReviewDecision,
|
decision: &ReviewDecision,
|
||||||
source: Option<ToolDecisionSource>,
|
source: Option<ToolDecisionSource>,
|
||||||
) {
|
) {
|
||||||
|
let tool_namespace = crate::events::shared::tool_namespace(tool_name);
|
||||||
match source {
|
match source {
|
||||||
Some(source) => log_event!(
|
Some(source) => log_event!(
|
||||||
self,
|
self,
|
||||||
event.name = "codex.tool_decision",
|
event.name = "codex.tool_decision",
|
||||||
tool_name = %tool_name,
|
tool_name = %tool_name.name,
|
||||||
|
tool_namespace = %tool_namespace,
|
||||||
call_id = %call_id,
|
call_id = %call_id,
|
||||||
decision = %decision.to_opaque_string(),
|
decision = %decision.to_opaque_string(),
|
||||||
source = %source.to_string(),
|
source = %source.to_string(),
|
||||||
@@ -1025,7 +1053,8 @@ impl SessionTelemetry {
|
|||||||
None => log_event!(
|
None => log_event!(
|
||||||
self,
|
self,
|
||||||
event.name = "codex.tool_decision",
|
event.name = "codex.tool_decision",
|
||||||
tool_name = %tool_name,
|
tool_name = %tool_name.name,
|
||||||
|
tool_namespace = %tool_namespace,
|
||||||
call_id = %call_id,
|
call_id = %call_id,
|
||||||
decision = %decision.to_opaque_string(),
|
decision = %decision.to_opaque_string(),
|
||||||
),
|
),
|
||||||
@@ -1063,19 +1092,22 @@ impl SessionTelemetry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records a diagnostic representation and returns the original result. The temporary
|
||||||
|
/// output string is truncated and released before the caller resumes async work.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn log_tool_result_with_tags<F, Fut, E>(
|
pub async fn log_tool_result_with_tags<T, F, Fut, E>(
|
||||||
&self,
|
&self,
|
||||||
tool_name: &str,
|
tool_name: &ToolName,
|
||||||
call_id: &str,
|
call_id: &str,
|
||||||
arguments: &str,
|
arguments: &str,
|
||||||
extra_tags: &[(&str, &str)],
|
extra_tags: &[(&str, &str)],
|
||||||
extra_trace_fields: &[(&str, &str)],
|
extra_trace_fields: &[(&str, &str)],
|
||||||
f: F,
|
f: F,
|
||||||
) -> Result<(String, bool), E>
|
log_output: impl FnOnce(&T) -> (String, bool),
|
||||||
|
) -> Result<T, E>
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Fut,
|
F: FnOnce() -> Fut,
|
||||||
Fut: Future<Output = Result<(String, bool), E>>,
|
Fut: Future<Output = Result<T, E>>,
|
||||||
E: std::fmt::Display,
|
E: std::fmt::Display,
|
||||||
{
|
{
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -1083,8 +1115,8 @@ impl SessionTelemetry {
|
|||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
|
|
||||||
let (output, success) = match &result {
|
let (output, success) = match &result {
|
||||||
Ok((preview, success)) => (Cow::Borrowed(preview.as_str()), *success),
|
Ok(result) => log_output(result),
|
||||||
Err(error) => (Cow::Owned(error.to_string()), false),
|
Err(error) => (error.to_string(), false),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.tool_result_with_tags(
|
self.tool_result_with_tags(
|
||||||
@@ -1093,7 +1125,7 @@ impl SessionTelemetry {
|
|||||||
arguments,
|
arguments,
|
||||||
duration,
|
duration,
|
||||||
success,
|
success,
|
||||||
output.as_ref(),
|
&output,
|
||||||
extra_tags,
|
extra_tags,
|
||||||
extra_trace_fields,
|
extra_trace_fields,
|
||||||
);
|
);
|
||||||
@@ -1101,34 +1133,10 @@ impl SessionTelemetry {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_tool_failed(&self, tool_name: &str, error: &str) {
|
|
||||||
log_event!(
|
|
||||||
self,
|
|
||||||
event.name = "codex.tool_result",
|
|
||||||
tool_name = %tool_name,
|
|
||||||
duration_ms = %Duration::ZERO.as_millis(),
|
|
||||||
success = %false,
|
|
||||||
output = %error,
|
|
||||||
mcp_server = "",
|
|
||||||
mcp_server_origin = "",
|
|
||||||
);
|
|
||||||
trace_event!(
|
|
||||||
self,
|
|
||||||
event.name = "codex.tool_result",
|
|
||||||
tool_name = %tool_name,
|
|
||||||
duration_ms = %Duration::ZERO.as_millis(),
|
|
||||||
success = %false,
|
|
||||||
output_length = error.len() as i64,
|
|
||||||
output_line_count = error.lines().count() as i64,
|
|
||||||
tool_origin = %"builtin",
|
|
||||||
error.message = %error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn tool_result_with_tags(
|
pub fn tool_result_with_tags(
|
||||||
&self,
|
&self,
|
||||||
tool_name: &str,
|
tool_name: &ToolName,
|
||||||
call_id: &str,
|
call_id: &str,
|
||||||
arguments: &str,
|
arguments: &str,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
@@ -1137,9 +1145,10 @@ impl SessionTelemetry {
|
|||||||
extra_tags: &[(&str, &str)],
|
extra_tags: &[(&str, &str)],
|
||||||
extra_trace_fields: &[(&str, &str)],
|
extra_trace_fields: &[(&str, &str)],
|
||||||
) {
|
) {
|
||||||
|
let flat_tool_name = tool_name.to_string();
|
||||||
let success_str = if success { "true" } else { "false" };
|
let success_str = if success { "true" } else { "false" };
|
||||||
let mut tags = Vec::with_capacity(2 + extra_tags.len());
|
let mut tags = Vec::with_capacity(2 + extra_tags.len());
|
||||||
tags.push(("tool", tool_name));
|
tags.push(("tool", flat_tool_name.as_str()));
|
||||||
tags.push(("success", success_str));
|
tags.push(("success", success_str));
|
||||||
tags.extend_from_slice(extra_tags);
|
tags.extend_from_slice(extra_tags);
|
||||||
self.counter(TOOL_CALL_COUNT_METRIC, /*inc*/ 1, &tags);
|
self.counter(TOOL_CALL_COUNT_METRIC, /*inc*/ 1, &tags);
|
||||||
@@ -1147,30 +1156,19 @@ impl SessionTelemetry {
|
|||||||
let mcp_server = trace_field_value(extra_trace_fields, "mcp_server").unwrap_or("");
|
let mcp_server = trace_field_value(extra_trace_fields, "mcp_server").unwrap_or("");
|
||||||
let mcp_server_origin =
|
let mcp_server_origin =
|
||||||
trace_field_value(extra_trace_fields, "mcp_server_origin").unwrap_or("");
|
trace_field_value(extra_trace_fields, "mcp_server_origin").unwrap_or("");
|
||||||
log_event!(
|
emit_tool_result(
|
||||||
self,
|
self,
|
||||||
event.name = "codex.tool_result",
|
self.tool_result_log_config,
|
||||||
tool_name = %tool_name,
|
ToolResultEvent {
|
||||||
call_id = %call_id,
|
tool_name,
|
||||||
arguments = %arguments,
|
call_id,
|
||||||
duration_ms = %duration.as_millis(),
|
arguments,
|
||||||
success = %success_str,
|
mcp_server,
|
||||||
output = %output,
|
mcp_server_origin,
|
||||||
mcp_server = %mcp_server,
|
duration,
|
||||||
mcp_server_origin = %mcp_server_origin,
|
success,
|
||||||
);
|
output,
|
||||||
trace_event!(
|
},
|
||||||
self,
|
|
||||||
event.name = "codex.tool_result",
|
|
||||||
tool_name = %tool_name,
|
|
||||||
call_id = %call_id,
|
|
||||||
duration_ms = %duration.as_millis(),
|
|
||||||
success = %success_str,
|
|
||||||
arguments_length = arguments.len() as i64,
|
|
||||||
output_length = output.len() as i64,
|
|
||||||
output_line_count = output.lines().count() as i64,
|
|
||||||
tool_origin = if mcp_server.is_empty() { "builtin" } else { "mcp" },
|
|
||||||
mcp_tool = !mcp_server.is_empty(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
use chrono::SecondsFormat;
|
use chrono::SecondsFormat;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||||
|
use codex_protocol::ToolName;
|
||||||
|
|
||||||
|
pub(crate) fn tool_namespace(tool_name: &ToolName) -> &str {
|
||||||
|
tool_name
|
||||||
|
.namespace
|
||||||
|
.as_deref()
|
||||||
|
.filter(|namespace| !namespace.is_empty())
|
||||||
|
.unwrap_or(DEFAULT_FUNCTION_NAMESPACE)
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! log_event {
|
macro_rules! log_event {
|
||||||
($self:expr, $($fields:tt)*) => {{
|
($self:expr, $($fields:tt)*) => {{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub(crate) mod trace_context;
|
|||||||
|
|
||||||
mod otlp;
|
mod otlp;
|
||||||
mod targets;
|
mod targets;
|
||||||
|
mod tool_result;
|
||||||
|
|
||||||
use crate::metrics::Result as MetricsResult;
|
use crate::metrics::Result as MetricsResult;
|
||||||
use codex_protocol::auth::AuthMode;
|
use codex_protocol::auth::AuthMode;
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
use crate::SessionTelemetry;
|
||||||
|
use crate::events::shared::log_and_trace_event;
|
||||||
|
use crate::events::shared::log_event;
|
||||||
|
use crate::events::shared::tool_namespace;
|
||||||
|
use crate::events::shared::trace_event;
|
||||||
|
use codex_protocol::ToolName;
|
||||||
|
use codex_protocol::config_types::ToolResultLogConfig;
|
||||||
|
use codex_utils_string::take_bytes_at_char_boundary;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const TRUNCATION_NOTICE: &str = "[... telemetry preview truncated ...]";
|
||||||
|
|
||||||
|
/// Result-recording order across all sessions in this process, not tool-start order.
|
||||||
|
/// Independent of the optional rollout-trace writer and reset on process restart.
|
||||||
|
fn next_tool_result_seq() -> u64 {
|
||||||
|
static NEXT_SEQ: AtomicU64 = AtomicU64::new(1);
|
||||||
|
NEXT_SEQ.fetch_add(/*val*/ 1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ToolResultEvent<'a> {
|
||||||
|
pub tool_name: &'a ToolName,
|
||||||
|
pub call_id: &'a str,
|
||||||
|
pub arguments: &'a str,
|
||||||
|
pub mcp_server: &'a str,
|
||||||
|
pub mcp_server_origin: &'a str,
|
||||||
|
pub duration: Duration,
|
||||||
|
pub success: bool,
|
||||||
|
pub output: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits diagnostic logs and trace-safe metadata without changing tool-call metrics.
|
||||||
|
pub(crate) fn emit_tool_result(
|
||||||
|
telemetry: &SessionTelemetry,
|
||||||
|
limits: ToolResultLogConfig,
|
||||||
|
event: ToolResultEvent<'_>,
|
||||||
|
) {
|
||||||
|
let ToolResultEvent {
|
||||||
|
tool_name,
|
||||||
|
call_id,
|
||||||
|
arguments,
|
||||||
|
mcp_server,
|
||||||
|
mcp_server_origin,
|
||||||
|
duration,
|
||||||
|
success,
|
||||||
|
output,
|
||||||
|
} = event;
|
||||||
|
let tool_namespace = tool_namespace(tool_name);
|
||||||
|
let preview = telemetry_preview(output, limits);
|
||||||
|
let tool_result_seq = next_tool_result_seq();
|
||||||
|
|
||||||
|
log_and_trace_event!(
|
||||||
|
telemetry,
|
||||||
|
common: {
|
||||||
|
event.name = "codex.tool_result",
|
||||||
|
tool_result_seq = tool_result_seq,
|
||||||
|
tool_name = %tool_name.name,
|
||||||
|
tool_namespace = %tool_namespace,
|
||||||
|
call_id = %call_id,
|
||||||
|
duration_ms = %duration.as_millis(),
|
||||||
|
success = %success,
|
||||||
|
output_truncated = preview.truncated,
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
agent_name = %telemetry.metadata.agent_name,
|
||||||
|
arguments = %arguments,
|
||||||
|
output = %preview.text,
|
||||||
|
mcp_server = %mcp_server,
|
||||||
|
mcp_server_origin = %mcp_server_origin,
|
||||||
|
},
|
||||||
|
trace: {
|
||||||
|
arguments_length = arguments.len() as i64,
|
||||||
|
output_length = output.len() as i64,
|
||||||
|
output_line_count = output.lines().count() as i64,
|
||||||
|
tool_origin = if mcp_server.is_empty() { "builtin" } else { "mcp" },
|
||||||
|
mcp_tool = !mcp_server.is_empty(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct ToolResultPreview<'a> {
|
||||||
|
pub text: Cow<'a, str>,
|
||||||
|
/// Whether this telemetry limit removed text, not whether the tool result was lossless.
|
||||||
|
pub truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn telemetry_preview(
|
||||||
|
content: &str,
|
||||||
|
limits: ToolResultLogConfig,
|
||||||
|
) -> ToolResultPreview<'_> {
|
||||||
|
let prefix = take_bytes_at_char_boundary(content, limits.max_bytes);
|
||||||
|
if prefix.len() == content.len() {
|
||||||
|
return ToolResultPreview {
|
||||||
|
text: Cow::Borrowed(content),
|
||||||
|
truncated: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let separator = if prefix.is_empty() || prefix.ends_with('\n') {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"\n"
|
||||||
|
};
|
||||||
|
ToolResultPreview {
|
||||||
|
text: Cow::Owned(format!("{prefix}{separator}{TRUNCATION_NOTICE}")),
|
||||||
|
truncated: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tool_result_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telemetry_preview_returns_original_within_limits() {
|
||||||
|
// A literal marker in the tool's own output must not imply telemetry truncation.
|
||||||
|
for content in ["", "first\r\nsecond\r\n", TRUNCATION_NOTICE] {
|
||||||
|
let preview = telemetry_preview(content, ToolResultLogConfig::default());
|
||||||
|
assert!(matches!(preview.text, Cow::Borrowed(_)));
|
||||||
|
assert_eq!(
|
||||||
|
preview,
|
||||||
|
ToolResultPreview {
|
||||||
|
text: Cow::Borrowed(content),
|
||||||
|
truncated: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telemetry_preview_truncates_by_bytes() {
|
||||||
|
let limits = ToolResultLogConfig::default();
|
||||||
|
let content = "x".repeat(limits.max_bytes + 8);
|
||||||
|
let preview = telemetry_preview(&content, limits);
|
||||||
|
assert!(preview.truncated);
|
||||||
|
assert!(preview.text.contains(TRUNCATION_NOTICE));
|
||||||
|
assert!(preview.text.len() <= limits.max_bytes + TRUNCATION_NOTICE.len() + 1);
|
||||||
|
assert_eq!(
|
||||||
|
telemetry_preview(
|
||||||
|
&content,
|
||||||
|
ToolResultLogConfig {
|
||||||
|
max_bytes: content.len(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ToolResultPreview {
|
||||||
|
text: Cow::Borrowed(&content),
|
||||||
|
truncated: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
telemetry_preview("éé", ToolResultLogConfig { max_bytes: 3 }),
|
||||||
|
ToolResultPreview {
|
||||||
|
text: Cow::Owned(format!("é\n{TRUNCATION_NOTICE}")),
|
||||||
|
truncated: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
for (content, max_bytes, expected, truncated) in [
|
||||||
|
("", 0, "".to_string(), false),
|
||||||
|
("\nsecret", 0, TRUNCATION_NOTICE.to_string(), true),
|
||||||
|
(
|
||||||
|
"first\r\nsecond",
|
||||||
|
7,
|
||||||
|
format!("first\r\n{TRUNCATION_NOTICE}"),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
("first\r\n", 7, "first\r\n".to_string(), false),
|
||||||
|
("\n\nsecret", 1, format!("\n{TRUNCATION_NOTICE}"), true),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
telemetry_preview(content, ToolResultLogConfig { max_bytes }),
|
||||||
|
ToolResultPreview {
|
||||||
|
text: Cow::Owned(expected),
|
||||||
|
truncated
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,15 @@ use tracing_subscriber::Layer;
|
|||||||
use tracing_subscriber::filter::filter_fn;
|
use tracing_subscriber::filter::filter_fn;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
||||||
|
use codex_protocol::AgentPath;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::ToolName;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
|
use codex_protocol::config_types::ToolResultLogConfig;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::SandboxPolicy;
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
use codex_protocol::user_input::UserInput;
|
use codex_protocol::user_input::UserInput;
|
||||||
|
|
||||||
fn log_attributes(record: &SdkLogRecord) -> BTreeMap<String, String> {
|
fn log_attributes(record: &SdkLogRecord) -> BTreeMap<String, String> {
|
||||||
@@ -205,6 +209,25 @@ fn otel_export_routing_policy_routes_user_prompt_log_and_trace_events() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
|
fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
|
||||||
|
let output = "secret output\nsecond line\n".repeat(100);
|
||||||
|
let root_id = ThreadId::new();
|
||||||
|
let child_id = ThreadId::new();
|
||||||
|
let legacy_id = ThreadId::new();
|
||||||
|
let unnamed_id = ThreadId::new();
|
||||||
|
let make_manager = |thread_id, source| {
|
||||||
|
SessionTelemetry::new(
|
||||||
|
thread_id,
|
||||||
|
"gpt-5.1",
|
||||||
|
"gpt-5.1",
|
||||||
|
Some("account-id".to_string()),
|
||||||
|
Some("engineer@example.com".to_string()),
|
||||||
|
Some(TelemetryAuthMode::ApiKey),
|
||||||
|
"codex_exec".to_string(),
|
||||||
|
/*log_user_prompts*/ true,
|
||||||
|
"tty".to_string(),
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
};
|
||||||
let log_exporter = InMemoryLogExporter::default();
|
let log_exporter = InMemoryLogExporter::default();
|
||||||
let logger_provider = SdkLoggerProvider::builder()
|
let logger_provider = SdkLoggerProvider::builder()
|
||||||
.with_simple_exporter(log_exporter.clone())
|
.with_simple_exporter(log_exporter.clone())
|
||||||
@@ -230,33 +253,72 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
|
|||||||
|
|
||||||
tracing::subscriber::with_default(subscriber, || {
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
tracing::callsite::rebuild_interest_cache();
|
tracing::callsite::rebuild_interest_cache();
|
||||||
let manager = SessionTelemetry::new(
|
let manager = make_manager(root_id, SessionSource::Cli);
|
||||||
ThreadId::new(),
|
|
||||||
"gpt-5.1",
|
|
||||||
"gpt-5.1",
|
|
||||||
Some("account-id".to_string()),
|
|
||||||
Some("engineer@example.com".to_string()),
|
|
||||||
Some(TelemetryAuthMode::ApiKey),
|
|
||||||
"codex_exec".to_string(),
|
|
||||||
/*log_user_prompts*/ true,
|
|
||||||
"tty".to_string(),
|
|
||||||
SessionSource::Cli,
|
|
||||||
);
|
|
||||||
let root_span = tracing::info_span!("root");
|
let root_span = tracing::info_span!("root");
|
||||||
let _root_guard = root_span.enter();
|
let _root_guard = root_span.enter();
|
||||||
|
let manager = manager.with_tool_result_log_config(ToolResultLogConfig {
|
||||||
|
max_bytes: output.len(),
|
||||||
|
});
|
||||||
manager.tool_result_with_tags(
|
manager.tool_result_with_tags(
|
||||||
"shell",
|
&ToolName::namespaced("mcp__example", "shell"),
|
||||||
"call-1",
|
"call-1",
|
||||||
"secret arguments",
|
"secret arguments",
|
||||||
std::time::Duration::from_millis(42),
|
std::time::Duration::from_millis(42),
|
||||||
/*success*/ true,
|
/*success*/ true,
|
||||||
"secret output\nsecond line",
|
&output,
|
||||||
&[],
|
&[],
|
||||||
&[
|
&[
|
||||||
("mcp_server", "internal-mcp"),
|
("mcp_server", "internal-mcp"),
|
||||||
("mcp_server_origin", "stdio"),
|
("mcp_server_origin", "stdio"),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
let child = make_manager(
|
||||||
|
child_id,
|
||||||
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||||
|
parent_thread_id: root_id,
|
||||||
|
depth: 1,
|
||||||
|
agent_path: Some(AgentPath::try_from("/root/reviewer").expect("agent path")),
|
||||||
|
agent_nickname: Some("legacy nickname".to_string()),
|
||||||
|
agent_role: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
child.tool_result_with_tags(
|
||||||
|
&ToolName::plain("shell"),
|
||||||
|
"call-2",
|
||||||
|
"{}",
|
||||||
|
std::time::Duration::ZERO,
|
||||||
|
/*success*/ true,
|
||||||
|
&output,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
let legacy = make_manager(
|
||||||
|
legacy_id,
|
||||||
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||||
|
parent_thread_id: root_id,
|
||||||
|
depth: 1,
|
||||||
|
agent_path: None,
|
||||||
|
agent_nickname: Some("Mendel".to_string()),
|
||||||
|
agent_role: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let unnamed = make_manager(unnamed_id, SessionSource::SubAgent(SubAgentSource::Review));
|
||||||
|
for (manager, name, error) in [
|
||||||
|
(&manager, "root_failure", "failure"),
|
||||||
|
(&legacy, "legacy_failure", output.as_str()),
|
||||||
|
(&unnamed, "unnamed_failure", "failure"),
|
||||||
|
] {
|
||||||
|
manager.tool_result_with_tags(
|
||||||
|
&ToolName::plain(name),
|
||||||
|
name,
|
||||||
|
"{}",
|
||||||
|
std::time::Duration::ZERO,
|
||||||
|
/*success*/ false,
|
||||||
|
error,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger_provider.force_flush().expect("flush logs");
|
logger_provider.force_flush().expect("flush logs");
|
||||||
@@ -270,14 +332,71 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
|
|||||||
|
|
||||||
let tool_log = find_log_by_event_name(&logs, "codex.tool_result");
|
let tool_log = find_log_by_event_name(&logs, "codex.tool_result");
|
||||||
let tool_log_attrs = log_attributes(&tool_log.record);
|
let tool_log_attrs = log_attributes(&tool_log.record);
|
||||||
|
assert_eq!(
|
||||||
|
tool_log_attrs.get("tool_name").map(String::as_str),
|
||||||
|
Some("shell")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tool_log_attrs.get("tool_namespace").map(String::as_str),
|
||||||
|
Some("mcp__example")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tool_log_attrs.get("arguments").map(String::as_str),
|
tool_log_attrs.get("arguments").map(String::as_str),
|
||||||
Some("secret arguments")
|
Some("secret arguments")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tool_log_attrs.get("output").map(String::as_str),
|
tool_log_attrs.get("output").map(String::as_str),
|
||||||
Some("secret output\nsecond line")
|
Some(output.as_str())
|
||||||
);
|
);
|
||||||
|
let tool_logs: Vec<_> = logs
|
||||||
|
.iter()
|
||||||
|
.map(|log| log_attributes(&log.record))
|
||||||
|
.filter(|attrs| attrs.get("event.name").map(String::as_str) == Some("codex.tool_result"))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
tool_logs
|
||||||
|
.iter()
|
||||||
|
.map(|attrs| (
|
||||||
|
attrs["conversation.id"].clone(),
|
||||||
|
attrs["agent_name"].clone(),
|
||||||
|
attrs["output_truncated"].clone(),
|
||||||
|
))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
root_id.to_string(),
|
||||||
|
"/root".to_string(),
|
||||||
|
"false".to_string()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
child_id.to_string(),
|
||||||
|
"/root/reviewer".to_string(),
|
||||||
|
"true".to_string()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
root_id.to_string(),
|
||||||
|
"/root".to_string(),
|
||||||
|
"false".to_string()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
legacy_id.to_string(),
|
||||||
|
"Mendel".to_string(),
|
||||||
|
"true".to_string()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unnamed_id.to_string(),
|
||||||
|
unnamed_id.to_string(),
|
||||||
|
"false".to_string()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(tool_logs[1]["output"].ends_with("[... telemetry preview truncated ...]"));
|
||||||
|
let sequences: Vec<u64> = tool_logs
|
||||||
|
.iter()
|
||||||
|
.map(|attrs| attrs["tool_result_seq"].parse().expect("numeric sequence"))
|
||||||
|
.collect();
|
||||||
|
assert!(sequences[0] > 0);
|
||||||
|
assert!(sequences.windows(2).all(|pair| pair[0] < pair[1]));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tool_log_attrs.get("mcp_server").map(String::as_str),
|
tool_log_attrs.get("mcp_server").map(String::as_str),
|
||||||
Some("internal-mcp")
|
Some("internal-mcp")
|
||||||
@@ -290,7 +409,15 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
|
|||||||
let spans = span_exporter.get_finished_spans().expect("span export");
|
let spans = span_exporter.get_finished_spans().expect("span export");
|
||||||
assert_eq!(spans.len(), 1);
|
assert_eq!(spans.len(), 1);
|
||||||
let span_events = &spans[0].events.events;
|
let span_events = &spans[0].events.events;
|
||||||
assert_eq!(span_events.len(), 1);
|
assert_eq!(span_events.len(), tool_logs.len());
|
||||||
|
for (event, log) in span_events.iter().zip(&tool_logs) {
|
||||||
|
let attrs = span_event_attributes(event);
|
||||||
|
assert_eq!(attrs.get("tool_name"), log.get("tool_name"));
|
||||||
|
assert_eq!(attrs.get("tool_namespace"), log.get("tool_namespace"));
|
||||||
|
assert_eq!(attrs.get("tool_result_seq"), log.get("tool_result_seq"));
|
||||||
|
assert_eq!(attrs.get("output_truncated"), log.get("output_truncated"));
|
||||||
|
assert!(!attrs.contains_key("agent_name"));
|
||||||
|
}
|
||||||
|
|
||||||
let tool_trace_event = find_span_event_by_name_attr(span_events, "codex.tool_result");
|
let tool_trace_event = find_span_event_by_name_attr(span_events, "codex.tool_result");
|
||||||
let tool_trace_attrs = span_event_attributes(tool_trace_event);
|
let tool_trace_attrs = span_event_attributes(tool_trace_event);
|
||||||
@@ -300,13 +427,13 @@ fn otel_export_routing_policy_routes_tool_result_log_and_trace_events() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tool_trace_attrs.get("output_length").map(String::as_str),
|
tool_trace_attrs.get("output_length").map(String::as_str),
|
||||||
Some("25")
|
Some(output.len().to_string().as_str())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tool_trace_attrs
|
tool_trace_attrs
|
||||||
.get("output_line_count")
|
.get("output_line_count")
|
||||||
.map(String::as_str),
|
.map(String::as_str),
|
||||||
Some("2")
|
Some(output.lines().count().to_string().as_str())
|
||||||
);
|
);
|
||||||
assert!(!tool_trace_attrs.contains_key("arguments"));
|
assert!(!tool_trace_attrs.contains_key("arguments"));
|
||||||
assert!(!tool_trace_attrs.contains_key("output"));
|
assert!(!tool_trace_attrs.contains_key("output"));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use codex_otel::RuntimeMetricsSummary;
|
|||||||
use codex_otel::SessionTelemetry;
|
use codex_otel::SessionTelemetry;
|
||||||
use codex_otel::TelemetryAuthMode;
|
use codex_otel::TelemetryAuthMode;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::ToolName;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
use eventsource_stream::Event as StreamEvent;
|
use eventsource_stream::Event as StreamEvent;
|
||||||
use opentelemetry_sdk::metrics::InMemoryMetricExporter;
|
use opentelemetry_sdk::metrics::InMemoryMetricExporter;
|
||||||
@@ -37,7 +38,7 @@ fn runtime_metrics_summary_collects_tool_api_and_streaming_metrics() -> Result<(
|
|||||||
manager.reset_runtime_metrics();
|
manager.reset_runtime_metrics();
|
||||||
|
|
||||||
manager.tool_result_with_tags(
|
manager.tool_result_with_tags(
|
||||||
"shell",
|
&ToolName::plain("shell"),
|
||||||
"call-1",
|
"call-1",
|
||||||
"{\"cmd\":\"echo\"}",
|
"{\"cmd\":\"echo\"}",
|
||||||
Duration::from_millis(250),
|
Duration::from_millis(250),
|
||||||
|
|||||||
@@ -21,6 +21,23 @@ use wildmatch::WildMatchPattern;
|
|||||||
|
|
||||||
use crate::openai_models::ReasoningEffort;
|
use crate::openai_models::ReasoningEffort;
|
||||||
|
|
||||||
|
/// Limit for the text included in `codex.tool_result` log records.
|
||||||
|
/// This does not affect model-visible output. Raising it can expose more tool data to logs.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ToolResultLogConfig {
|
||||||
|
/// Maximum UTF-8 bytes before the truncation notice. Defaults to 2048.
|
||||||
|
pub max_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ToolResultLogConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_bytes: 2 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Selects which part of the active context is charged against
|
/// Selects which part of the active context is charged against
|
||||||
/// `model_auto_compact_token_limit`.
|
/// `model_auto_compact_token_limit`.
|
||||||
#[derive(
|
#[derive(
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ use codex_protocol::models::FunctionCallOutputBody;
|
|||||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||||
use codex_protocol::models::FunctionCallOutputPayload;
|
use codex_protocol::models::FunctionCallOutputPayload;
|
||||||
use codex_protocol::models::ResponseInputItem;
|
use codex_protocol::models::ResponseInputItem;
|
||||||
use codex_utils_string::take_bytes_at_char_boundary;
|
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::ToolPayload;
|
use crate::ToolPayload;
|
||||||
|
|
||||||
const TELEMETRY_PREVIEW_MAX_BYTES: usize = 2 * 1024;
|
|
||||||
const TELEMETRY_PREVIEW_MAX_LINES: usize = 64;
|
|
||||||
const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str = "[... telemetry preview truncated ...]";
|
|
||||||
|
|
||||||
/// Model-facing output contract returned by executable tool runtimes.
|
/// Model-facing output contract returned by executable tool runtimes.
|
||||||
pub trait ToolOutput: Send {
|
pub trait ToolOutput: Send {
|
||||||
fn log_preview(&self) -> String;
|
/// Returns a deliberately lossy diagnostic representation, before telemetry size limits.
|
||||||
|
/// Implementations may summarize results or omit media and encrypted content. This is not
|
||||||
|
/// the authoritative tool result; an untruncated log does not imply a complete result.
|
||||||
|
/// The logger owns the additional configurable byte limit.
|
||||||
|
fn log_output(&self) -> String;
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool;
|
fn success_for_logging(&self) -> bool;
|
||||||
|
|
||||||
@@ -56,8 +55,8 @@ impl<T> ToolOutput for Box<T>
|
|||||||
where
|
where
|
||||||
T: ToolOutput + ?Sized,
|
T: ToolOutput + ?Sized,
|
||||||
{
|
{
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
(**self).log_preview()
|
(**self).log_output()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -120,8 +119,8 @@ impl JsonToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for JsonToolOutput {
|
impl ToolOutput for JsonToolOutput {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
telemetry_preview(&self.value.to_string())
|
self.value.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -162,10 +161,10 @@ impl ToolOutput for JsonToolOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolOutput for codex_protocol::mcp::CallToolResult {
|
impl ToolOutput for codex_protocol::mcp::CallToolResult {
|
||||||
fn log_preview(&self) -> String {
|
fn log_output(&self) -> String {
|
||||||
let output = self.as_function_call_output_payload();
|
let output = self.as_function_call_output_payload();
|
||||||
let preview = output.body.to_text().unwrap_or_else(|| output.to_string());
|
// Do not fall back to serializing media or encrypted content into logs.
|
||||||
telemetry_preview(&preview)
|
output.body.to_text().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success_for_logging(&self) -> bool {
|
fn success_for_logging(&self) -> bool {
|
||||||
@@ -255,43 +254,3 @@ fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) ->
|
|||||||
.join("\n"),
|
.join("\n"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn telemetry_preview(content: &str) -> String {
|
|
||||||
let truncated_slice = take_bytes_at_char_boundary(content, TELEMETRY_PREVIEW_MAX_BYTES);
|
|
||||||
let truncated_by_bytes = truncated_slice.len() < content.len();
|
|
||||||
|
|
||||||
let mut preview = String::new();
|
|
||||||
let mut lines_iter = truncated_slice.lines();
|
|
||||||
for idx in 0..TELEMETRY_PREVIEW_MAX_LINES {
|
|
||||||
match lines_iter.next() {
|
|
||||||
Some(line) => {
|
|
||||||
if idx > 0 {
|
|
||||||
preview.push('\n');
|
|
||||||
}
|
|
||||||
preview.push_str(line);
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let truncated_by_lines = lines_iter.next().is_some();
|
|
||||||
|
|
||||||
if !truncated_by_bytes && !truncated_by_lines {
|
|
||||||
return content.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if preview.len() < truncated_slice.len()
|
|
||||||
&& truncated_slice
|
|
||||||
.as_bytes()
|
|
||||||
.get(preview.len())
|
|
||||||
.is_some_and(|byte| *byte == b'\n')
|
|
||||||
{
|
|
||||||
preview.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if !preview.is_empty() && !preview.ends_with('\n') {
|
|
||||||
preview.push('\n');
|
|
||||||
}
|
|
||||||
preview.push_str(TELEMETRY_PREVIEW_TRUNCATION_NOTICE);
|
|
||||||
|
|
||||||
preview
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user