Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions crates/client/src/client_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,45 @@ impl ServerClientCore {
.await
}

pub(crate) async fn turn_read_native(
&mut self,
session_id: SessionId,
turn_id: TurnId,
) -> Result<devo_protocol::native::rpc_turn::TurnReadResult> {
self.request(
"turn/read",
devo_protocol::native::rpc_turn::TurnReadParams {
session_id: devo_protocol::native::ids::SessionId::from_string(
session_id.to_string(),
),
turn_id: devo_protocol::native::ids::TurnId::from_string(turn_id.to_string()),
},
)
.await
}

pub(crate) async fn turn_items_list_native(
&mut self,
session_id: SessionId,
turn_id: TurnId,
cursor: Option<String>,
limit: Option<u32>,
) -> Result<devo_protocol::native::page::Page<devo_protocol::native::item::ItemEnvelope>> {
self.request(
"session/items/list",
devo_protocol::native::rpc_session::SessionItemsListParams {
session_id: devo_protocol::native::ids::SessionId::from_string(
session_id.to_string(),
),
turn_id: Some(devo_protocol::native::ids::TurnId::from_string(
turn_id.to_string(),
)),
page: devo_protocol::native::page::PageParams { cursor, limit },
},
)
.await
}

/// Native `session/fork` (L2-DES-APP-008 Phase B).
pub(crate) async fn session_fork_native(
&mut self,
Expand Down
20 changes: 20 additions & 0 deletions crates/client/src/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,26 @@ impl StdioServerClient {
.await
}

pub async fn turn_read_native(
&mut self,
session_id: SessionId,
turn_id: TurnId,
) -> Result<devo_protocol::native::rpc_turn::TurnReadResult> {
self.core.turn_read_native(session_id, turn_id).await
}

pub async fn turn_items_list_native(
&mut self,
session_id: SessionId,
turn_id: TurnId,
cursor: Option<String>,
limit: Option<u32>,
) -> Result<devo_protocol::native::page::Page<devo_protocol::native::item::ItemEnvelope>> {
self.core
.turn_items_list_native(session_id, turn_id, cursor, limit)
.await
}

/// Native `session/compact/start`; see
/// `client_core::session_compact_start_native`.
pub async fn session_compact_start_native(
Expand Down
8 changes: 8 additions & 0 deletions crates/server/src/runtime/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,14 @@ async fn remove_pending_client_request(

fn outbound_delivery_policy(event: &ServerEvent) -> OutboundDeliveryPolicy {
match event {
// Tool-call argument deltas are low-volume and drive the client's
// running-row parameter display; losing one permanently truncates the
// accumulated JSON until the item refresh, so they ride the reliable
// lane unlike the high-volume text/output deltas below.
ServerEvent::ItemDelta {
delta_kind: ItemDeltaKind::ToolCallInputDelta,
..
} => OutboundDeliveryPolicy::Reliable,
ServerEvent::ItemDelta { .. }
| ServerEvent::TurnUsageUpdated(_)
| ServerEvent::ContextUsageUpdated(_)
Expand Down
7 changes: 5 additions & 2 deletions crates/server/src/runtime/outbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ pub(crate) const OUTBOUND_RELIABLE_RESERVED_CAPACITY: usize = 64;
pub(crate) const OUTBOUND_BACKPRESSURE_LOG_THRESHOLD: Duration = Duration::from_millis(50);
/// Max time streaming notifications wait for outbound capacity before being
/// dropped. Event streams must not park forever on a slow client: parent+child
/// turns share one connection and can fill the queue quickly.
pub(crate) const OUTBOUND_NOTIFICATION_MAX_WAIT: Duration = Duration::from_millis(200);
/// turns share one connection and can fill the queue quickly. Lifecycle
/// traffic (`item/completed`, `turn/completed`, …) rides this wait too —
/// dropping it wedges client render state — so the budget is sized for bursty
/// slow consumers (Windows console pipes) rather than round-trip latency.
pub(crate) const OUTBOUND_NOTIFICATION_MAX_WAIT: Duration = Duration::from_secs(2);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum OutboundDeliveryPolicy {
Expand Down
36 changes: 35 additions & 1 deletion crates/server/src/runtime/turn_exec/event_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,9 +525,43 @@ async fn handle_tool_use_start(
event_tool_registry: &Arc<devo_core::tools::ToolRegistry>,
) {
tool_names_by_id.insert(id.clone(), name.clone());
if let Some(pending) = pending_tool_calls.get_mut(&id) {
if let Some(mut pending) = pending_tool_calls.remove(&id) {
let input_is_empty = |value: &serde_json::Value| {
value.is_null() || matches!(value, serde_json::Value::Object(map) if map.is_empty())
};
let previously_empty_input = input_is_empty(&pending.input);
pending.input = input.clone();
pending.command = command_display_from_input(&name, &input);
// The first `item/started` for a streamed tool call carries empty
// parameters (the provider streams arguments afterwards). When the
// assembled turn delivers the complete input, re-broadcast the same
// item so live clients can render the running row's parameters —
// the input-delta channel alone is best-effort and only parses once
// the full JSON accumulates.
if previously_empty_input
&& !input_is_empty(&pending.input)
&& let (Some(item_id), Some(item_seq)) = (pending.item_id, pending.item_seq)
{
let start_item = tool_start_item_from_input(
&id,
&name,
&pending.command,
&pending.input,
pending.display_kind,
event_tool_registry.preparation_feedback(&name),
);
runtime
.emit_item_started(
session_id,
turn_id,
item_id,
Some(item_seq),
start_item.item_kind,
start_item.payload,
)
.await;
}
pending_tool_calls.insert(id, pending);
return;
}
if let (Some(item_id), Some(item_seq)) = (reasoning_item_id.take(), reasoning_item_seq.take()) {
Expand Down
Loading
Loading