Let the smoke show the whole membrane
A test-only TracingInferencePort wraps any inference port and appends each request's purpose, model, instructions, input turns, tool catalog, and output to the file GHOSTLIGHT_SMOKE_TRACE names. The logs redact prose by design and the store is a temp directory, so this is the one way to read what the Projector, the Persona, and the Interpreter were each given and what each returned on a road run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
4297b7c0c2
commit
f3d1904428
3 changed files with 111 additions and 2 deletions
|
|
@ -2477,6 +2477,9 @@ mod tests {
|
|||
/// instead of, or alongside, the connector.
|
||||
sdk: Option<SdkBinding>,
|
||||
models: ControllerModels,
|
||||
/// Where the whole membrane is written, request by request, when the
|
||||
/// smoke is asked to show it.
|
||||
trace: Option<PathBuf>,
|
||||
}
|
||||
|
||||
async fn fixture() -> Fixture {
|
||||
|
|
@ -2516,8 +2519,13 @@ mod tests {
|
|||
operational_agent: "gpt-5.6-terra".into(),
|
||||
elaborator: "gpt-5.6-terra".into(),
|
||||
},
|
||||
trace: None,
|
||||
});
|
||||
let inference = open_inference(live.connector, live.sdk, &live.models).unwrap();
|
||||
let inference: Arc<dyn crate::world::InferencePort> = match live.trace {
|
||||
Some(path) => Arc::new(crate::world::TracingInferencePort::new(inference, path)),
|
||||
None => inference,
|
||||
};
|
||||
let work = open_controller_work(directory.path().join("controller-work.cc")).unwrap();
|
||||
let controllers =
|
||||
ControllerRunner::open(world.clone(), inference, work, live.models).unwrap();
|
||||
|
|
@ -3634,6 +3642,7 @@ mod tests {
|
|||
operational_agent: env("GHOSTLIGHT_CONTROLLER_OPERATIONAL_MODEL"),
|
||||
elaborator: env("GHOSTLIGHT_CONTROLLER_ELABORATOR_MODEL"),
|
||||
},
|
||||
trace: std::env::var_os("GHOSTLIGHT_SMOKE_TRACE").map(PathBuf::from),
|
||||
};
|
||||
let ticks: u32 = env("GHOSTLIGHT_SMOKE_TICKS").parse().unwrap();
|
||||
let log_path = PathBuf::from(env("GHOSTLIGHT_SMOKE_LOG"));
|
||||
|
|
|
|||
|
|
@ -268,6 +268,106 @@ pub(crate) trait InferencePort: Send + Sync {
|
|||
}
|
||||
}
|
||||
|
||||
/// Test-only. Wraps any `InferencePort` and appends every prepared request and
|
||||
/// its output to one file, so a road run can show the whole membrane: what the
|
||||
/// Projector, the Persona, and the Interpreter were each given and what each
|
||||
/// returned. Production never constructs it; prose reaches durable state only
|
||||
/// as receipt-bound evidence, and the logs redact it by design.
|
||||
#[cfg(test)]
|
||||
pub(crate) struct TracingInferencePort {
|
||||
inner: Arc<dyn InferencePort>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl TracingInferencePort {
|
||||
pub(crate) fn new(inner: Arc<dyn InferencePort>, path: PathBuf) -> Self {
|
||||
Self { inner, path }
|
||||
}
|
||||
|
||||
fn render_request(prepared: &PreparedInference) -> String {
|
||||
let request = &prepared.invocation.request;
|
||||
let mut out = format!(
|
||||
"\n==== {:?} model={} request={}\n",
|
||||
prepared.purpose, request.model, request.request_id
|
||||
);
|
||||
if !request.instructions.is_empty() {
|
||||
out.push_str(&format!("-- instructions --\n{}\n", request.instructions));
|
||||
}
|
||||
out.push_str("-- input --\n");
|
||||
for item in &request.input {
|
||||
match item {
|
||||
CodexInputItem::UserText { text } => out.push_str(&format!("[user]\n{text}\n")),
|
||||
CodexInputItem::AssistantText { text } => {
|
||||
out.push_str(&format!("[assistant]\n{text}\n"));
|
||||
}
|
||||
CodexInputItem::ToolCall {
|
||||
name, arguments, ..
|
||||
} => out.push_str(&format!("[tool call {name}]\n{arguments}\n")),
|
||||
CodexInputItem::ToolResult { output, .. } => {
|
||||
out.push_str(&format!("[tool result]\n{output}\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !request.tools.is_empty() {
|
||||
out.push_str("-- tools --\n");
|
||||
for tool in &request.tools {
|
||||
out.push_str(&format!(
|
||||
"{}: {}\n{}\n",
|
||||
tool.name, tool.description, tool.parameters_json
|
||||
));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn render_output(result: &Result<InferenceOutput, InferenceFault>) -> String {
|
||||
let mut out = String::from("-- output --\n");
|
||||
match result {
|
||||
Ok(output) => {
|
||||
for event in &output.events {
|
||||
match event {
|
||||
InferenceEvent::Text(text) => out.push_str(&format!("[text]\n{text}\n")),
|
||||
InferenceEvent::ToolCall {
|
||||
name, arguments, ..
|
||||
} => out.push_str(&format!("[tool call {name}]\n{arguments}\n")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(fault) => out.push_str(&format!("[fault]\n{}\n", fault.detail)),
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[async_trait]
|
||||
impl InferencePort for TracingInferencePort {
|
||||
fn prepare(&self, request: InferenceRequest) -> Result<PreparedInference, InferenceFault> {
|
||||
self.inner.prepare(request)
|
||||
}
|
||||
|
||||
async fn infer(&self, request: PreparedInference) -> Result<InferenceOutput, InferenceFault> {
|
||||
let rendered = Self::render_request(&request);
|
||||
let result = self.inner.infer(request).await;
|
||||
let mut text = rendered;
|
||||
text.push_str(&Self::render_output(&result));
|
||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)
|
||||
{
|
||||
use std::io::Write as _;
|
||||
let _ = file.write_all(text.as_bytes());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn lend_tool_results(&self, prepared: &PreparedInference, oracle: Box<dyn ToolResultOracle>) {
|
||||
self.inner.lend_tool_results(prepared, oracle);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `PreparedInference` outside the real CodexConnector wiring. Exists
|
||||
/// so a test port defined outside this module (`runtime`'s own spec tests, for
|
||||
/// the tick driver's concurrency and quarantine behaviour) has one legal way to
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ pub(crate) use vault::VaultEvidenceSource;
|
|||
pub(crate) use controllers::{
|
||||
ControllerWork, ControllerWorkLookup, ControllerWorkStore, ControllerWorkStoreError,
|
||||
ControllerWorkWrite, InferenceEvent, InferenceFault, InferenceOutput, InferencePort,
|
||||
InferencePurpose, InferenceRequest, PreparedInference, fixture_inference_events,
|
||||
fixture_inference_output, fixture_prepared_inference,
|
||||
InferencePurpose, InferenceRequest, PreparedInference, TracingInferencePort,
|
||||
fixture_inference_events, fixture_inference_output, fixture_prepared_inference,
|
||||
};
|
||||
use patch::{EdgeRecord, EntityRecord, LedgerDelta, ResolvedOp, ResolvedPatch, Site};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue