Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2f13f00
Merge branch 'life-scenarios-cause1' into life-scenarios-cache
senamakel Sep 23, 2026
8184729
Merge remote-tracking branch 'upstream/main' into life-scenarios-cache
senamakel Sep 23, 2026
5aa40ec
fix(agent): handle tool output with no content
senamakel Sep 23, 2026
b351fb3
fix(agent): handle tool output with no content
senamakel Sep 23, 2026
a12462f
fix(tool_output): correct processing order in comments and logs
senamakel Sep 23, 2026
be7987c
fix(config): update agent schema to use correct field name
senamakel Sep 23, 2026
6940dfe
chore: files changed crates/openhuman-core/src/config/schema/agent.rs
senamakel Sep 23, 2026
58cd38d
fix(web_chat): handle empty session ID in session lookup
senamakel Sep 23, 2026
6d8b6cd
feat(life-scenarios): make benchmark agent available on both drivers
senamakel Sep 23, 2026
da9c1e3
test: add tests for chat_agent_id selection and fallback behaviour
senamakel Sep 23, 2026
da27461
feat(config): add chat_agent_id field to agent settings patch
senamakel Sep 23, 2026
84b3d93
feat(config): add chat_agent_id field to AgentSettingsUpdate
senamakel Sep 23, 2026
e12db45
feat(config): extend update_agent_settings schema with chat_agent_id …
senamakel Sep 23, 2026
05a2fb9
fix(scripts/life-scenarios): set chat_agent_id via RPC before running…
senamakel Sep 23, 2026
7830814
Merge remote-tracking branch 'upstream/main' into life-scenarios-cache
senamakel Sep 23, 2026
f619ad2
feat(tool_output): add raw fetch detection to skip summarizer
senamakel Sep 23, 2026
e02e9f7
fix(tool_output): exempt raw fetch results from payload summarization
senamakel Sep 23, 2026
3cebf56
feat(tool_output): consume raw fetch entry unconditionally
senamakel Sep 23, 2026
16daec2
feat(tinyagents): add raw_fetches field to middleware contexts
senamakel Sep 23, 2026
40716fa
chore: files changed crates/openhuman-core/src/agent/tinyagents/middl…
senamakel Sep 23, 2026
3c0b46d
test(tool-output): add tests for raw web_fetch exemption from payload…
senamakel Sep 23, 2026
5100716
test: add missing fields to test struct constructors
senamakel Sep 23, 2026
25e2f42
Merge remote-tracking branch 'upstream/main' into life-scenarios-cache
senamakel Sep 24, 2026
8dfb666
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/6586
senamakel Sep 24, 2026
63d76d1
fix(scripts): handle empty agentId in life-scenarios runner
senamakel Sep 24, 2026
bcf271b
feat(agent): validate chat_agent_id before persisting or routing
senamakel Sep 24, 2026
67b07ad
test(middleware-tool-output): add test that raw web fetch skips paylo…
senamakel Sep 24, 2026
69ed370
fix(orchestrator): simplify workflow building instructions in prompt
senamakel Sep 24, 2026
45ba097
fix(agent): validate agent timeout before applying chat agent id
senamakel Sep 24, 2026
9296c49
fix(config): avoid moving the chat agent id on update
senamakel Sep 24, 2026
85c0f2c
chore: retrigger timed-out review
senamakel Sep 24, 2026
762ce76
test(config): add test for blank chat agent id clearing override
senamakel Sep 24, 2026
2a71309
test(config): reformat assertion for readability
senamakel Sep 24, 2026
7cea769
merge: reconcile upstream main
senamakel Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ use tinytools_agent::dialect::{
};

