Synced from monorepo

Synced from monorepo

Changes:
- Temporarily disable session share link creation in the TUI
- Do not approve plan on empty Enter from the revise prompt
- Expose chat product Skills via ACP available_commands_update
- Return immediately from a blocking wait on an already-completed ACP task
- Split headless pager module for clearer structure
- Stop git worktree prune from removing user registrations on resume
- Use compaction sampler tokenizer for item token counts
- Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE
- Cancel all session subagents when the user stops
- Let the session persistence actor exit when its session ends
- Make fullscreen terminal resize much cheaper on long sessions
- Report honestly from kill_task when an ACP task does not exist
- Hide /usage for external-auth deployments
- Forward the history-load trailer’s computer_reason to the client
- Remove ineffective no-op tool reminder
- Declare slash-command screen-mode support in one place
- Keep settings enum picker on the committed value until Enter
- Reap a PTY’s full process tree
- Stream tool calls from headless mode over ACP
- Bridge gateway task lifecycle to ACP for chat session background tasks
- Don’t warn about truncated history on a suppressed replay
- Fit full-replace summarizer input and recover on context-length errors
- Stop dropping agents over an unrecognized frontmatter color
- Add /undo as a slash alias for /rewind
- Harden sleep/wake token-refresh paths against forced re-login
- Add session/list ACP method
- Give each sampling backend its own conversion module
- Treat an unenrolled child process as a lint error
- Suppress the cancelled marker on send-now wake turns
- Stop tearing down Roslyn on every edit, and read C# diagnostics

Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
This commit is contained in:
grokkybara[bot] 2026-07-30 19:07:40 +00:00
commit dd04f397b1
367 changed files with 29489 additions and 10051 deletions

View file

@ -292,6 +292,9 @@ pub fn stream_chat_completions<'a>(
message_chunks_emitted: message_chunk_count,
doom_loop_signals: Vec::new(),
stop_message: None,
message_id: None,
raw_stop_reason: None,
stop_sequence: None,
};
yield SamplingEvent::Completed {

View file

@ -171,6 +171,9 @@ mod tests {
message_chunks_emitted: 1,
doom_loop_signals: Vec::new(),
stop_message: None,
message_id: None,
raw_stop_reason: None,
stop_sequence: None,
}),
metrics: InferenceLatencyStats::default(),
};

View file

