From ebba7b699c537ddf0c427f9c80b8715dd0606dcc Mon Sep 17 00:00:00 2001 From: Steven Enamakel's Droid Date: Fri, 26 Jun 2026 00:19:45 -0700 Subject: [PATCH] fix(agent): tune tokenjuice compression per agent (#4139) --- .../settings-account-preferences.spec.ts | 27 ++++- .../developing/architecture/agent-harness.md | 2 +- .../agent/harness/builtin_definitions.rs | 3 + src/openhuman/agent/harness/definition.rs | 23 ++++ .../agent/harness/definition_tests.rs | 1 + .../agent/harness/engine/tool_source.rs | 1 + src/openhuman/agent/harness/engine/tools.rs | 20 ++-- .../agent/harness/payload_summarizer.rs | 1 + .../agent/harness/session/agent_tool_exec.rs | 5 +- .../agent/harness/session/builder/factory.rs | 6 +- .../agent/harness/session/builder/setters.rs | 11 ++ .../agent/harness/session/turn/core.rs | 1 + .../agent/harness/session/turn/tools.rs | 1 + .../harness/session/turn_engine_adapter.rs | 3 + src/openhuman/agent/harness/session/types.rs | 5 + .../harness/subagent_runner/ops/loop_.rs | 2 + .../harness/subagent_runner/ops/runner.rs | 1 + .../subagent_runner/ops/tool_source.rs | 2 + .../harness/subagent_runner/ops_tests.rs | 1 + src/openhuman/agent/library/ops.rs | 1 + src/openhuman/agent_registry/agents/loader.rs | 13 +++ .../channels/runtime/dispatch/mod.rs | 1 + src/openhuman/tokenjuice/mod.rs | 8 +- src/openhuman/tokenjuice/tool_integration.rs | 105 +++++++++++++++++- src/openhuman/tokenjuice/types.rs | 31 ++++++ src/openhuman/tools/orchestrator_tools.rs | 1 + ...rchivist_debug_round21_raw_coverage_e2e.rs | 2 + ...gent_harness_leftovers_raw_coverage_e2e.rs | 2 + tests/agent_harness_raw_coverage_e2e.rs | 2 + tests/agent_large_round25_raw_coverage_e2e.rs | 2 + ...agent_prompts_subagent_raw_coverage_e2e.rs | 2 + tests/agent_session_turn_raw_coverage_e2e.rs | 2 + tests/inference_agent_raw_coverage_e2e.rs | 2 + ...gent_credentials_state_raw_coverage_e2e.rs | 2 + ...ools_approval_channels_raw_coverage_e2e.rs | 2 + 35 files changed, 274 insertions(+), 20 deletions(-) diff --git a/app/test/playwright/specs/settings-account-preferences.spec.ts b/app/test/playwright/specs/settings-account-preferences.spec.ts index 9f2d4407e..70c40fe1a 100644 --- a/app/test/playwright/specs/settings-account-preferences.spec.ts +++ b/app/test/playwright/specs/settings-account-preferences.spec.ts @@ -25,6 +25,29 @@ async function gotoSettingsRoute(page: Page, hash: string): Promise { await dismissWalkthroughIfPresent(page); } +async function ensureRecoveryPhraseGenerateMode(page: Page): Promise { + const copyButton = page.getByRole('button', { name: 'Copy to Clipboard' }); + const replaceButton = page.getByRole('button', { name: 'Replace wallet' }); + + await expect + .poll( + async () => { + if (await copyButton.isVisible()) return 'generate'; + if (await replaceButton.isVisible()) return 'configured'; + return 'loading'; + }, + { timeout: 15_000 } + ) + .not.toBe('loading'); + + if (await replaceButton.isVisible()) { + await replaceButton.click(); + await page.getByRole('button', { name: 'I understand, replace my wallet' }).click(); + } + + await expect(copyButton).toBeVisible(); +} + test.describe('Settings - Account Preferences', () => { test.beforeEach(async ({ page }) => { await bootAuthenticatedPage(page, 'pw-settings-account-user'); @@ -64,8 +87,8 @@ test.describe('Settings - Account Preferences', () => { }) => { await gotoSettingsRoute(page, '/settings/recovery-phrase'); - await expect(page.getByRole('button', { name: 'Copy to Clipboard' })).toBeVisible(); - await page.locator('input[type="checkbox"]').first().check(); + await ensureRecoveryPhraseGenerateMode(page); + await page.locator('#mnemonic-confirm-checkbox').check(); await page.getByRole('button', { name: 'Save Recovery Phrase' }).click(); await expect(page.getByText('Recovery phrase saved')).toBeVisible(); diff --git a/gitbooks/developing/architecture/agent-harness.md b/gitbooks/developing/architecture/agent-harness.md index 30ebdfc2c..d01a421ed 100644 --- a/gitbooks/developing/architecture/agent-harness.md +++ b/gitbooks/developing/architecture/agent-harness.md @@ -129,7 +129,7 @@ Before a fresh tool result enters history (and ahead of the byte-budget backstop * **HTML** → strip markup to readable text. * **Plain text** → the opt-in Python/ML "Kompress" compressor (ModernBERT), or pass-through. -Every lossy compression offloads the original to the **CCR (Compress-Cache-Retrieve)** store behind a `⟦tj:⟧` marker, so compaction is effectively lossless: the agent calls `tokenjuice_retrieve` (token + optional byte/line range) to fetch the full original on demand. The same engine is exposed as a universal `compress_content(content, hint, opts)` for any large payload (file reads, web fetches), and as read-only `tokenjuice.*` debug RPCs. Configured via the `[tokenjuice]` block / `OPENHUMAN_TOKENJUICE_*` env. The ML (Kompress) path runs as a `kompress` backend of the shared [`runtime_python_server`](../../../src/openhuman/runtime_python_server/) (torch + ModernBERT pip-installed at runtime), gated by the `ml_compression_enabled` flag and degrading gracefully to a native compressor when the Python runtime is unavailable. +Every lossy compression offloads the original to the **CCR (Compress-Cache-Retrieve)** store behind a `⟦tj:⟧` marker, so compaction is effectively lossless: the agent calls `tokenjuice_retrieve` (token + optional byte/line range) to fetch the full original on demand. The same engine is exposed as a universal `compress_content(content, hint, opts)` for any large payload (file reads, web fetches), and as read-only `tokenjuice.*` debug RPCs. Configured via the `[tokenjuice]` block / `OPENHUMAN_TOKENJUICE_*` env. Agent definitions can override tool-result compression with `tokenjuice_compression = "auto" | "full" | "light" | "off"`; `auto` resolves coding-model agents (`[model] hint = "coding"`) to `light`, which disables CCR-backed lossy compression so coding agents keep raw build/test/diff/search text unless a reduction is truly lossless. Other agents default to `full`. The ML (Kompress) path runs as a `kompress` backend of the shared [`runtime_python_server`](../../../src/openhuman/runtime_python_server/) (torch + ModernBERT pip-installed at runtime), gated by the `ml_compression_enabled` flag and degrading gracefully to a native compressor when the Python runtime is unavailable. ### Self-healing for missing commands diff --git a/src/openhuman/agent/harness/builtin_definitions.rs b/src/openhuman/agent/harness/builtin_definitions.rs index 178f9477b..fbc67f77b 100644 --- a/src/openhuman/agent/harness/builtin_definitions.rs +++ b/src/openhuman/agent/harness/builtin_definitions.rs @@ -74,6 +74,7 @@ pub(crate) fn test_main_def() -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![ SubagentEntry::AgentId("__test_inherit_echo".into()), SubagentEntry::AgentId("__test_inherit_parallel_worker".into()), @@ -118,6 +119,7 @@ pub(crate) fn test_inherit_echo_def() -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: None, agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, @@ -155,6 +157,7 @@ pub(crate) fn test_inherit_parallel_worker_def() -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: None, agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, diff --git a/src/openhuman/agent/harness/definition.rs b/src/openhuman/agent/harness/definition.rs index 8106c5a4e..74560e7a2 100644 --- a/src/openhuman/agent/harness/definition.rs +++ b/src/openhuman/agent/harness/definition.rs @@ -25,6 +25,8 @@ use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize}; use std::path::PathBuf; +use crate::openhuman::tokenjuice::AgentTokenjuiceCompression; + /// Iteration-cap policy for a sub-agent. /// /// Controls how the harness enforces [`AgentDefinition::max_iterations`]: @@ -194,6 +196,14 @@ pub struct AgentDefinition { #[serde(default)] pub trigger_memory_agent: TriggerMemoryAgent, + /// Per-agent TokenJuice tool-result compression profile. + /// + /// `auto` keeps compression on for normal agents, but resolves coding-model + /// agents to `light` so CCR-backed lossy compression does not replace raw + /// build/test/diff/search text that coding agents often need exactly. + #[serde(default)] + pub tokenjuice_compression: AgentTokenjuiceCompression, + // ── delegation surface ───────────────────────────────────────────── /// Subagents this agent is allowed to spawn via synthesised /// `delegate_*` tools. Each entry expands at agent-build time into @@ -449,6 +459,19 @@ impl AgentDefinition { .max(super::tool_loop::EXTENDED_MAX_TOOL_ITERATIONS), } } + + /// Resolve the authored TokenJuice profile to the concrete per-call policy. + pub fn effective_tokenjuice_compression(&self) -> AgentTokenjuiceCompression { + match self.tokenjuice_compression { + AgentTokenjuiceCompression::Auto => match &self.model { + ModelSpec::Hint(hint) if hint.trim().eq_ignore_ascii_case("coding") => { + AgentTokenjuiceCompression::Light + } + _ => AgentTokenjuiceCompression::Full, + }, + other => other, + } + } } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/openhuman/agent/harness/definition_tests.rs b/src/openhuman/agent/harness/definition_tests.rs index fe4a0c5da..41bed6e7c 100644 --- a/src/openhuman/agent/harness/definition_tests.rs +++ b/src/openhuman/agent/harness/definition_tests.rs @@ -25,6 +25,7 @@ fn make_def(id: &str) -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: None, agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, diff --git a/src/openhuman/agent/harness/engine/tool_source.rs b/src/openhuman/agent/harness/engine/tool_source.rs index 328ae36ae..5e84a347c 100644 --- a/src/openhuman/agent/harness/engine/tool_source.rs +++ b/src/openhuman/agent/harness/engine/tool_source.rs @@ -143,6 +143,7 @@ impl ToolSource for RegistryToolSource<'_> { self.tool_policy, self.payload_summarizer, progress_call_id, + crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Full, ) .await } diff --git a/src/openhuman/agent/harness/engine/tools.rs b/src/openhuman/agent/harness/engine/tools.rs index 5cabbe57a..37d26b714 100644 --- a/src/openhuman/agent/harness/engine/tools.rs +++ b/src/openhuman/agent/harness/engine/tools.rs @@ -90,6 +90,7 @@ pub(crate) async fn run_one_tool( tool_policy: &dyn ToolPolicy, payload_summarizer: Option<&dyn PayloadSummarizer>, progress_call_id: &str, + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, ) -> ToolRunResult { let iteration_u32 = (iteration + 1) as u32; @@ -289,13 +290,15 @@ pub(crate) async fn run_one_tool( "[agent_loop] tool succeeded" ); let mut scrubbed = scrub_credentials(&output); - let (compacted, tj_stats) = crate::openhuman::tokenjuice::compact_tool_output( - &call.name, - Some(&call.arguments), - &scrubbed, - Some(0), - ) - .await; + let (compacted, tj_stats) = + crate::openhuman::tokenjuice::compact_tool_output_with_policy( + &call.name, + Some(&call.arguments), + &scrubbed, + Some(0), + tokenjuice_compression, + ) + .await; if tj_stats.applied { log::debug!( "[agent_loop] tokenjuice applied tool={} rule={} {}->{} bytes", @@ -379,11 +382,12 @@ pub(crate) async fn run_one_tool( tool = call.name.as_str(), "[agent_loop] tool returned error: {scrubbed}" ); - let (compacted, _) = crate::openhuman::tokenjuice::compact_tool_output( + let (compacted, _) = crate::openhuman::tokenjuice::compact_tool_output_with_policy( &call.name, Some(&call.arguments), &scrubbed, Some(1), + tokenjuice_compression, ) .await; (format!("Error: {compacted}"), false) diff --git a/src/openhuman/agent/harness/payload_summarizer.rs b/src/openhuman/agent/harness/payload_summarizer.rs index 8f68110ef..de679d2fd 100644 --- a/src/openhuman/agent/harness/payload_summarizer.rs +++ b/src/openhuman/agent/harness/payload_summarizer.rs @@ -365,6 +365,7 @@ mod tests { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: None, agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, diff --git a/src/openhuman/agent/harness/session/agent_tool_exec.rs b/src/openhuman/agent/harness/session/agent_tool_exec.rs index cbe7dc2fc..571367455 100644 --- a/src/openhuman/agent/harness/session/agent_tool_exec.rs +++ b/src/openhuman/agent/harness/session/agent_tool_exec.rs @@ -41,6 +41,8 @@ pub(super) struct AgentToolExecCtx<'a> { /// Whether Stage 1a (native content-aware compaction) runs before the /// byte budget. Sourced from `ContextManager::compaction_enabled`. pub compaction_enabled: bool, + /// Agent-level TokenJuice profile for this session's tool results. + pub tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, pub artifact_store: Option<&'a ToolResultArtifactStore>, } @@ -247,10 +249,11 @@ pub(super) async fn run_agent_tool_call( // content kind, routes to the matching compressor, and offloads the // original to CCR (recoverable via `tokenjuice_retrieve`); only ever // shrinks, otherwise passes the original through. - let raw_result = crate::openhuman::tokenjuice::compact_output( + let raw_result = crate::openhuman::tokenjuice::compact_output_with_policy( raw_result, &call.name, ctx.compaction_enabled, + ctx.tokenjuice_compression, ) .await; diff --git a/src/openhuman/agent/harness/session/builder/factory.rs b/src/openhuman/agent/harness/session/builder/factory.rs index c52104804..19cb04f89 100644 --- a/src/openhuman/agent/harness/session/builder/factory.rs +++ b/src/openhuman/agent/harness/session/builder/factory.rs @@ -1079,6 +1079,9 @@ impl Agent { let effective_trigger_memory_agent = target_def .map(|def| def.trigger_memory_agent) .unwrap_or_default(); + let effective_tokenjuice_compression = target_def + .map(|def| def.effective_tokenjuice_compression()) + .unwrap_or(crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Full); // Stamp the resolved agent definition id onto the Agent via the // builder. Without this call, `agent_definition_name` falls @@ -1201,7 +1204,8 @@ impl Agent { .agent_definition_name(agent_id.to_string()) .omit_profile(effective_omit_profile) .omit_memory_md(effective_omit_memory_md) - .trigger_memory_agent(effective_trigger_memory_agent); + .trigger_memory_agent(effective_trigger_memory_agent) + .tokenjuice_compression(effective_tokenjuice_compression); if let Some(ps) = payload_summarizer { builder = builder.payload_summarizer(ps); } diff --git a/src/openhuman/agent/harness/session/builder/setters.rs b/src/openhuman/agent/harness/session/builder/setters.rs index 61d8be429..09e81ede8 100644 --- a/src/openhuman/agent/harness/session/builder/setters.rs +++ b/src/openhuman/agent/harness/session/builder/setters.rs @@ -46,6 +46,7 @@ impl AgentBuilder { omit_memory_md: None, payload_summarizer: None, trigger_memory_agent: None, + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Full, tool_policy: None, archivist_hook: None, unified_compaction_enabled: true, @@ -365,6 +366,15 @@ impl AgentBuilder { self } + /// Set the per-agent TokenJuice tool-output compression profile. + pub fn tokenjuice_compression( + mut self, + profile: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, + ) -> Self { + self.tokenjuice_compression = profile; + self + } + /// Validates the configuration and constructs a new `Agent` instance. /// /// This method is responsible for wiring together the provided components, @@ -578,6 +588,7 @@ impl AgentBuilder { omit_memory_md: self.omit_memory_md.unwrap_or(true), payload_summarizer: self.payload_summarizer, trigger_memory_agent: self.trigger_memory_agent.unwrap_or_default(), + tokenjuice_compression: self.tokenjuice_compression, tool_policy: self.tool_policy.unwrap_or_else(|| { Arc::new(crate::openhuman::agent::tool_policy::AllowAllToolPolicy) }), diff --git a/src/openhuman/agent/harness/session/turn/core.rs b/src/openhuman/agent/harness/session/turn/core.rs index 35318e395..ebde73d0d 100644 --- a/src/openhuman/agent/harness/session/turn/core.rs +++ b/src/openhuman/agent/harness/session/turn/core.rs @@ -722,6 +722,7 @@ impl Agent { prefer_markdown: self.context.prefer_markdown_tool_output(), budget_bytes: self.context.tool_result_budget_bytes(), compaction_enabled: self.context.compaction_enabled(), + tokenjuice_compression: self.tokenjuice_compression, artifact_store: artifact_store.clone(), should_send_specs: self.tool_dispatcher.should_send_tool_specs(), advertised_specs: self.visible_tool_specs.as_ref().clone(), diff --git a/src/openhuman/agent/harness/session/turn/tools.rs b/src/openhuman/agent/harness/session/turn/tools.rs index a960aef12..d0fa9580d 100644 --- a/src/openhuman/agent/harness/session/turn/tools.rs +++ b/src/openhuman/agent/harness/session/turn/tools.rs @@ -58,6 +58,7 @@ impl Agent { prefer_markdown: self.context.prefer_markdown_tool_output(), budget_bytes: self.context.tool_result_budget_bytes(), compaction_enabled: self.context.compaction_enabled(), + tokenjuice_compression: self.tokenjuice_compression, artifact_store: Some(&artifact_store), }; agent_tool_exec::run_agent_tool_call(&ctx, &progress, call, iteration).await diff --git a/src/openhuman/agent/harness/session/turn_engine_adapter.rs b/src/openhuman/agent/harness/session/turn_engine_adapter.rs index 8cd8c5511..060e41d81 100644 --- a/src/openhuman/agent/harness/session/turn_engine_adapter.rs +++ b/src/openhuman/agent/harness/session/turn_engine_adapter.rs @@ -103,6 +103,8 @@ pub(super) struct AgentToolSource { /// Stage 1a kill-switch. Constant for the session, so (unlike the tool /// surface) it is set once at construction and never re-synced. pub compaction_enabled: bool, + /// Agent-level TokenJuice profile. Constant for the session. + pub tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, pub artifact_store: Option, pub should_send_specs: bool, pub advertised_specs: Vec, @@ -146,6 +148,7 @@ impl ToolSource for AgentToolSource { prefer_markdown: self.prefer_markdown, budget_bytes: self.budget_bytes, compaction_enabled: self.compaction_enabled, + tokenjuice_compression: self.tokenjuice_compression, artifact_store: self.artifact_store.as_ref(), }; let (exec_result, record) = diff --git a/src/openhuman/agent/harness/session/types.rs b/src/openhuman/agent/harness/session/types.rs index 82de2ee30..ca401dcd8 100644 --- a/src/openhuman/agent/harness/session/types.rs +++ b/src/openhuman/agent/harness/session/types.rs @@ -184,6 +184,9 @@ pub struct Agent { /// `Always` runs the dedicated memory retrieval agent once before /// the user's prompt is sent to this agent. pub(super) trigger_memory_agent: TriggerMemoryAgent, + /// Per-agent TokenJuice profile for tool results entering this session's + /// model context. + pub(super) tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, /// Pre-execution policy hook for tool calls in this session. The /// default policy allows all calls so existing agents keep their /// behaviour unless a caller opts into stricter policy. @@ -356,6 +359,8 @@ pub struct AgentBuilder { Option>, /// Forwarded to [`Agent::trigger_memory_agent`] at build time. pub(super) trigger_memory_agent: Option, + /// Per-agent TokenJuice tool-output compression profile. + pub(super) tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, /// Optional pre-execution tool policy. Defaults to allow-all. pub(super) tool_policy: Option>, /// Optional reference to the production `ArchivistHook`. Set when diff --git a/src/openhuman/agent/harness/subagent_runner/ops/loop_.rs b/src/openhuman/agent/harness/subagent_runner/ops/loop_.rs index 769fd12a5..8215fca88 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops/loop_.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops/loop_.rs @@ -60,6 +60,7 @@ pub(super) async fn run_inner_loop( handoff_cache: Option<&ResultHandoffCache>, parent: &ParentExecutionContext, extended_policy: bool, + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, // Optional steering channel. When `Some`, the child engine drains // steer/collect messages at iteration boundaries so the parent can // `steer_subagent` a running async sub-agent. `None` = non-steerable. @@ -146,6 +147,7 @@ pub(super) async fn run_inner_loop( handoff_cache, policy: crate::openhuman::tools::policy::DefaultToolPolicy, agent_id: agent_id.to_string(), + tokenjuice_compression, }; let mut observer = SubagentObserver { worker_thread_id, diff --git a/src/openhuman/agent/harness/subagent_runner/ops/runner.rs b/src/openhuman/agent/harness/subagent_runner/ops/runner.rs index 0bfe3dd50..c9ccaf211 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops/runner.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops/runner.rs @@ -812,6 +812,7 @@ async fn run_typed_mode( handoff_cache.as_deref(), parent, definition.iteration_policy == IterationPolicy::Extended, + definition.effective_tokenjuice_compression(), options.run_queue.clone(), )) .await?; diff --git a/src/openhuman/agent/harness/subagent_runner/ops/tool_source.rs b/src/openhuman/agent/harness/subagent_runner/ops/tool_source.rs index 4006bdb65..cce0fc554 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops/tool_source.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops/tool_source.rs @@ -30,6 +30,7 @@ pub(super) struct SubagentToolSource<'a> { pub(super) handoff_cache: Option<&'a ResultHandoffCache>, pub(super) policy: crate::openhuman::tools::policy::DefaultToolPolicy, pub(super) agent_id: String, + pub(super) tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression, } #[async_trait::async_trait] @@ -118,6 +119,7 @@ impl super::super::super::engine::ToolSource for SubagentToolSource<'_> { &self.policy, None, progress_call_id, + self.tokenjuice_compression, ) .await; diff --git a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs index ab079a7a9..fcdf2fbe0 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs @@ -64,6 +64,7 @@ fn make_def_named_tools(names: &[&str]) -> AgentDefinition { sandbox_mode: crate::openhuman::agent::harness::definition::SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: None, agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, diff --git a/src/openhuman/agent/library/ops.rs b/src/openhuman/agent/library/ops.rs index 7314dbb33..7a0e9c45a 100644 --- a/src/openhuman/agent/library/ops.rs +++ b/src/openhuman/agent/library/ops.rs @@ -147,6 +147,7 @@ mod tests { sandbox_mode: SandboxMode::ReadOnly, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![ SubagentEntry::AgentId("critic".to_string()), SubagentEntry::Skills(SkillsWildcard { diff --git a/src/openhuman/agent_registry/agents/loader.rs b/src/openhuman/agent_registry/agents/loader.rs index 7e823f1ea..713c9e17e 100644 --- a/src/openhuman/agent_registry/agents/loader.rs +++ b/src/openhuman/agent_registry/agents/loader.rs @@ -356,6 +356,7 @@ mod tests { use crate::openhuman::agent::harness::definition::{ ModelSpec, SandboxMode, SubagentEntry, ToolScope, TriggerMemoryAgent, }; + use crate::openhuman::tokenjuice::AgentTokenjuiceCompression; #[test] fn all_builtins_parse() { @@ -643,6 +644,10 @@ mod tests { assert_eq!(def.sandbox_mode, SandboxMode::Sandboxed); assert!(!def.omit_safety_preamble); assert_eq!(def.max_iterations, 10); + assert_eq!( + def.effective_tokenjuice_compression(), + AgentTokenjuiceCompression::Light + ); } #[test] @@ -651,6 +656,10 @@ mod tests { assert_eq!(def.sandbox_mode, SandboxMode::Sandboxed); assert_eq!(def.max_iterations, 2); assert!(!def.omit_safety_preamble); + assert_eq!( + def.effective_tokenjuice_compression(), + AgentTokenjuiceCompression::Light + ); } #[test] @@ -659,6 +668,10 @@ mod tests { assert_eq!(def.sandbox_mode, SandboxMode::Sandboxed); assert_eq!(def.max_iterations, 10); assert!(!def.omit_safety_preamble); + assert_eq!( + def.effective_tokenjuice_compression(), + AgentTokenjuiceCompression::Light + ); match &def.tools { ToolScope::Named(names) => { for required in ["node_exec", "npm_exec", "apply_patch", "update_memory_md"] { diff --git a/src/openhuman/channels/runtime/dispatch/mod.rs b/src/openhuman/channels/runtime/dispatch/mod.rs index ca8b8fb87..00de3d349 100644 --- a/src/openhuman/channels/runtime/dispatch/mod.rs +++ b/src/openhuman/channels/runtime/dispatch/mod.rs @@ -121,6 +121,7 @@ mod scoping_tests { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: None, agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, diff --git a/src/openhuman/tokenjuice/mod.rs b/src/openhuman/tokenjuice/mod.rs index ba2f3d40c..e2a136f2f 100644 --- a/src/openhuman/tokenjuice/mod.rs +++ b/src/openhuman/tokenjuice/mod.rs @@ -124,11 +124,11 @@ pub use detect::detect_content_kind; pub use reduce::reduce_execution_with_rules; pub use rules::{load_builtin_rules, load_rules, LoadRuleOptions}; pub use tool_integration::{ - compact_output, compact_tool_output, configure, current_options, install_config, - CompactionStats, + compact_output, compact_output_with_policy, compact_tool_output, + compact_tool_output_with_policy, configure, current_options, install_config, CompactionStats, }; pub use tools::TokenjuiceRetrieveTool; pub use types::{ - CompactResult, CompressInput, CompressOptions, CompressOutput, CompressedOutput, - CompressorKind, ContentHint, ContentKind, ReduceOptions, ToolExecutionInput, + AgentTokenjuiceCompression, CompactResult, CompressInput, CompressOptions, CompressOutput, + CompressedOutput, CompressorKind, ContentHint, ContentKind, ReduceOptions, ToolExecutionInput, }; diff --git a/src/openhuman/tokenjuice/tool_integration.rs b/src/openhuman/tokenjuice/tool_integration.rs index 79a3fd1cd..920f0ca82 100644 --- a/src/openhuman/tokenjuice/tool_integration.rs +++ b/src/openhuman/tokenjuice/tool_integration.rs @@ -20,7 +20,7 @@ use serde_json::Value; use std::sync::RwLock; use super::compress::route; -use super::types::{CompressInput, CompressOptions, ContentHint}; +use super::types::{AgentTokenjuiceCompression, CompressInput, CompressOptions, ContentHint}; /// Skip compaction for outputs smaller than this (bytes) by default. Tiny /// outputs have no headroom and risk distortion. Overridable per the config's @@ -52,6 +52,24 @@ pub fn current_options() -> CompressOptions { .clone() } +fn options_for_agent(profile: AgentTokenjuiceCompression) -> Option { + match profile { + AgentTokenjuiceCompression::Off => None, + AgentTokenjuiceCompression::Auto | AgentTokenjuiceCompression::Full => { + Some(current_options()) + } + AgentTokenjuiceCompression::Light => { + let mut opts = current_options(); + // Coding agents need raw, exact tool text more than aggressive token + // savings. Disabling CCR makes every lossy compressor decline in + // route(), while still allowing any truly lossless reduction. + opts.ccr_enabled = false; + opts.ml_text_enabled = false; + Some(opts) + } + } +} + /// Install the full TokenJuice runtime configuration in one call at startup: /// router/compressor options, CCR cache limits, and the optional on-disk tier. /// Kept free of the config-schema type so `tokenjuice` stays decoupled — the @@ -112,9 +130,45 @@ pub async fn compact_tool_output( arguments: Option<&Value>, output: &str, exit_code: Option, +) -> (String, CompactionStats) { + compact_tool_output_with_policy( + tool_name, + arguments, + output, + exit_code, + AgentTokenjuiceCompression::Full, + ) + .await +} + +/// Compact a tool call's output using an agent-level TokenJuice profile. +pub async fn compact_tool_output_with_policy( + tool_name: &str, + arguments: Option<&Value>, + output: &str, + exit_code: Option, + profile: AgentTokenjuiceCompression, ) -> (String, CompactionStats) { let original_bytes = output.len(); + let Some(opts) = options_for_agent(profile) else { + log::debug!( + "[tokenjuice] agent profile disabled compaction tool={} bytes={}", + tool_name, + original_bytes + ); + return ( + output.to_string(), + CompactionStats { + tool_name: tool_name.to_string(), + original_bytes, + compacted_bytes: original_bytes, + rule_id: "none/agent-profile-off".to_string(), + applied: false, + }, + ); + }; + // A recovery tool's output is the original we previously offloaded — never // re-compact it, or the agent could never see the full data it asked for. if super::cache::is_recovery_tool(tool_name) { @@ -130,7 +184,6 @@ pub async fn compact_tool_output( ); } - let opts = current_options(); let (command, argv) = extract_command_argv(arguments); let hint = ContentHint { source_tool: Some(tool_name.to_string()), @@ -167,12 +220,29 @@ pub async fn compact_tool_output( /// Minimal compaction for call sites that only have content + tool name. The /// `enabled` flag is an explicit kill-switch on top of the configured options. pub async fn compact_output(content: String, tool_name: &str, enabled: bool) -> String { + compact_output_with_policy( + content, + tool_name, + enabled, + AgentTokenjuiceCompression::Full, + ) + .await +} + +/// Minimal compaction with an agent-level TokenJuice profile. +pub async fn compact_output_with_policy( + content: String, + tool_name: &str, + enabled: bool, + profile: AgentTokenjuiceCompression, +) -> String { // The call-site `enabled` flag and the configured router switch are both // hard off-switches; either one short-circuits to the untouched original. if !enabled || !current_options().router_enabled { return content; } - let (text, _stats) = compact_tool_output(tool_name, None, &content, None).await; + let (text, _stats) = + compact_tool_output_with_policy(tool_name, None, &content, None, profile).await; text } @@ -280,6 +350,35 @@ mod tests { assert_eq!(compact_output(big.clone(), "grep", false).await, big); } + #[tokio::test] + async fn light_agent_profile_declines_lossy_ccr_compaction() { + let mut lines = vec!["On branch main".to_owned()]; + for i in 0..200 { + lines.push(format!("\tmodified: src/file_{i}.rs")); + } + let output = lines.join("\n"); + let args = json!({"command": "git status"}); + let (returned, stats) = compact_tool_output_with_policy( + "shell", + Some(&args), + &output, + Some(0), + AgentTokenjuiceCompression::Light, + ) + .await; + assert_eq!(returned, output); + assert!(!stats.applied); + } + + #[tokio::test] + async fn off_agent_profile_bypasses_router() { + let big = "x".repeat(5000); + let returned = + compact_output_with_policy(big.clone(), "grep", true, AgentTokenjuiceCompression::Off) + .await; + assert_eq!(returned, big); + } + #[test] fn extract_argv_handles_common_shapes() { let (cmd, argv) = extract_command_argv(Some(&json!({"command": "git status"}))); diff --git a/src/openhuman/tokenjuice/types.rs b/src/openhuman/tokenjuice/types.rs index 4355c81a2..d72c50412 100644 --- a/src/openhuman/tokenjuice/types.rs +++ b/src/openhuman/tokenjuice/types.rs @@ -301,6 +301,37 @@ pub struct CompactResult { pub classification: ClassificationResult, } +/// Per-agent TokenJuice profile. +/// +/// `Auto` is resolved by the agent definition layer. TokenJuice itself treats +/// `Auto` like `Full` so non-agent callers keep the global `[tokenjuice]` +/// behaviour unless they explicitly pass a narrower profile. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum AgentTokenjuiceCompression { + /// Let the agent definition/runtime choose. Coding agents resolve this to + /// [`Self::Light`]; other agents resolve to [`Self::Full`]. + #[default] + Auto, + /// Use the process-global TokenJuice configuration unchanged. + Full, + /// Keep only non-lossy reductions; disables CCR-backed lossy compaction. + Light, + /// Bypass TokenJuice for this agent's tool results. + Off, +} + +impl AgentTokenjuiceCompression { + pub fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::Full => "full", + Self::Light => "light", + Self::Off => "off", + } + } +} + // --------------------------------------------------------------------------- // Content Router (TokenJuice 2.0) — content-kind detection + compressor dispatch // --------------------------------------------------------------------------- diff --git a/src/openhuman/tools/orchestrator_tools.rs b/src/openhuman/tools/orchestrator_tools.rs index 0209acb91..a6907554d 100644 --- a/src/openhuman/tools/orchestrator_tools.rs +++ b/src/openhuman/tools/orchestrator_tools.rs @@ -280,6 +280,7 @@ mod tests { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: crate::openhuman::tokenjuice::AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: delegate_name.map(String::from), agent_tier: crate::openhuman::agent::harness::definition::AgentTier::Worker, diff --git a/tests/agent_archivist_debug_round21_raw_coverage_e2e.rs b/tests/agent_archivist_debug_round21_raw_coverage_e2e.rs index 9cc8913bc..503cd5c9b 100644 --- a/tests/agent_archivist_debug_round21_raw_coverage_e2e.rs +++ b/tests/agent_archivist_debug_round21_raw_coverage_e2e.rs @@ -20,6 +20,7 @@ use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts, }; use openhuman_core::openhuman::memory_store::{events, fts5, profile, segments}; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{PermissionLevel, Tool, ToolResult}; use parking_lot::Mutex; use rusqlite::Connection; @@ -255,6 +256,7 @@ fn definition(max_iterations: usize) -> AgentDefinition { timeout_secs: None, sandbox_mode: SandboxMode::None, background: false, + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: Default::default(), diff --git a/tests/agent_harness_leftovers_raw_coverage_e2e.rs b/tests/agent_harness_leftovers_raw_coverage_e2e.rs index 12a7a1682..6eb57d57e 100644 --- a/tests/agent_harness_leftovers_raw_coverage_e2e.rs +++ b/tests/agent_harness_leftovers_raw_coverage_e2e.rs @@ -21,6 +21,7 @@ use openhuman_core::openhuman::inference::provider::{ use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary as MemoryNamespaceSummary, RecallOpts, }; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{PermissionLevel, Tool, ToolContent, ToolResult}; use parking_lot::Mutex; use serde_json::json; @@ -363,6 +364,7 @@ fn definition(max_result_chars: Option) -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: AgentTier::Worker, diff --git a/tests/agent_harness_raw_coverage_e2e.rs b/tests/agent_harness_raw_coverage_e2e.rs index 008bf22ae..098c00efd 100644 --- a/tests/agent_harness_raw_coverage_e2e.rs +++ b/tests/agent_harness_raw_coverage_e2e.rs @@ -15,6 +15,7 @@ use openhuman_core::openhuman::inference::provider::{ ChatMessage, ChatRequest, ChatResponse, Provider, ToolCall, UsageInfo, }; use openhuman_core::openhuman::memory::{Memory, MemoryCategory, MemoryEntry, NamespaceSummary}; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::SpawnSubagentTool; use openhuman_core::openhuman::tools::{Tool, ToolResult}; use parking_lot::Mutex; @@ -321,6 +322,7 @@ fn coverage_definition() -> AgentDefinition { sandbox_mode: SandboxMode::ReadOnly, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: Default::default(), diff --git a/tests/agent_large_round25_raw_coverage_e2e.rs b/tests/agent_large_round25_raw_coverage_e2e.rs index 5d3750ca9..61e1845cf 100644 --- a/tests/agent_large_round25_raw_coverage_e2e.rs +++ b/tests/agent_large_round25_raw_coverage_e2e.rs @@ -15,6 +15,7 @@ use openhuman_core::openhuman::inference::provider::{ use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts, }; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{PermissionLevel, Tool, ToolResult}; use parking_lot::Mutex; use serde_json::json; @@ -295,6 +296,7 @@ fn integrations_definition() -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: Default::default(), diff --git a/tests/agent_prompts_subagent_raw_coverage_e2e.rs b/tests/agent_prompts_subagent_raw_coverage_e2e.rs index f7beb6bd6..a070d3169 100644 --- a/tests/agent_prompts_subagent_raw_coverage_e2e.rs +++ b/tests/agent_prompts_subagent_raw_coverage_e2e.rs @@ -21,6 +21,7 @@ use openhuman_core::openhuman::inference::provider::{ use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary as MemoryNamespaceSummary, RecallOpts, }; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{PermissionLevel, Tool, ToolResult}; use parking_lot::Mutex; use serde_json::json; @@ -261,6 +262,7 @@ fn definition(prompt: PromptSource) -> AgentDefinition { sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: Default::default(), diff --git a/tests/agent_session_turn_raw_coverage_e2e.rs b/tests/agent_session_turn_raw_coverage_e2e.rs index 9ba4b4eb0..8c5ce94ce 100644 --- a/tests/agent_session_turn_raw_coverage_e2e.rs +++ b/tests/agent_session_turn_raw_coverage_e2e.rs @@ -22,6 +22,7 @@ use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts, }; use openhuman_core::openhuman::memory_store; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::traits::ToolCallOptions; use openhuman_core::openhuman::tools::{ PermissionLevel, Tool, ToolContent, ToolResult, ToolScope as RuntimeToolScope, @@ -1020,6 +1021,7 @@ fn definition( sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: AgentTier::Worker, diff --git a/tests/inference_agent_raw_coverage_e2e.rs b/tests/inference_agent_raw_coverage_e2e.rs index 20128744f..d5c5ec115 100644 --- a/tests/inference_agent_raw_coverage_e2e.rs +++ b/tests/inference_agent_raw_coverage_e2e.rs @@ -181,6 +181,7 @@ use openhuman_core::openhuman::profiles::{ }; use openhuman_core::openhuman::security::SecurityPolicy; use openhuman_core::openhuman::todos::ops::BoardLocation; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{Tool, ToolResult, ToolSpec}; static ENV_LOCK: Mutex<()> = Mutex::new(()); @@ -1602,6 +1603,7 @@ named = ["todo", "plan_exit"] sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: AgentTier::Worker, diff --git a/tests/tools_agent_credentials_state_raw_coverage_e2e.rs b/tests/tools_agent_credentials_state_raw_coverage_e2e.rs index 60c985077..4755777e4 100644 --- a/tests/tools_agent_credentials_state_raw_coverage_e2e.rs +++ b/tests/tools_agent_credentials_state_raw_coverage_e2e.rs @@ -42,6 +42,7 @@ use openhuman_core::openhuman::inference::provider::{ }; use openhuman_core::openhuman::memory::{Memory, MemoryCategory, MemoryEntry, NamespaceSummary}; use openhuman_core::openhuman::security::{AuditLogger, SecurityPolicy}; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{ all_tools, BrowserTool, ComputerUseConfig, SpawnSubagentTool, Tool, ToolResult, }; @@ -408,6 +409,7 @@ fn agent_definition(id: &str, max_result_chars: Option) -> AgentDefinitio sandbox_mode: SandboxMode::ReadOnly, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: Default::default(), diff --git a/tests/tools_approval_channels_raw_coverage_e2e.rs b/tests/tools_approval_channels_raw_coverage_e2e.rs index 9bbf27f8e..37e16fb29 100644 --- a/tests/tools_approval_channels_raw_coverage_e2e.rs +++ b/tests/tools_approval_channels_raw_coverage_e2e.rs @@ -85,6 +85,7 @@ use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts, }; use openhuman_core::openhuman::security::{AuditLogger, AutonomyLevel, SecurityPolicy}; +use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tool_registry::ops::diagnostics_for_config; use openhuman_core::openhuman::tool_registry::{ all_tool_registry_controller_schemas, all_tool_registry_registered_controllers, @@ -301,6 +302,7 @@ fn coverage_agent_definition( sandbox_mode: SandboxMode::None, background: false, trigger_memory_agent: Default::default(), + tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: vec![], delegate_name: delegate_name.map(str::to_string), agent_tier: AgentTier::Worker,