impl OpenHumanSessionHost {
/// Returns whether `agent_id` resolves to a runnable definition for this
/// configuration. This is deliberately the same resolution path used by
/// [`Self::from_config_for_agent`], so configuration writers cannot save
/// a web-chat route that the session factory would later reject.
pub(crate) fn is_runnable_agent_id(config: &Config, agent_id: &str) -> bool {
resolve_target_definition(config, agent_id).is_ok()
}

/// Constructs an `OpenHumanSessionHost` instance from a global system configuration.
///
/// Thin wrapper around [`OpenHumanSessionHost::from_config_for_agent`] that always
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,37 @@ pub(crate) fn is_truncation_exempt(name: &str) -> bool {
COMPACTION_EXEMPT_TOOLS.contains(&name) || DISCOVERY_TOOLS.contains(&name)
}

/// Whether this call is a `web_fetch` that asked for the body **as sent**
/// (`raw: true`), following `use_skill` into the tool it wraps exactly as
/// [`artifact_read_target`] does.
///
/// Such a result is exempt from the payload summarizer (step 2). `web_fetch`
/// normally returns HTML as Markdown — `tinyjuice::compressors::html::
/// html_to_markdown`, which drops scripts and styling — and `raw: true` turns
/// that off, so the payload is unconverted markup. Paying a full-price,
/// *uncached* model call to have an LLM paraphrase minified JS and CSS is the
/// worst trade in the ladder: one observed `raw: true` fetch of a 183 KB page
/// cost 44,561 prompt tokens, over half that turn's entire summarizer budget,
/// to re-describe a page the same turn had already read as clean Markdown.
///
/// It is also the wrong answer to the question asked. A caller who wants the
/// body as sent wants the bytes, not a summary of them; steps 3–4 still bound
/// the result and spill the remainder to an artifact the model pages with
/// `file_read`, which returns the real markup, losslessly and without a model
/// call.
fn is_raw_fetch(tool_name: &str, args: &serde_json::Value) -> bool {
Comment thread
senamakel marked this conversation as resolved.
Comment thread
senamakel marked this conversation as resolved.
const FETCH_TOOL: &str = "web_fetch";
let (name, args) = if tool_name == "use_skill" {
match (args.get("tool").and_then(|t| t.as_str()), args.get("args")) {
(Some(inner), Some(inner_args)) => (inner, inner_args),
_ => return false,
}
} else {
(tool_name, args)
};
name == FETCH_TOOL && args.get("raw").and_then(|r| r.as_bool()).unwrap_or(false)
}

/// `after_tool`: apply the semantic payload summarizer (when configured) and
/// then the hard per-tool-result byte cap to each tool result's model-facing
/// content, before it enters the transcript. The graph analogue of the byte cap
Expand Down Expand Up @@ -160,6 +191,11 @@ pub(crate) struct ToolOutputMiddleware {
/// their calls lose the argument; any other tool with a parameter of the
/// same name (an MCP server's, say) keeps it.
pub(crate) summary_focus_tools: HashSet<String>,
/// Calls that asked `web_fetch` for the raw body, keyed by call id. Filled
/// in `before_tool`, where the arguments are visible, and consumed in
/// `after_tool`, where they are not — the same seam `artifact_reads` uses,
/// and for the same reason. See [`is_raw_fetch`].
pub(crate) raw_fetches: Mutex<std::collections::HashSet<String>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority critical security confident

Initialize raw_fetches at every construction site

This adds a required field to ToolOutputMiddleware, but existing struct literals in the middleware test helpers and artifact tests are not updated by this change. Those literals will fail to compile with a missing-field error. Add raw_fetches: Default::default() to every constructor, or provide a constructor that centralizes initialization.


Additional critique observation

priority critical confident

Initialize raw_fetches in every middleware constructor

[RULE] build-break

This adds a required field to ToolOutputMiddleware, but the diff updates only the constructor in tool_output_tests.rs and the production constructor in turn_context.rs. The repository search still finds middleware literals in middleware_tests.rs, middleware_tool_output_artifact_tests.rs, and several locations in middleware_tool_output_tests.rs that are outside this diff and therefore still lack raw_fetches. Those test targets fail to compile with a missing-field error. Add raw_fetches: Default::default() to every remaining literal (or make the field constructible by default).

[RULE] build-break ·

}

impl ToolOutputMiddleware {
Expand Down Expand Up @@ -251,6 +287,16 @@ impl Middleware<(), crate::agent::tinyagents::host::OpenHumanRunContext> for Too
reads.insert(call.id.clone(), read);
}
}
if is_raw_fetch(&call.name, &call.arguments) {
tracing::debug!(
tool = %call.name,
call_id = %call.id,
"[tinyagents::mw] raw fetch: exempting the result from the payload summarizer"
);
if let Ok(mut raw) = self.raw_fetches.lock() {
raw.insert(call.id.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

Clean up raw-fetch entries when after_tool is skipped

before_tool records every raw fetch, but the entry is only removed in after_tool. A later middleware can veto the call, in which case after_tool is never invoked; the call ID then remains in this set for the lifetime of the middleware. Repeated vetoed raw fetches therefore grow the set without bound. Ensure entries are removed when a call is rejected or use lifecycle cleanup that runs for vetoed calls as well.

[RULE] unbounded-state ·

}
}
Ok(())
}