@ -19,6 +19,22 @@ use crate::events::{SamplingChannel, SamplingErrorInfo, SamplingEvent};
use crate::metrics::InferenceLatencyStats;
use crate::types::RequestId;
/// The verbatim wire string for a Messages API stop reason, before it collapses
/// into the internal [`StopReason`]. Uses the enum's serde `snake_case`
/// renaming so it cannot drift from the wire contract.
fn messages_stop_reason_wire(sr: &messages::StopReason) -> String {
match serde_json::to_value(sr) {
Ok(serde_json::Value::String(s)) => s,
other => {
debug_assert!(
false,
"StopReason must serialize to a string, got {other:?}"
);
"end_turn".to_string()
}
}
}
/// Returns whether a Messages API event reflects real model progress
/// rather than a liveness-only heartbeat (Ping).
pub(crate) fn messages_event_has_meaningful_content(event: &MessageStreamEvent) -> bool {
@ -99,6 +115,12 @@ pub fn stream_messages<'a>(
let mut final_output_tokens: u32 = 0;
let mut final_stop_reason: Option<StopReason> = None;
let mut final_stop_message: Option<String> = None;
let mut final_message_id: Option<String> = None;
let mut final_raw_stop_reason: Option<String> = None;
// The provider's matched stop sequence (Messages `message_delta.stop_sequence`),
// set only on a `stop_sequence`-terminated turn; carried through so the
// headless `streaming-messages-json` consumer can echo it.
let mut final_stop_sequence: Option<String> = None;
// Assistant-response accumulators (built up as ContentBlockStop
// events fire). Reasoning is collected into a synthesized
@ -152,10 +174,26 @@ pub fn stream_messages<'a>(
match event {
MessageStreamEvent::MessageStart { message } => {
final_message_id = Some(message.id.clone());
final_model = Some(message.model.clone());
final_input_tokens = message.usage.input_tokens;
final_cache_read_input_tokens = message.usage.cache_read_input_tokens;
final_cache_creation_input_tokens = message.usage.cache_creation_input_tokens;
// Surface the real id/model/input-usage in order, before any
// content, so partial-mode framing emits them on the real
// `message_start` instead of a synthesized placeholder.
yield SamplingEvent::ResponseStarted {
request_id: request_id.clone(),
message_id: message.id,
model: message.model,
input_tokens: u64::from(message.usage.input_tokens),
cache_read_input_tokens: u64::from(
message.usage.cache_read_input_tokens,
),
cache_creation_input_tokens: u64::from(
message.usage.cache_creation_input_tokens,
),
};
}
MessageStreamEvent::ContentBlockStart {
@ -237,7 +275,19 @@ pub fn stream_messages<'a>(
arguments_delta: None,
};
}
_ => {} // Image / ToolResult are not expected in assistant streams.
// Encrypted reasoning the model chose to redact. Deliberately
// parse-only: the `RedactedThinking` wire variant exists so a
// stream that includes one deserializes instead of failing the
// whole event parse and discarding an already-streamed
// response, but its opaque `data` blob is not surfaced as a
// `SamplingEvent` — forwarding it to the headless reducer's
// `redacted_thinking` block would need a new event threaded
// through the deferred sampler→shell→reducer hop and handled by
// every `SamplingEvent` consumer (TUI included), so it is not
// wired. No consumer claims redacted_thinking support.
ContentBlock::RedactedThinking { .. } => {}
// Image / ToolResult are not expected in assistant streams.
_ => {}
},
MessageStreamEvent::ContentBlockDelta { index, delta } => {
@ -312,6 +362,15 @@ pub fn stream_messages<'a>(
}
}
BlockType::Thinking => {
// Surface the encrypted signature in order (at the
// thinking block's stop) so partial-mode framing can
// emit `signature_delta` before its `content_block_stop`.
if !state.signature.is_empty() {
yield SamplingEvent::ReasoningCompleted {
request_id: request_id.clone(),
signature: state.signature.clone(),
};
}
if !state.thinking_acc.is_empty() || !state.signature.is_empty() {
// Anthropic Messages API `Thinking` blocks uniquely
// carry an encrypted `signature` distinct
@ -359,6 +418,14 @@ pub fn stream_messages<'a>(
if let Some(details) = delta.stop_details {
final_stop_message = details.explanation;
}
// Keep the exact wire string so consumers can echo it.
final_raw_stop_reason =
delta.stop_reason.as_ref().map(messages_stop_reason_wire);
// The matched stop sequence rides the same terminal delta
// (present only on a `stop_sequence` stop); carry it verbatim.
if delta.stop_sequence.is_some() {
final_stop_sequence = delta.stop_sequence.clone();
}
final_stop_reason = delta.stop_reason.map(|sr| match sr {
messages::StopReason::EndTurn => StopReason::Stop,
messages::StopReason::MaxTokens => StopReason::Length,
@ -470,6 +537,7 @@ pub fn stream_messages<'a>(
total_tokens: total_prompt_tokens.saturating_add(final_output_tokens),
reasoning_tokens: 0,
cached_prompt_tokens: final_cache_read_input_tokens,
cache_creation_prompt_tokens: final_cache_creation_input_tokens,
})
} else {
None
@ -511,6 +579,9 @@ pub fn stream_messages<'a>(
message_chunks_emitted: message_chunk_count,
doom_loop_signals: Vec::new(),
stop_message: final_stop_message,
message_id: final_message_id,
raw_stop_reason: final_raw_stop_reason,
stop_sequence: final_stop_sequence,
};
yield SamplingEvent::Completed {

View file

@ -58,6 +58,7 @@ fn message_delta_with_stop(stop: messages::StopReason) -> MessageStreamEvent {
MessageStreamEvent::MessageDelta {
delta: MessageDeltaBody {
stop_reason: Some(stop),
stop_sequence: None,
stop_details: None,
},
usage: MessageDeltaUsage {
@ -75,6 +76,7 @@ fn message_delta_refusal_with_explanation(explanation: &str) -> MessageStreamEve
MessageStreamEvent::MessageDelta {
delta: MessageDeltaBody {
stop_reason: Some(messages::StopReason::Refusal),
stop_sequence: None,
stop_details: Some(messages::StopDetails {
r#type: Some("refusal".to_string()),
category: Some("frontier_llm".to_string()),
@ -141,6 +143,10 @@ async fn text_block_assembles_into_completed_response() {
assert_eq!(a.content.as_ref(), "Hello, world!");
assert_eq!(a.model_id.as_deref(), Some("messages-compatible-model"));
assert_eq!(response.stop_reason, Some(StopReason::Stop));
// Provider message id and the verbatim wire stop reason survive
// onto the response (collapsed `stop_reason` loses the string).
assert_eq!(response.message_id.as_deref(), Some("msg_1"));
assert_eq!(response.raw_stop_reason.as_deref(), Some("end_turn"));
let u = response.usage.as_ref().expect("usage extracted");
assert_eq!(u.prompt_tokens, 10);
assert_eq!(u.completion_tokens, 5);
@ -208,6 +214,61 @@ async fn thinking_block_emits_reasoning_channel_and_preserved_in_response() {
}
}
/// `thinking(sig1) → text → thinking(sig2)` must surface each thinking block's
/// OWN signature, in order, on its own `ReasoningCompleted` (emitted at that
/// block's stop) — so the per-index signature reaches the headless reducer and
/// each block keeps its own signature rather than collapsing to one.
#[tokio::test]
async fn multiple_thinking_blocks_emit_per_block_signatures_in_order() {
let thinking_block = |index: u32, text: &str, sig: &str| {
vec![
Ok(MessageStreamEvent::ContentBlockStart {
index,
content_block: ContentBlock::Thinking {
thinking: String::new(),
signature: String::new(),
},
}),
Ok(MessageStreamEvent::ContentBlockDelta {
index,
delta: StreamDelta::ThinkingDelta {
thinking: text.into(),
},
}),
Ok(MessageStreamEvent::ContentBlockDelta {
index,
delta: StreamDelta::SignatureDelta {
signature: sig.into(),
},
}),
Ok(block_stop(index)),
]
};
let mut events: Vec<Result<MessageStreamEvent, SamplingError>> = vec![Ok(message_start())];
events.extend(thinking_block(0, "first", "sig-1"));
events.push(Ok(text_block_start(1)));
events.push(Ok(text_delta(1, "interlude")));
events.push(Ok(block_stop(1)));
events.extend(thinking_block(2, "second", "sig-2"));
events.push(Ok(MessageStreamEvent::MessageStop));
let raw = stream::iter(events).boxed();
let evs = collect(stream_messages(raw, None, rid(), Duration::from_secs(60))).await;
let sigs: Vec<&str> = evs
.iter()
.filter_map(|e| match e {
SamplingEvent::ReasoningCompleted { signature, .. } => Some(signature.as_str()),
_ => None,
})
.collect();
assert_eq!(
sigs,
vec!["sig-1", "sig-2"],
"each thinking block emits its own signature in order"
);
}
#[tokio::test]
async fn tool_use_block_assembles_into_tool_call() {
let tool_start = MessageStreamEvent::ContentBlockStart {
@ -574,6 +635,7 @@ fn message_delta_with_cache(
MessageStreamEvent::MessageDelta {
delta: MessageDeltaBody {
stop_reason: Some(messages::StopReason::EndTurn),
stop_sequence: None,
stop_details: None,
},
usage: MessageDeltaUsage {
@ -621,6 +683,7 @@ async fn prompt_tokens_sums_all_three_anthropic_buckets() {
assert_eq!(usage.prompt_tokens, 100 + 5000 + 200);
assert_eq!(usage.cached_prompt_tokens, 5000);
assert_eq!(usage.cache_creation_prompt_tokens, 200);
assert_eq!(usage.completion_tokens, 7);
assert_eq!(usage.total_tokens, 100 + 5000 + 200 + 7);
}
@ -638,6 +701,7 @@ async fn message_delta_cache_fields_override_message_start() {
assert_eq!(usage.prompt_tokens, 10 + 900 + 50);
assert_eq!(usage.cached_prompt_tokens, 900);
assert_eq!(usage.cache_creation_prompt_tokens, 50);
assert_eq!(usage.completion_tokens, 4);
}

View file

@ -381,6 +381,25 @@ pub(crate) fn stream_responses_tracked<'a>(
ResponseStreamEvent::ResponseWebSearchCallCompleted(_)
| ResponseStreamEvent::ResponseWebSearchCallSearching(_) => {}
// Code interpreter (server-side, like web/x search). Surface it
// the same way x_search is: a generic backend tool call that the
// shell renders as a client `tool_use` + `user` `tool_result`
// split (grok has no HostedTool::CodeInterpreter, so these events
// are latent under the current hosted-tool set). The started
// event fires on InProgress; the full payload (code + outputs)
// rides ResponseOutputItemDone(CodeInterpreterCall) below.
ResponseStreamEvent::ResponseCodeInterpreterCallInProgress(ev) => {
yield SamplingEvent::BackendToolCallStarted {
request_id: request_id.clone(),
call_id: ev.item_id.clone(),
name: "code_interpreter".to_string(),
};
}
// Interpreting/Completed carry no payload — the result arrives
// via ResponseOutputItemDone(CodeInterpreterCall) below.
ResponseStreamEvent::ResponseCodeInterpreterCallInterpreting(_)
| ResponseStreamEvent::ResponseCodeInterpreterCallCompleted(_) => {}
// OutputItemDone carries the full result for backend tools.
// For WebSearchCall this includes the query and source URLs.
// For CustomToolCall this includes x_search results.
@ -409,6 +428,19 @@ pub(crate) fn stream_responses_tracked<'a>(
result,
};
}
// Code interpreter: the full call (code + outputs) rides
// the done item. Surfaced under the shared "code_interpreter"
// name (matching the Started event); the shell renders it via
// the client `tool_use` + `user` `tool_result` split.
rs::OutputItem::CodeInterpreterCall(ci) => {
let result = serde_json::to_value(ci).ok();
yield SamplingEvent::BackendToolCallCompleted {
request_id: request_id.clone(),
call_id: ci.id.clone(),
name: "code_interpreter".to_string(),
result,
};
}
_ => {}
}
}
@ -485,6 +517,7 @@ pub(crate) fn stream_responses_tracked<'a>(
total_tokens: u.total_tokens,
reasoning_tokens: u.output_tokens_details.reasoning_tokens,
cached_prompt_tokens: u.input_tokens_details.cached_tokens,
cache_creation_prompt_tokens: 0,
});
let cost_usd_ticks = response
@ -543,6 +576,9 @@ pub(crate) fn stream_responses_tracked<'a>(
message_chunks_emitted: message_chunk_count,
doom_loop_signals,
stop_message: None, // not reported on the Responses API
message_id: None, // no provider message id on the Responses API
raw_stop_reason: None,
stop_sequence: None,
};
yield SamplingEvent::Completed {
@ -874,6 +910,67 @@ mod tests {
assert!(output_observed.load(Ordering::Relaxed));
}
/// A server-side code-interpreter run surfaces as a generic backend tool
/// call (started on InProgress, completed on OutputItemDone) named
/// "code_interpreter" — the same shape as x_search — so it is no longer
/// silently dropped from the event stream.
#[tokio::test]
async fn code_interpreter_forwards_backend_tool_call() {
let in_progress = rs::ResponseStreamEvent::ResponseCodeInterpreterCallInProgress(
rs_types::ResponseCodeInterpreterCallInProgressEvent {
sequence_number: 0,
output_index: 0,
item_id: "ci-1".into(),
},
);
let done = rs::ResponseStreamEvent::ResponseOutputItemDone(
rs_types::ResponseOutputItemDoneEvent {
sequence_number: 1,
output_index: 0,
item: rs_types::OutputItem::CodeInterpreterCall(
rs_types::CodeInterpreterToolCall {
code: Some("print(1)".into()),
container_id: "cont-1".into(),
id: "ci-1".into(),
outputs: None,
status: rs_types::CodeInterpreterToolCallStatus::Completed,
},
),
},
);
let raw = stream::iter(vec![Ok(in_progress), Ok(done), Ok(completed_event())]).boxed();
let events = collect(stream_responses(
raw,
None,
rid(),
Duration::from_secs(60),
None,
))
.await;
assert!(
events.iter().any(|e| matches!(
e,
SamplingEvent::BackendToolCallStarted { call_id, name, .. }
if call_id == "ci-1" && name == "code_interpreter"
)),
"expected a code_interpreter BackendToolCallStarted, got {events:?}"
);
let completed = events.iter().find_map(|e| match e {
SamplingEvent::BackendToolCallCompleted {
call_id,
name,
result,
..
} if name == "code_interpreter" => Some((call_id.clone(), result.clone())),
_ => None,
});
let (call_id, result) = completed.expect("a code_interpreter BackendToolCallCompleted");
assert_eq!(call_id, "ci-1");
let result = result.expect("serialized code-interpreter payload");
assert_eq!(result["code"], "print(1)");
}
fn function_call_added_event(
output_index: u32,
call_id: &str,