use std::collections::HashMap;
use std::io;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use serde::{Deserialize, Serialize};
use crate::tools::spec::ToolResult;
pub const DEFAULT_LARGE_OUTPUT_THRESHOLD_TOKENS: usize = 32_768;
const CHARS_PER_TOKEN_ESTIMATE: usize = 3;
pub const WORKSHOP_LAST_TOOL_RESULT_VAR: &str = "last_tool_result";
static ACTIVE_WORKSHOP: OnceLock<Mutex<WorkshopConfig>> = OnceLock::new();
#[cfg(test)]
static ACTIVE_WORKSHOP_TEST_SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
#[cfg(test)]
std::thread_local! {
static ACTIVE_WORKSHOP_TEST_SERIAL_HELD: std::cell::Cell<bool> = const {
std::cell::Cell::new(false)
};
}
#[cfg(test)]
pub(crate) struct ActiveWorkshopTestGuard {
_serial: std::sync::MutexGuard<'static, ()>,
}
#[cfg(test)]
impl Drop for ActiveWorkshopTestGuard {
fn drop(&mut self) {
ACTIVE_WORKSHOP_TEST_SERIAL_HELD.with(|held| held.set(false));
}
}
#[cfg(test)]
pub(crate) fn active_workshop_test_guard() -> ActiveWorkshopTestGuard {
assert!(
!ACTIVE_WORKSHOP_TEST_SERIAL_HELD.with(std::cell::Cell::get),
"active workshop test guard is not reentrant"
);
let serial = ACTIVE_WORKSHOP_TEST_SERIAL
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ACTIVE_WORKSHOP_TEST_SERIAL_HELD.with(|held| held.set(true));
ActiveWorkshopTestGuard { _serial: serial }
}
fn active_workshop_slot() -> &'static Mutex<WorkshopConfig> {
ACTIVE_WORKSHOP.get_or_init(|| Mutex::new(WorkshopConfig::default()))
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct WorkshopConfig {
#[serde(default)]
pub large_output_threshold_tokens: Option<usize>,
#[serde(default)]
pub per_tool_thresholds: Option<HashMap<String, usize>>,
#[serde(default)]
pub read_result_max_bytes: Option<usize>,
#[serde(default)]
pub tool_result_max_bytes: Option<usize>,
}
impl WorkshopConfig {
pub fn install_active(config: Option<&Self>) -> Self {
#[cfg(test)]
let _test_serial = if ACTIVE_WORKSHOP_TEST_SERIAL_HELD.with(std::cell::Cell::get) {
None
} else {
Some(
ACTIVE_WORKSHOP_TEST_SERIAL
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
};
let snapshot = config.cloned().unwrap_or_default();
let mut slot = active_workshop_slot()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*slot = snapshot;
slot.clone()
}
#[must_use]
pub fn active_read_result_max_bytes() -> Option<usize> {
active_workshop_slot()
.lock()
.ok()
.and_then(|cfg| cfg.read_result_max_bytes.filter(|n| *n > 0))
}
#[must_use]
pub fn active_tool_result_max_bytes() -> Option<usize> {
active_workshop_slot()
.lock()
.ok()
.and_then(|cfg| cfg.tool_result_max_bytes.filter(|n| *n > 0))
}
#[must_use]
pub fn threshold_for(&self, tool_name: &str) -> usize {
if let Some(per_tool) = self.per_tool_thresholds.as_ref()
&& let Some(&limit) = per_tool.get(tool_name)
{
return limit;
}
self.large_output_threshold_tokens
.unwrap_or(DEFAULT_LARGE_OUTPUT_THRESHOLD_TOKENS)
}
}
#[must_use]
pub fn estimate_tokens(text: &str) -> usize {
let chars = text.chars().count();
chars.div_ceil(CHARS_PER_TOKEN_ESTIMATE)
}
#[derive(Debug, Clone, PartialEq)]
pub enum RouteDecision {
PassThrough,
Synthesise {
estimated_tokens: usize,
threshold: usize,
},
}
#[derive(Debug, Clone, Default)]
pub struct LargeOutputRouter {
config: WorkshopConfig,
}
impl LargeOutputRouter {
#[must_use]
pub fn new(config: WorkshopConfig) -> Self {
Self { config }
}
#[must_use]
pub fn route(&self, tool_name: &str, result: &ToolResult, raw_bypass: bool) -> RouteDecision {
if raw_bypass || !result.success {
return RouteDecision::PassThrough;
}
let threshold = self.config.threshold_for(tool_name);
let estimated_tokens = estimate_tokens(&result.content);
if estimated_tokens > threshold {
RouteDecision::Synthesise {
estimated_tokens,
threshold,
}
} else {
RouteDecision::PassThrough
}
}
#[must_use]
pub fn evidence_routing(
&self,
tool_name: &str,
result: &ToolResult,
_raw_bypass: bool,
) -> (EvidenceRouting, usize, usize) {
let threshold = self.config.threshold_for(tool_name);
let estimated_tokens = estimate_tokens(&result.content);
let routing = EvidenceRouting::from_token_estimate(estimated_tokens, threshold);
(routing, estimated_tokens, threshold)
}
#[must_use]
pub fn wrap_synthesis(
tool_name: &str,
synthesis: &str,
estimated_tokens: usize,
threshold: usize,
) -> String {
format!(
"[workshop-synthesis: tool={tool_name}, raw_tokens≈{estimated_tokens}, \
threshold={threshold}, raw_stored_in={WORKSHOP_LAST_TOOL_RESULT_VAR}]\n\n{synthesis}"
)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WorkshopVariables {
#[serde(default)]
pub last_tool_result: String,
#[serde(default)]
pub last_tool_name: String,
}
impl WorkshopVariables {
pub fn store_raw(&mut self, tool_name: &str, raw: &str) {
self.last_tool_result = raw.to_string();
self.last_tool_name = tool_name.to_string();
}
#[must_use]
#[allow(dead_code)] pub fn take_raw(&mut self) -> Option<(String, String)> {
if self.last_tool_result.is_empty() {
return None;
}
let content = std::mem::take(&mut self.last_tool_result);
let name = std::mem::take(&mut self.last_tool_name);
Some((name, content))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceRouting {
Inline,
Hybrid,
HandleOnly,
}
impl EvidenceRouting {
#[must_use]
pub fn from_token_estimate(estimated_tokens: usize, threshold: usize) -> Self {
if estimated_tokens <= threshold / 4 {
Self::Inline
} else if estimated_tokens <= threshold {
Self::Hybrid
} else {
Self::HandleOnly
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceArtifact {
pub handle: String,
pub digest: String,
pub size_bytes: u64,
pub content_type: String,
pub tool_name: String,
pub call_id: String,
pub origin_session: String,
pub generation: u32,
pub redacted: bool,
pub encoding: String,
pub retention_state: EvidenceRetentionState,
pub created_at_unix_ms: u64,
pub retain_until_unix_ms: u64,
pub storage_path: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceRetentionState {
Live,
Expired,
}
pub const EVIDENCE_RETENTION_SECS: u64 = 7 * 24 * 60 * 60;
#[must_use]
pub fn classic_output_routing_enabled() -> bool {
std::env::var("CODEWHALE_CLASSIC_OUTPUT_ROUTING")
.ok()
.is_some_and(|value| matches!(value.trim(), "1" | "true" | "yes" | "on"))
}
#[must_use]
pub fn evidence_metadata_relative_path(handle: &str) -> PathBuf {
PathBuf::from(crate::artifacts::ARTIFACTS_DIR_NAME).join(format!("{handle}.evidence.json"))
}
pub fn publish_evidence_metadata(
session_id: &str,
artifact: &EvidenceArtifact,
) -> io::Result<PathBuf> {
let bytes = serde_json::to_vec_pretty(artifact)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
crate::artifacts::write_session_relative_immutable(
session_id,
&evidence_metadata_relative_path(&artifact.handle),
&bytes,
)
}
pub fn read_evidence_metadata(session_id: &str, handle: &str) -> io::Result<EvidenceArtifact> {
let relative = evidence_metadata_relative_path(handle);
let path = crate::artifacts::session_artifact_absolute_path(session_id, &relative)
.ok_or_else(|| io::Error::new(io::ErrorKind::PermissionDenied, "invalid evidence owner"))?;
let raw = std::fs::read(path)?;
serde_json::from_slice(&raw).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
#[must_use]
pub fn unix_millis_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(u64::MAX)
}
#[must_use]
pub fn evidence_is_expired(artifact: &EvidenceArtifact, now_ms: u64) -> bool {
artifact.retention_state == EvidenceRetentionState::Expired
|| now_ms > artifact.retain_until_unix_ms
}
#[cfg(test)]
mod tests {
use super::*;
fn make_result(content: &str) -> ToolResult {
ToolResult::success(content.to_string())
}
#[test]
fn pass_through_below_threshold() {
let router = LargeOutputRouter::default();
let small = "x".repeat(100);
let result = make_result(&small);
assert_eq!(
router.route("read_file", &result, false),
RouteDecision::PassThrough
);
}
#[test]
fn default_threshold_is_32k_tokens() {
assert_eq!(DEFAULT_LARGE_OUTPUT_THRESHOLD_TOKENS, 32_768);
}
#[test]
fn synthesise_above_threshold() {
let router = LargeOutputRouter::default();
let big = "a".repeat(100_000);
let result = make_result(&big);
assert!(matches!(
router.route("read_file", &result, false),
RouteDecision::Synthesise { .. }
));
}
#[test]
fn raw_bypass_skips_routing() {
let router = LargeOutputRouter::default();
let big = "a".repeat(100_000);
let result = make_result(&big);
assert_eq!(
router.route("exec_shell", &result, true),
RouteDecision::PassThrough
);
}
#[test]
fn adaptive_evidence_cannot_bypass_context_bound_with_raw_flag() {
let router = LargeOutputRouter::default();
let big = make_result(&"a".repeat(100_000));
let (routing, _, _) = router.evidence_routing("exec_shell", &big, true);
assert_eq!(routing, EvidenceRouting::HandleOnly);
}
#[test]
fn error_results_always_pass_through() {
let router = LargeOutputRouter::default();
let big = "error: ".repeat(2_000);
let result = ToolResult::error(big);
assert_eq!(
router.route("exec_shell", &result, false),
RouteDecision::PassThrough
);
}
#[test]
fn per_tool_threshold_override() {
let mut per_tool = HashMap::new();
per_tool.insert("grep_files".to_string(), 100); let config = WorkshopConfig {
large_output_threshold_tokens: Some(4096),
per_tool_thresholds: Some(per_tool),
read_result_max_bytes: None,
tool_result_max_bytes: None,
};
let router = LargeOutputRouter::new(config);
let medium = "b".repeat(400);
let result = make_result(&medium);
assert!(matches!(
router.route("grep_files", &result, false),
RouteDecision::Synthesise { .. }
));
assert_eq!(
router.route("read_file", &result, false),
RouteDecision::PassThrough
);
}
#[test]
fn workshop_byte_budgets_raise_floor_only() {
let _guard = active_workshop_test_guard();
let installed = WorkshopConfig::install_active(Some(&WorkshopConfig {
large_output_threshold_tokens: None,
per_tool_thresholds: None,
read_result_max_bytes: Some(102_400),
tool_result_max_bytes: Some(80_000),
}));
assert_eq!(installed.read_result_max_bytes, Some(102_400));
assert_eq!(installed.tool_result_max_bytes, Some(80_000));
assert_eq!(
WorkshopConfig::active_read_result_max_bytes(),
Some(102_400)
);
assert_eq!(WorkshopConfig::active_tool_result_max_bytes(), Some(80_000));
let cleared = WorkshopConfig::install_active(None);
assert_eq!(cleared.read_result_max_bytes, None);
assert_eq!(cleared.tool_result_max_bytes, None);
assert_eq!(WorkshopConfig::active_read_result_max_bytes(), None);
assert_eq!(WorkshopConfig::active_tool_result_max_bytes(), None);
}
#[test]
fn estimate_tokens_conservative() {
assert_eq!(estimate_tokens("123456789"), 3);
assert_eq!(estimate_tokens("1234567890"), 4);
assert_eq!(estimate_tokens(""), 0);
}
#[test]
fn workshop_variables_store_and_take() {
let mut vars = WorkshopVariables::default();
assert!(vars.take_raw().is_none());
vars.store_raw("read_file", "raw content here");
let taken = vars.take_raw().expect("should have content");
assert_eq!(taken.0, "read_file");
assert_eq!(taken.1, "raw content here");
assert!(vars.take_raw().is_none());
}
#[test]
fn wrap_synthesis_includes_provenance_header() {
let wrapped = LargeOutputRouter::wrap_synthesis("web_search", "key facts here", 5000, 4096);
assert!(wrapped.contains("workshop-synthesis"));
assert!(wrapped.contains("web_search"));
assert!(wrapped.contains("5000"));
assert!(wrapped.contains("key facts here"));
}
}