Expand All @@ -270,6 +316,13 @@ impl Middleware<(), crate::agent::tinyagents::host::OpenHumanRunContext> for Too
// compacts it, and the byte budget persists it as a *new* artifact with
// the same bounded preview — a loop that never reaches the data (#6284).
// Serve it verbatim, one bounded page at a time.
// Consumed unconditionally so the entry cannot outlive its call, even on
// the artifact-read early return below.
let raw_fetch = self
Comment thread
senamakel marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Test the after_tool raw-fetch skip branch

The current test coverage verifies only the classifier, not the stateful before_tool → after_tool path that consumes the call ID. Add a direct test for this branch, including a wrapped use_skill call, to ensure the recorded exemption reaches after_tool and is removed from the pending set.

[RULE] missing-behavior-test ·

.raw_fetches
.lock()
.ok()
.is_some_and(|mut raw| raw.remove(&call_id));
let artifact_read = self
.artifact_reads
.lock()
Expand Down Expand Up @@ -311,7 +364,7 @@ impl Middleware<(), crate::agent::tinyagents::host::OpenHumanRunContext> for Too
tracing::debug!(
tool = tool_name,
bytes = content.len(),
"[tinyagents::mw] compaction-exempt: skipping payload summarizer + tokenjuice"
"[tinyagents::mw] compaction-exempt: skipping tokenjuice + payload summarizer"
);
}
if truncation_exempt {
Expand Down Expand Up @@ -386,7 +439,29 @@ impl Middleware<(), crate::agent::tinyagents::host::OpenHumanRunContext> for Too
.and_then(|mut focus| focus.remove(&call_id));
let wants_tinyjuice =
self.tokenjuice_compaction_enabled || self.payload_summarizer.is_some();
if !compaction_exempt && wants_tinyjuice && (tool_cap.is_none() || focus.is_some()) {
// A `raw: true` `web_fetch` is excluded outright, `summary_focus`
// or not: it asked for the body *as sent*, which switches off the
// HTML→Markdown conversion, so the payload is unconverted markup
// and a summary of it is an uncached model call spent paraphrasing
// minified JS. One observed such fetch cost 44,561 prompt tokens —
// over half that turn's summarizer budget — to re-describe a page
// the same turn had already read as clean Markdown. Step 3 still
// bounds it and spills the rest to an artifact, which hands back
// the real markup losslessly and for no model call. See
// [`is_raw_fetch`].
if raw_fetch {
Comment thread
senamakel marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Add an end-to-end test for raw-fetch summarizer exemption

The new behavior is exercised only through is_raw_fetch; no test drives after_tool with a raw web_fetch result to prove that the payload summarizer is actually skipped while the result still follows the capping/artifact path. A regression in the call-id bookkeeping or middleware branch would leave the helper test passing while reintroducing the expensive summarizer call. Add a middleware-level test that invokes the before/after hooks with raw: true and asserts the summarizer is not called and the result is bounded as intended.


Additional critique observation

priority medium confident

Test the after_tool raw-fetch skip branch

[RULE] missing-regression-test

This adds a stateful before_tool/after_tool path, but no test exercises a web_fetch call with raw: true (or the use_skill wrapper) and verifies that the payload summarizer is not invoked while the result still follows the cap and artifact path. A future change can easily break the call-id bookkeeping or wrapper detection without detection. Add a focused Rust domain test beside this middleware.

[RULE] missing-behavior-test ·

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium tests likely

Drive raw-fetch summarizer exemption through an end-to-end test

The exemption for raw: true web_fetch is verified by a direct middleware unit test (a_raw_web_fetch_never_prepares_a_payload_summary), but no end-to-end test exercises the full tool execution path to confirm that the production tool loop records the raw-fetch identity and reaches the exemption. A regression in the wiring or metadata propagation could leave the unit test green while the actual feature breaks.

[RULE] missing-integration-test ·

tracing::info!(
tool = tool_name,
bytes = content.len(),
"[tinyagents::mw] raw fetch: skipping the tinyjuice summary, \
capping and spilling to an artifact instead"
);
}
if !raw_fetch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Drive raw-fetch summarizer exemption with an end-to-end test

The exemption changes the model-facing behavior of a real web_fetch(raw: true) turn, but the available coverage does not drive that route through the harness. Add an end-to-end scenario that invokes raw fetch, verifies no summarizer call is made, and verifies oversized content remains recoverable through the artifact paging path.

[RULE] missing-end-to-end-test ·

&& !compaction_exempt
&& wants_tinyjuice
&& (tool_cap.is_none() || focus.is_some())
{
// Bind a summary call to this turn only when the result is big
// enough for TinyJuice to want one; building the child context for
// every small result would be waste.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ async fn same_tool_calls_persist_artifacts_under_distinct_call_ids() {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
};
let mut ctx = context();

Expand Down Expand Up @@ -54,3 +55,66 @@ async fn same_tool_calls_persist_artifacts_under_distinct_call_ids() {
"second result is deliberately oversized"
);
}

/// `raw: true` asks `web_fetch` for the body as sent, which switches off the
/// HTML→Markdown conversion — so the payload is unconverted markup, and handing
/// it to the summarizer buys an uncached model call to paraphrase minified JS.
/// One observed fetch cost 44,561 prompt tokens that way. These pin which calls
/// earn the exemption, not what the ladder then does with them.
#[test]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Test the after_tool raw-fetch skip branch

These tests only call is_raw_fetch directly; they never invoke before_tool to record the call ID and after_tool to consume it and skip the summarizer. A regression in call-ID propagation, middleware ordering, or the stateful bookkeeping could therefore re-enable summarization while all of these tests remain green. Add a middleware-level test with a raw web_fetch result that asserts the summarizer is not invoked and the result still follows the cap/artifact path.

[RULE] missing-regression-test ·

fn only_a_raw_web_fetch_is_exempt_from_the_payload_summarizer() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Test the after_tool raw-fetch skip branch

This only tests the pure is_raw_fetch predicate. It does not call before_tool and after_tool with a raw web_fetch result, so regressions in call-ID bookkeeping could cause the summarizer exemption to be lost while this test remains green. Add a middleware-level test that uses the real call identity and verifies the summarizer is not invoked while the result still follows the cap or artifact path.

[RULE] missing-regression-test ·

use serde_json::json;

assert!(is_raw_fetch(
"web_fetch",
&json!({"url": "https://x", "raw": true})
));

// A converted fetch is the normal path and stays summarizer-eligible: its
// Markdown is prose the summarizer compresses well.
for args in [
json!({"url": "https://x"}),
json!({"url": "https://x", "raw": false}),
json!({"url": "https://x", "raw": null}),
// `raw` is a bool on the wire; a string is not a request for raw bytes.
json!({"url": "https://x", "raw": "true"}),
] {
assert!(
!is_raw_fetch("web_fetch", &args),
"{args} is a converted fetch"
);
}

// The exemption is about `web_fetch`'s conversion, so a `raw` argument on
// any other tool means nothing here.
for tool in ["file_read", "shell", "http_request"] {
assert!(
!is_raw_fetch(tool, &json!({"raw": true})),
"{tool} has no HTML conversion to switch off"
);
}
}

