Files
buzz/crates/buzz-agent/tests/anthropic_continuation.rs
Wes 0cc63fe3e6 fix(agent): preserve Databricks Opus UC reasoning and tool continuation (#7840)
## Summary
Fix `reasoning_effort: Extra inputs are not permitted` for Databricks v2
model `data_workflow_tools.goose.goose-claude-opus-5-5` without dropping
explicit High.

- Add one provider-qualified exact capability record: Anthropic
Messages, adaptive thinking, `output_config.effort`, medium upstream
default. Mirror exact-record precedence in Rust/TS; keep uncurated UC
services neutral and existing GPT routing unchanged.
- Preserve ordered native content, including signed/empty thinking and
redacted blocks, across tool results. Do not cache-stamp thinking blocks
or leak native state into Chat reasoning details. Discard truncated
native state; reject more than 64 native calls before any tool executes
instead of altering signed content.
- Add production-dispatch/ACP regressions and six shared capability
vectors. No relay/ACP protocol, schema, settings, inheritance, retries,
or other harness changes.

## Validation
At final head `82f43f8d07bfcc3f4af492afc0a58a02f8e549c4`, clean tree:
- Rebuilt real `buzz-agent`, drove ACP with a harmless MCP fixture
against the actual service. Two POSTs to
`/ai-gateway/anthropic/v1/messages`, both HTTP 200, exact FQN, High via
`output_config`, adaptive/summarized thinking, no `reasoning_effort`.
First response contained signed thinking + tool use; second request
preserved signature/content order and paired result; final ACP stop
`end_turn`. No credentials, signature contents, or conversation data
included here.
- Mandatory pre-commit/pre-push checks passed: formatting/lint,
file-size gate, Rust test lanes, desktop typechecking and full JS tests,
Tauri clippy/tests.
- Full `cargo test -p buzz-agent` passed on the pre-commit tree (only
hook formatting followed), including the new native truncation/65-call
ACP regressions, ordered interleaved signed/redacted two-tool capture,
nearby-ID negatives, and existing provider regressions.
- Independent API/replay, regression-boundary, and Rust/TS capability
reviews cleared.

Full `just ci`, packaging and manual desktop UI testing were not run
locally; repository CI remains the merge gate. Cross-harness internals
and other UC service contracts are outside scope.

## References
- [Databricks model
services](https://docs.databricks.com/aws/en/ai-gateway/query-model-services)
- [Anthropic thinking
preservation](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model)
- [Anthropic
effort](https://platform.claude.com/docs/en/build-with-claude/effort)
- Originating Buzz channel: `fccdea96-0f31-4343-ba6a-324214dc8d73`
- Originating thread:
buzz://message?channel=fccdea96-0f31-4343-ba6a-324214dc8d73&id=33f242923a317e269c1ff7b960cacd6ec1f3371eff2f97592857804c50fb100c

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-23 09:44:13 -06:00

137 lines
4.4 KiB
Rust

//! Native replay must remain consistent with the real ACP loop's recovery limits.
mod common;
use common::{spawn_capturing_llm, Harness};
use serde_json::json;
async fn session(h: &mut Harness) -> String {
let id = h
.send(
"initialize",
json!({"protocolVersion":1,"clientCapabilities":{}}),
)
.await;
h.recv_until(|v| v["id"] == id).await;
let id = h
.send(
"session/new",
json!({"cwd":"/tmp","mcpServers":[{
"name":"probe", "command":env!("CARGO_BIN_EXE_fake-mcp"), "args":[], "env":[]
}]}),
)
.await;
h.recv_until(|v| v["id"] == id).await["result"]["sessionId"]
.as_str()
.unwrap()
.to_owned()
}
async fn native_harness(url: &str) -> Harness {
// Native Anthropic avoids Databricks discovery consuming response fixtures;
// the exact UC dispatcher and effort contract are tested in llm_fqn_tests.
Harness::spawn_with_env(
url,
&[
("BUZZ_AGENT_PROVIDER", "anthropic"),
("ANTHROPIC_API_KEY", "test"),
("ANTHROPIC_BASE_URL", url),
("BUZZ_AGENT_MODEL", "claude-opus-5-5"),
("BUZZ_AGENT_THINKING_EFFORT", "high"),
],
)
.await
}
#[tokio::test]
async fn oversized_native_turn_fails_before_tools_execute() {
let mut content = vec![json!({"type":"thinking", "thinking":"", "signature":"signed"})];
content.extend((0..65).map(|n| {
json!({"type":"tool_use", "id":format!("call-{n}"),
"name":"probe__tool_0", "input":{}})
}));
let llm = spawn_capturing_llm(vec![json!({"stop_reason":"tool_use", "content":content})]).await;
let mut h = native_harness(&llm.url).await;
let sid = session(&mut h).await;
let id = h
.send(
"session/prompt",
json!({"sessionId":sid,"prompt":[{"type":"text","text":"go"}]}),
)
.await;
let mut tool_started = false;
let reply = h
.recv_until_approving(|v| {
tool_started |= v["params"]["update"]["sessionUpdate"] == "tool_call";
v["id"] == id
})
.await;
assert!(
reply["error"]
.to_string()
.contains("cannot truncate native content"),
"{reply}"
);
assert!(
!tool_started,
"oversized native turn must execute no partial subset"
);
assert_eq!(llm.captured.lock().await.len(), 1);
h.shutdown().await;
}
#[tokio::test]
async fn native_max_tokens_recovers_without_signed_state_or_partial_tools() {
let llm = spawn_capturing_llm(vec![
json!({"stop_reason":"max_tokens", "content":[
{"type":"thinking", "thinking":"", "signature":"incomplete-signature"},
{"type":"text", "text":"partial-text"},
{"type":"tool_use", "id":"partial-call", "name":"probe__tool_0", "input":"{broken"},
{"type":"redacted_thinking", "data":"incomplete-redaction"}
]}),
json!({"stop_reason":"end_turn", "content":[{"type":"text", "text":"done"}]}),
])
.await;
let mut h = native_harness(&llm.url).await;
let sid = session(&mut h).await;
let id = h
.send(
"session/prompt",
json!({"sessionId":sid,"prompt":[{"type":"text","text":"go"}]}),
)
.await;
let mut tool_started = false;
let reply = h
.recv_until_approving(|v| {
tool_started |= v["params"]["update"]["sessionUpdate"] == "tool_call";
v["id"] == id
})
.await;
assert_eq!(reply["result"]["stopReason"], "end_turn", "{reply}");
assert!(!tool_started);
let requests = llm.captured.lock().await;
assert_eq!(requests.len(), 2);
let retry = &requests[1]["messages"];
let assistant = retry
.as_array()
.unwrap()
.iter()
.find(|m| m["role"] == "assistant")
.unwrap();
let blocks = assistant["content"].as_array().unwrap();
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], "partial-text");
let serialized = retry.to_string();
for forbidden in [
"incomplete-signature",
"incomplete-redaction",
"partial-call",
"tool_result",
] {
assert!(!serialized.contains(forbidden), "{retry}");
}
assert!(serialized.contains("output token limit"));
drop(requests);
h.shutdown().await;
}