merge: provider trace spawn preservation

This commit is contained in:
Keisuke Hirata 2026-05-30 09:37:46 +09:00
commit f927b37b83
No known key found for this signature in database
6 changed files with 92 additions and 11 deletions

View File

@ -9,7 +9,8 @@ mod scope;
pub use cascade::{LayerLoadError, find_project_manifest_from, load_layer};
pub use config::{
CompactionConfigPartial, FileUploadLimitsPartial, PermissionConfigPartial, PodManifestConfig,
PodMetaConfig, ResolveError, ToolOutputLimitsPartial, WorkerManifestConfig,
PodMetaConfig, ResolveError, SessionConfigPartial, ToolOutputLimitsPartial,
WorkerManifestConfig,
};
pub use model::{
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
@ -395,9 +396,10 @@ pub struct ScopeConfig {
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SessionConfig {
/// Persist every provider stream event directly to `trace.jsonl` next to the
/// segment log. Intended for debugging stalls between stream requests; off
/// by default because it can be verbose.
/// Persist normalized provider stream events and lifecycle diagnostics to a
/// `.trace.jsonl` sidecar next to the segment log. This is not guaranteed to
/// be a byte-for-byte raw SSE capture. Intended for debugging stalls between
/// stream requests; off by default because it can be verbose.
#[serde(default)]
pub record_event_trace: bool,
}

View File

@ -898,6 +898,27 @@ mod tests {
);
}
#[test]
fn profile_artifact_preserves_session_record_event_trace() {
let mut raw = artifact();
raw["manifest"]["session"] = serde_json::json!({ "record_event_trace": true });
let resolved = resolve_profile_artifact(
ProfileSource::Path {
path: PathBuf::from("/profiles/coder.nix"),
},
Path::new("/workspace/project"),
raw,
)
.unwrap();
assert!(resolved.manifest.session.record_event_trace);
assert_eq!(
resolved.manifest_snapshot["session"]["record_event_trace"],
serde_json::json!(true)
);
}
#[test]
fn rejects_both_manifest_and_config_fields() {
let err = resolve_profile_artifact(

View File

@ -502,6 +502,7 @@ where
let web_config = pod.manifest().web.clone();
let spawner_name = pod.manifest().pod.name.clone();
let spawner_model = pod.manifest().model.clone();
let spawner_record_event_trace = pod.manifest().session.record_event_trace;
let pod_store = pod.store().clone();
let self_parent_socket = pod.callback_socket().cloned();
@ -556,6 +557,7 @@ where
spawned_registry.clone(),
self_parent_socket,
spawner_model,
spawner_record_event_trace,
scope_handle,
));
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));

View File