/// `use_skill` forwards the wrapped tool's result verbatim, so a raw fetch
/// reached through it is still a raw fetch — the same wrapper-following
/// `artifact_read_target` does.
#[test]
fn a_raw_fetch_wrapped_in_use_skill_is_still_a_raw_fetch() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Drive raw-fetch exemption through the agent harness

The new behavior changes the model-facing result of a real web_fetch(raw: true) turn, but the added coverage only invokes the classifier directly. It does not verify that production tool execution records the raw-fetch call under the expected ID and reaches after_tool with the exemption intact. Add a harness-level scenario using the repository's mocked backend that asserts no summarizer request is made and oversized raw content remains recoverable through artifact paging.

[RULE] missing-integration-test ·

use serde_json::json;

assert!(is_raw_fetch(
"use_skill",
&json!({"skill": "web", "tool": "web_fetch", "args": {"url": "https://x", "raw": true}})
));
assert!(!is_raw_fetch(
"use_skill",
&json!({"skill": "web", "tool": "web_fetch", "args": {"url": "https://x"}})
));
// A wrapper naming some other tool, and a malformed one, are not raw
// fetches — neither may silently inherit the exemption.
assert!(!is_raw_fetch(
"use_skill",
&json!({"skill": "files", "tool": "file_read", "args": {"raw": true}})
));
assert!(!is_raw_fetch("use_skill", &json!({"raw": true})));
}
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ impl TurnContextMiddleware {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools,
raw_fetches: Default::default(),
}));
}
// Push the handoff LAST (so its `after_tool` runs FIRST): it observes the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ fn summarizer_mw(ps: Arc<dyn PayloadSummarizer>) -> ToolOutputMiddleware {
focus_by_call: Default::default(),
// `web_fetch` declares `summary_focus` in production.
summary_focus_tools: ["web_fetch".to_string()].into(),
raw_fetches: Default::default(),
}
}

Expand Down Expand Up @@ -220,6 +221,7 @@ fn compaction_enabled_mw() -> ToolOutputMiddleware {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
}
}

Expand Down Expand Up @@ -270,6 +272,7 @@ fn truncation_probe_mw() -> ToolOutputMiddleware {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn artifact_mw(
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ async fn tool_output_truncates_over_the_flat_budget() {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
};
let mut result = tool_result("echo", &"x".repeat(5_000));
mw.after_tool(
Expand Down Expand Up @@ -502,6 +503,7 @@ async fn tool_output_leaves_small_results_untouched() {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
};
let mut result = tool_result("echo", "tiny");
mw.after_tool(
Expand Down Expand Up @@ -543,6 +545,7 @@ fn tool_char_cap_reads_the_tools_own_declared_cap() {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
};
// Tool declares its own char cap → surfaced for the per-tool truncation.
assert_eq!(mw.tool_char_cap("big"), Some(10));
Expand Down Expand Up @@ -647,6 +650,36 @@ async fn a_tool_that_caps_itself_is_summarized_when_the_caller_gives_a_focus() {
assert!(result_text(&result).contains("focused"));
}

#[tokio::test]
Comment thread
senamakel marked this conversation as resolved.
async fn a_raw_web_fetch_never_prepares_a_payload_summary() {
let stub = StubSummarizer::replying(Ok("must remain unused".into()));
let mw = summarizer_mw(stub.clone());
let mut call = TaToolCall::new(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Exercise the self-capped tool spill path end to end

The new scenario covers only web_fetch with raw: true and uses a middleware with no artifact store and a 10 MB budget. It does not cover the separate production behavior for a tool declaring max_result_size_chars/max_result_bytes, where summarization must be skipped and the oversized result must take the spill-to-artifact path. That path remains vulnerable to regressions without an agent-run test using a real self-capped tool and asserting the persisted artifact or paging reference.

[RULE] insufficient-test-coverage ·

"raw-fetch",
"web_fetch",
json!({"url": "https://example.test", "raw": true}),
);
let mut ctx = ctx();
mw.before_tool(&mut ctx, &(), &mut call)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Drive raw-fetch routing through the production tool path

This test invokes before_tool and after_tool directly with handcrafted identities and results. It does not verify that the real web_fetch(raw: true) execution path records the call under the same ID and reaches the exemption, so a regression in tool-loop metadata propagation could re-enable payload summarization while this test remains green. Add an end-to-end scenario using the repository's mocked backend that executes the actual tool path and asserts no summarizer request is issued.


Additional critique observation

priority medium confident

Drive raw-fetch exemption through the production tool path

[RULE] missing-integration-test

This test calls ToolOutputMiddleware::before_tool and after_tool directly with a handcrafted TaToolCall; it does not verify that the real web_fetch(raw: true) execution records the call under the same identity and reaches the exemption. A regression in tool-loop metadata propagation could therefore leave this test green while production invokes the payload summarizer. Add a harness-level scenario using the repository's mocked backend and assert that no summarizer request is issued.

[RULE] missing-integration-test ·

.await
.expect("raw fetch is recorded before execution");

let mut result = tool_result("web_fetch", &"<html>markup</html>".repeat(300));
let (outcome, requests) = with_module(mw.after_tool(
&mut ctx,
&(),
&invocation("raw-fetch", "web_fetch"),
&mut result,
))
.await;

outcome.expect("raw fetch result is processed");
assert!(
!stub.was_prepared() && requests.is_empty(),
"raw fetches must bypass the payload summarizer and TinyJuice"
);
}

#[tokio::test]
async fn tool_output_honors_a_tools_own_cap() {
let mut tool_policies = HashMap::new();
Expand Down Expand Up @@ -675,6 +708,7 @@ async fn tool_output_honors_a_tools_own_cap() {
artifact_reads: Default::default(),
focus_by_call: Default::default(),
summary_focus_tools: Default::default(),
raw_fetches: Default::default(),
};
let mut result = tool_result("capped", &"y".repeat(500));
mw.after_tool(
Expand Down
33 changes: 33 additions & 0 deletions crates/openhuman-core/src/config/ops/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ pub struct AgentSettingsPatch {
/// Tool/action wall-clock timeout in seconds. Validated to
/// `tool_timeout::MIN_TIMEOUT_SECS..=tool_timeout::MAX_TIMEOUT_SECS`.
pub agent_timeout_secs: Option<u64>,
/// Agent the web-chat path routes a turn to (`[agent] chat_agent_id`).
/// `Some("")`/whitespace clears the override and reverts to the
/// orchestrator; `Some(id)` sets it; `None` leaves it unchanged.
///
/// Settable over RPC and not only in the TOML because the file on disk is
/// not reliably the file the core reads: once a user dir is active its
/// per-user `config.toml` takes precedence, so a value pre-written to the
/// root (or to a guessed user dir) is silently ignored. Going through the
/// running core writes wherever `Config::save` actually points.
pub chat_agent_id: Option<String>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Drive chat-agent routing through a real web-chat turn

The new setting is covered only by configuration-level tests. None of the changed tests drives a web-chat turn through the actual session checkout/routing path with a valid agent ID, then verifies that the selected definition is used. A regression in the consumer, persistence reload, or route resolution would leave these tests green while the setting appears to save successfully.

[RULE] missing-integration-test ·

}

/// Partial update for the agent's editable filesystem roots.
Expand Down Expand Up @@ -217,9 +227,32 @@ pub async fn apply_agent_settings(
"agent_timeout_secs must be between {MIN_TIMEOUT_SECS} and {MAX_TIMEOUT_SECS} seconds (got {timeout_secs})"
));
}
}

if let Some(chat_agent_id) = update.chat_agent_id.as_deref() {
let trimmed = chat_agent_id.trim();
if !trimmed.is_empty()
Comment thread
senamakel marked this conversation as resolved.
Comment thread
senamakel marked this conversation as resolved.
&& !crate::agent::OpenHumanSessionHost::is_runnable_agent_id(config, trimmed)
{
return Err(format!(
"chat_agent_id '{trimmed}' is not a runnable agent definition"
));
}
Comment thread
senamakel marked this conversation as resolved.
}

if let Some(timeout_secs) = update.agent_timeout_secs {
config.agent.agent_timeout_secs = timeout_secs;
}

if let Some(chat_agent_id) = update.chat_agent_id {
let trimmed = chat_agent_id.trim();
config.agent.chat_agent_id = (!trimmed.is_empty()).then(|| trimmed.to_string());
log::debug!(
"[config][agent] chat_agent_id -> {:?}",
config.agent.chat_agent_id
);
}

config.save().await.map_err(|e| e.to_string())?;

let effective = crate::tools::timeout::set_tool_timeout_secs(config.agent.agent_timeout_secs);
Expand Down
Loading
Loading