@ -15,7 +15,7 @@ use async_trait::async_trait;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{
ModelManifest, Permission, PodManifestConfig, PodMetaConfig, ScopeConfig, ScopeRule,
SharedScope, WorkerManifestConfig,
SessionConfigPartial, SharedScope, WorkerManifestConfig,
};
use serde::Deserialize;
use tokio::net::UnixStream;
@ -119,6 +119,9 @@ pub struct SpawnPodTool {
/// configuration. Per-spawn override is
/// out of scope here (see `tickets/spawn-inherit-provider.md`).
spawner_model: ModelManifest,
/// Spawner's session diagnostics policy. Preserved for spawned Pods so
/// opt-in provider event traces continue across delegation.
spawner_record_event_trace: bool,
/// Spawner's runtime scope. After a successful spawn, the
/// `Permission::Write` rules in the delegated scope are revoked
/// from the spawner's in-memory view (a `deny(Write, target)` is
@ -138,6 +141,7 @@ impl SpawnPodTool {
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_model: ModelManifest,
spawner_record_event_trace: bool,
spawner_scope: SharedScope,
) -> Self {
Self {
@ -148,6 +152,7 @@ impl SpawnPodTool {
registry,
parent_socket,
spawner_model,
spawner_record_event_trace,
spawner_scope,
}
}
@ -207,6 +212,7 @@ impl Tool for SpawnPodTool {
&instruction,
&scope_allow,
&self.spawner_model,
self.spawner_record_event_trace,
) {
Ok(s) => s,
Err(e) => {
@ -384,6 +390,7 @@ fn build_spawn_config_json(
instruction: &str,
scope_allow: &[ScopeRule],
model: &ModelManifest,
record_event_trace: bool,
) -> Result<String, serde_json::Error> {
let config = PodManifestConfig {
pod: PodMetaConfig {
@ -399,6 +406,9 @@ fn build_spawn_config_json(
allow: scope_allow.to_vec(),
deny: Vec::new(),
},
session: record_event_trace.then_some(SessionConfigPartial {
record_event_trace: Some(true),
}),
..Default::default()
};
serde_json::to_string(&config)
@ -484,6 +494,7 @@ pub fn spawn_pod_tool(
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_model: ModelManifest,
spawner_record_event_trace: bool,
spawner_scope: SharedScope,
) -> ToolDefinition {
Arc::new(move || {
@ -500,6 +511,7 @@ pub fn spawn_pod_tool(
registry.clone(),
parent_socket.clone(),
spawner_model.clone(),
spawner_record_event_trace,
spawner_scope.clone(),
));
(meta, tool)
@ -509,7 +521,7 @@ pub fn spawn_pod_tool(
#[cfg(test)]
mod tests {
use super::*;
use manifest::{AuthRef, SchemeKind};
use manifest::{AuthRef, PodManifest, SchemeKind};
#[test]
fn spawn_config_inherits_inline_spawner_model() {
@ -525,7 +537,7 @@ mod tests {
};
let config_json =
build_spawn_config_json("child", "$insomnia/default", &[], &model).unwrap();
build_spawn_config_json("child", "$insomnia/default", &[], &model, false).unwrap();
let parsed: PodManifestConfig = serde_json::from_str(&config_json).unwrap();
assert_eq!(parsed.model.scheme, Some(SchemeKind::Anthropic));
@ -548,11 +560,51 @@ mod tests {
..Default::default()
};
let config_json =
build_spawn_config_json("child", "$insomnia/default", &[], &model).unwrap();
build_spawn_config_json("child", "$insomnia/default", &[], &model, false).unwrap();
let parsed: PodManifestConfig = serde_json::from_str(&config_json).unwrap();
assert_eq!(
parsed.model.ref_.as_deref(),
Some("anthropic/claude-sonnet-4-6")
);
}
#[test]
fn spawn_config_preserves_record_event_trace_when_enabled() {
let model = ModelManifest {
ref_: Some("anthropic/claude-sonnet-4-6".into()),
..Default::default()
};
let scope = vec![ScopeRule {
target: PathBuf::from("/tmp/child"),
permission: Permission::Read,
recursive: true,
}];
let config_json =
build_spawn_config_json("child", "$insomnia/default", &scope, &model, true).unwrap();
let parsed: PodManifestConfig = serde_json::from_str(&config_json).unwrap();
assert_eq!(
parsed.session.as_ref().and_then(|s| s.record_event_trace),
Some(true)
);
let manifest: PodManifest = PodManifestConfig::builtin_defaults()
.merge(parsed)
.try_into()
.unwrap();
assert!(manifest.session.record_event_trace);
}
#[test]
fn spawn_config_omits_record_event_trace_when_disabled() {
let model = ModelManifest {
ref_: Some("anthropic/claude-sonnet-4-6".into()),
..Default::default()
};
let config_json =
build_spawn_config_json("child", "$insomnia/default", &[], &model, false).unwrap();
let parsed: PodManifestConfig = serde_json::from_str(&config_json).unwrap();
assert!(parsed.session.is_none());
}
}

View File

@ -192,6 +192,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
registry,
None,
dummy_model(),
false,
spawner_scope.clone(),
);
let (_meta, tool) = def();
@ -280,6 +281,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
registry,
None,
dummy_model(),
false,
spawner_scope.clone(),
);
let (_meta, tool) = def();
@ -351,6 +353,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
registry,
None,
dummy_model(),
false,
spawner_scope.clone(),
);
let (_meta, tool) = def();

View File

@ -1,7 +1,8 @@
//! Debug-only LLM request/stream trace recording.
//!
//! [`TraceEntry`] captures stream lifecycle markers and raw provider stream
//! events for debugging stalls. Written to a separate `.trace.jsonl` file,
//! [`TraceEntry`] captures stream lifecycle markers and normalized provider
//! stream events for debugging stalls. It is not a byte-for-byte raw SSE
//! capture. Written to a separate `.trace.jsonl` file,
//! completely independent of the segment log used for state restoration.
//!
//! Disabled by default. Enable via `SessionConfig::record_event_trace`.
@ -10,7 +11,7 @@ use llm_worker::llm_client::event::Event;
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// A single trace entry recording either a lifecycle marker or raw stream event.
/// A single trace entry recording either a lifecycle marker or normalized stream event.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TraceEntry {
/// Timestamp in milliseconds since Unix epoch.