1#![deny(missing_docs)]
4#![forbid(unsafe_code)]
5
6use std::{future::Future, pin::Pin, time::Duration};
7
8use anyhow::{Context, ensure};
9use kcode_codex_runtime_v2::{
10 AgentEvent, AgentRequest, DynamicTool, DynamicToolCall, ReasoningEffort, TokenUsage, ToolResult,
11};
12use kcode_intelligence_router::{AgentProvider, Intelligence, ResolvedAgentModel, UsageReceipt};
13use serde_json::{Value, json};
14use sha2::{Digest, Sha256};
15use uuid::Uuid;
16
17const DEFAULT_ROUND_LIMIT: u64 = 100;
18const PROTOCOL_TOKEN_RESERVE: u64 = 4_096;
19const INLINE_TOOL_RESULT_CHARACTERS: usize = 1_000;
20const SUPERSEDED_TOOL_OUTPUT: &str = "[Tool output was displayed here, but has since been updated and now appears elsewhere in the context]";
21const REMOVED_TOOL_OUTPUT: &str = "[Tool output was displayed here, but is no longer current]";
22
23pub type HostFuture<'a, T> = Pin<Box<dyn Future<Output = anyhow::Result<T>> + Send + 'a>>;
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct ToolCall {
29 pub name: String,
31 pub arguments: Value,
33}
34
35#[derive(Clone, Debug, PartialEq)]
37pub enum AuditEvent {
38 Started {
40 parent_operation_id: Uuid,
42 model: String,
44 provider_model: String,
46 provider: AgentProvider,
48 context_window_tokens: u64,
50 max_input_tokens: u64,
52 context: Vec<String>,
54 task: String,
56 host: Value,
58 },
59 InferenceSubmitted {
61 parent_operation_id: Uuid,
63 round: u64,
65 manifest_hash: String,
67 estimated_input_tokens: u64,
69 },
70 ToolCall {
72 parent_operation_id: Uuid,
74 name: String,
76 arguments: Value,
78 },
79 ToolResult {
81 parent_operation_id: Uuid,
83 name: String,
85 ok: bool,
87 projection_accepted: bool,
89 result: String,
91 },
92 ProviderReceipt {
94 parent_operation_id: Uuid,
96 round: u64,
98 manifest_hash: String,
100 usage: Option<kcode_codex_runtime_v2::TokenUsage>,
102 receipt: Box<UsageReceipt>,
104 },
105 Completed {
107 parent_operation_id: Uuid,
109 model: String,
111 response: String,
113 },
114}
115
116#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct StateUpdate {
119 pub key: String,
121 pub text: Option<String>,
123}
124
125#[derive(Clone, Debug, PartialEq)]
127pub struct ToolOutcome {
128 pub text: String,
130 pub ok: bool,
132 pub state_updates: Vec<StateUpdate>,
134 pub displayed_state_keys: Vec<String>,
140 pub capture: Option<Value>,
142}
143
144impl ToolOutcome {
145 pub fn success(text: impl Into<String>) -> Self {
147 Self {
148 text: text.into(),
149 ok: true,
150 state_updates: Vec::new(),
151 displayed_state_keys: Vec::new(),
152 capture: None,
153 }
154 }
155
156 pub fn failure(text: impl Into<String>) -> Self {
158 Self {
159 text: text.into(),
160 ok: false,
161 state_updates: Vec::new(),
162 displayed_state_keys: Vec::new(),
163 capture: None,
164 }
165 }
166}
167
168#[derive(Clone)]
170pub struct ContextBudget {
171 projection: Projection,
172 max_input_tokens: u64,
173}
174
175impl ContextBudget {
176 pub fn estimated_tokens(&self) -> u64 {
178 self.projection.estimated_tokens()
179 }
180
181 pub fn max_input_tokens(&self) -> u64 {
183 self.max_input_tokens
184 }
185
186 pub fn fits_state(&self, key: impl Into<String>, text: impl Into<String>) -> bool {
188 let mut projection = self.projection.clone();
189 projection.apply_updates(&[StateUpdate {
190 key: key.into(),
191 text: Some(text.into()),
192 }]);
193 projection.estimated_tokens() <= self.max_input_tokens
194 }
195}
196
197pub trait Host: Send {
199 fn render_tool_call(&mut self, call: &ToolCall) -> anyhow::Result<String>;
201
202 fn execute_tool<'a>(
204 &'a mut self,
205 call: ToolCall,
206 operation_id: Uuid,
207 budget: ContextBudget,
208 ) -> HostFuture<'a, ToolOutcome>;
209
210 fn complete_capture<'a>(
212 &'a mut self,
213 capture: Value,
214 contents: String,
215 budget: ContextBudget,
216 ) -> HostFuture<'a, ToolOutcome>;
217
218 fn record(&mut self, event: AuditEvent) -> anyhow::Result<()>;
220}
221
222#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct SessionRunRequest {
225 pub user_id: String,
227 pub operation_id: Uuid,
229 pub rounds_used: u64,
231 pub round_limit: u64,
233}
234
235#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct PreparedRound {
238 pub input: String,
240 pub model: String,
242 pub reasoning_effort: String,
244 pub tool_description: String,
246 pub timeout: Option<Duration>,
248}
249
250#[derive(Clone, Debug, Eq, PartialEq)]
252pub enum RoundPreparation {
253 Run(PreparedRound),
255 Complete(Option<String>),
257}
258
259#[derive(Clone, Debug, PartialEq)]
261pub enum SessionEvent {
262 InferenceSubmitted {
264 round: u64,
266 manifest_hash: String,
268 model: String,
270 },
271 ProviderInput {
273 round: u64,
275 input: String,
277 },
278 UsageUpdated {
280 round: u64,
282 usage: TokenUsage,
284 },
285 ProviderReceipt {
287 round: u64,
289 usage: Option<TokenUsage>,
291 receipt: Box<UsageReceipt>,
293 },
294}
295
296#[derive(Clone, Debug, PartialEq)]
298pub struct SessionToolOutcome {
299 pub text: String,
301 pub ok: bool,
303 pub capture: Option<Value>,
305 pub stop: bool,
307 pub finish_after_round: bool,
309 pub emitted_response: bool,
311}
312
313impl SessionToolOutcome {
314 pub fn success(text: impl Into<String>) -> Self {
316 Self {
317 text: text.into(),
318 ok: true,
319 capture: None,
320 stop: false,
321 finish_after_round: false,
322 emitted_response: false,
323 }
324 }
325
326 pub fn failure(text: impl Into<String>) -> Self {
328 Self {
329 text: text.into(),
330 ok: false,
331 capture: None,
332 stop: false,
333 finish_after_round: false,
334 emitted_response: false,
335 }
336 }
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
341pub struct RoundCompletion {
342 pub answer: String,
344 pub used_tool: bool,
346 pub finish_requested: bool,
348 pub emitted_response: bool,
350}
351
352#[derive(Clone, Debug, Eq, PartialEq)]
354pub enum SessionControl {
355 Continue,
357 Complete(Option<String>),
359}
360
361pub trait SessionHost: Send {
363 fn prepare_round<'a>(&'a mut self, round: u64) -> HostFuture<'a, RoundPreparation>;
365
366 fn record<'a>(&'a mut self, event: SessionEvent) -> HostFuture<'a, ()>;
368
369 fn execute_tool<'a>(
371 &'a mut self,
372 call: anyhow::Result<ToolCall>,
373 provider_operation_id: Uuid,
374 ) -> HostFuture<'a, SessionToolOutcome>;
375
376 fn complete_capture<'a>(
378 &'a mut self,
379 capture: Value,
380 contents: String,
381 ) -> HostFuture<'a, SessionControl>;
382
383 fn complete_round<'a>(
385 &'a mut self,
386 completion: RoundCompletion,
387 ) -> HostFuture<'a, SessionControl>;
388}
389
390#[derive(Debug)]
392pub struct SessionRoundLimitError {
393 limit: u64,
394}
395
396impl std::fmt::Display for SessionRoundLimitError {
397 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 write!(
399 formatter,
400 "agent exceeded the {}-round tool-loop safety limit",
401 self.limit
402 )
403 }
404}
405
406impl std::error::Error for SessionRoundLimitError {}
407
408pub fn is_session_round_limit(error: &anyhow::Error) -> bool {
410 error.downcast_ref::<SessionRoundLimitError>().is_some()
411}
412
413#[derive(Clone, Debug, PartialEq)]
415pub struct RunRequest {
416 pub user_id: String,
418 pub parent_operation_id: Uuid,
420 pub model: String,
422 pub reasoning_effort: String,
424 pub context: Vec<String>,
426 pub task: String,
428 pub timeout: Option<Duration>,
430 pub start_metadata: Value,
432}
433
434#[derive(Clone, Debug, Eq, PartialEq)]
436pub struct RunResult {
437 pub answer: String,
439 pub model: ResolvedAgentModel,
441}
442
443#[derive(Clone)]
445pub struct AgentRuntime {
446 intelligence: Intelligence,
447 round_limit: u64,
448}
449
450impl AgentRuntime {
451 pub fn new(intelligence: Intelligence) -> Self {
453 Self {
454 intelligence,
455 round_limit: DEFAULT_ROUND_LIMIT,
456 }
457 }
458
459 pub async fn resolve_model(&self, requested: &str) -> anyhow::Result<ResolvedAgentModel> {
461 self.intelligence
462 .resolve_agent_model(requested)
463 .await
464 .map_err(anyhow::Error::new)
465 }
466
467 pub async fn run_session<H: SessionHost>(
469 &self,
470 request: SessionRunRequest,
471 host: &mut H,
472 ) -> anyhow::Result<Option<String>> {
473 ensure!(
474 request.round_limit > 0,
475 "session round limit must be positive"
476 );
477 ensure!(
478 request.rounds_used <= request.round_limit,
479 "restored session round count exceeds its safety limit"
480 );
481 let user = self
482 .intelligence
483 .for_user(request.user_id)
484 .map_err(anyhow::Error::new)?;
485
486 for round_index in request.rounds_used..request.round_limit {
487 let round = round_index + 1;
488 let prepared = match host.prepare_round(round).await? {
489 RoundPreparation::Run(prepared) => prepared,
490 RoundPreparation::Complete(answer) => return Ok(answer),
491 };
492 let manifest_hash = hex::encode(Sha256::digest(prepared.input.as_bytes()));
493 host.record(SessionEvent::InferenceSubmitted {
494 round,
495 manifest_hash: manifest_hash.clone(),
496 model: prepared.model.clone(),
497 })
498 .await?;
499
500 let mut provider_request = AgentRequest::new(prepared.input, prepared.model);
501 provider_request.reasoning_effort = reasoning_effort(&prepared.reasoning_effort)?;
502 provider_request.previous_thread_id = None;
503 provider_request.tools = vec![ktool_definition(&prepared.tool_description)];
504 if let Some(timeout) = prepared.timeout {
505 provider_request.timeout = timeout;
506 }
507 let mut turn = match user
508 .start_agent_turn(request.operation_id, None, provider_request)
509 .await
510 {
511 Ok(turn) => turn,
512 Err(error) => {
513 if let Some(receipt) = error.receipt().cloned() {
514 host.record(SessionEvent::ProviderReceipt {
515 round,
516 usage: None,
517 receipt: Box::new(receipt),
518 })
519 .await?;
520 }
521 return Err(anyhow::Error::new(error));
522 }
523 };
524 let mut used_tool = false;
525 let mut finish_requested = false;
526 let mut emitted_response = false;
527 let mut pending_capture: Option<Value> = None;
528 let completed = loop {
529 let event = match turn.next_event().await {
530 Ok(Some(event)) => event,
531 Ok(None) => {
532 let receipt = turn.finish_unavailable()?.clone();
533 host.record(SessionEvent::ProviderReceipt {
534 round,
535 usage: None,
536 receipt: Box::new(receipt),
537 })
538 .await?;
539 anyhow::bail!("provider ended without a terminal turn event");
540 }
541 Err(error) => {
542 if let Some(receipt) = error.receipt().cloned() {
543 host.record(SessionEvent::ProviderReceipt {
544 round,
545 usage: None,
546 receipt: Box::new(receipt),
547 })
548 .await?;
549 }
550 return Err(anyhow::Error::new(error));
551 }
552 };
553 match event {
554 AgentEvent::ProviderInput(input) => {
555 host.record(SessionEvent::ProviderInput { round, input })
556 .await?;
557 }
558 AgentEvent::UsageUpdated(usage) => {
559 host.record(SessionEvent::UsageUpdated { round, usage })
560 .await?;
561 }
562 AgentEvent::ToolCall(native) => {
563 used_tool = true;
564 let call = parse_ktool_call(&native)
565 .map_err(|error| anyhow::anyhow!("Invalid Ktool call: {error}"));
566 let mut outcome = host.execute_tool(call, request.operation_id).await?;
567 finish_requested |= outcome.ok && outcome.finish_after_round;
568 emitted_response |= outcome.ok && outcome.emitted_response;
569 pending_capture = outcome.capture.take();
570 let stop = outcome.stop;
571 respond_session_or_record(
572 &mut turn,
573 host,
574 round,
575 &native.call_id,
576 if outcome.ok {
577 ToolResult::success(outcome.text)
578 } else {
579 ToolResult::failure(outcome.text)
580 },
581 )
582 .await?;
583 if stop {
584 return Ok(None);
585 }
586 }
587 AgentEvent::Completed(completed) => break completed,
588 }
589 };
590 let receipt = turn
591 .receipt()
592 .context("provider completed without a usage receipt")?
593 .clone();
594 host.record(SessionEvent::ProviderReceipt {
595 round,
596 usage: completed.usage.clone(),
597 receipt: Box::new(receipt),
598 })
599 .await?;
600
601 let control = if let Some(capture) = pending_capture {
602 host.complete_capture(capture, completed.answer).await?
603 } else {
604 host.complete_round(RoundCompletion {
605 answer: completed.answer,
606 used_tool,
607 finish_requested,
608 emitted_response,
609 })
610 .await?
611 };
612 match control {
613 SessionControl::Continue => {}
614 SessionControl::Complete(answer) => return Ok(answer),
615 }
616 }
617 Err(SessionRoundLimitError {
618 limit: request.round_limit,
619 }
620 .into())
621 }
622
623 pub async fn run<H: Host>(
625 &self,
626 request: RunRequest,
627 host: &mut H,
628 ) -> anyhow::Result<RunResult> {
629 let selected = self.resolve_model(&request.model).await?;
630 let reasoning_effort = reasoning_effort(&request.reasoning_effort)?;
631 let mut projection = Projection::new(request.context, request.task);
632 ensure_capacity(&projection, selected.max_input_tokens)?;
633 host.record(AuditEvent::Started {
634 parent_operation_id: request.parent_operation_id,
635 model: request.model.clone(),
636 provider_model: selected.provider_model.clone(),
637 provider: selected.provider,
638 context_window_tokens: selected.context_window_tokens,
639 max_input_tokens: selected.max_input_tokens,
640 context: projection.context.clone(),
641 task: projection.task.clone(),
642 host: request.start_metadata.clone(),
643 })?;
644 let user = self
645 .intelligence
646 .for_user(request.user_id)
647 .map_err(anyhow::Error::new)?;
648 let mut deferred_capture: Option<Value> = None;
649
650 for round in 0..self.round_limit {
651 let capturing = deferred_capture.is_some();
652 ensure_capacity(&projection, selected.max_input_tokens)?;
653 let input = projection.render();
654 let manifest_hash = hex::encode(Sha256::digest(input.as_bytes()));
655 host.record(AuditEvent::InferenceSubmitted {
656 parent_operation_id: request.parent_operation_id,
657 round: round + 1,
658 manifest_hash: manifest_hash.clone(),
659 estimated_input_tokens: projection.estimated_tokens(),
660 })?;
661 let mut provider_request = AgentRequest::new(input, selected.requested_model.clone());
662 provider_request.reasoning_effort = reasoning_effort;
663 provider_request.ephemeral = true;
664 provider_request.tools = if capturing {
665 Vec::new()
666 } else {
667 vec![ktool_definition(
668 "Call one available Ktool by its exact name.",
669 )]
670 };
671 if let Some(timeout) = request.timeout {
672 provider_request.timeout = timeout;
673 }
674 let child_operation_id = Uuid::new_v4();
675 let mut turn = match user
676 .start_agent_turn(
677 child_operation_id,
678 Some(request.parent_operation_id),
679 provider_request,
680 )
681 .await
682 {
683 Ok(turn) => turn,
684 Err(error) => {
685 if let Some(receipt) = error.receipt().cloned() {
686 host.record(AuditEvent::ProviderReceipt {
687 parent_operation_id: request.parent_operation_id,
688 round: round + 1,
689 manifest_hash,
690 usage: None,
691 receipt: Box::new(receipt),
692 })?;
693 }
694 return Err(anyhow::Error::new(error));
695 }
696 };
697 let mut used_tool = false;
698 let mut pending_capture: Option<Value> = None;
699 let mut requires_rerender = false;
700 let completed = loop {
701 let event = match turn.next_event().await {
702 Ok(Some(event)) => event,
703 Ok(None) => {
704 let receipt = turn.finish_unavailable()?.clone();
705 host.record(AuditEvent::ProviderReceipt {
706 parent_operation_id: request.parent_operation_id,
707 round: round + 1,
708 manifest_hash: manifest_hash.clone(),
709 usage: None,
710 receipt: Box::new(receipt),
711 })?;
712 anyhow::bail!("subagent provider ended without a terminal turn event");
713 }
714 Err(error) => {
715 if let Some(receipt) = error.receipt().cloned() {
716 host.record(AuditEvent::ProviderReceipt {
717 parent_operation_id: request.parent_operation_id,
718 round: round + 1,
719 manifest_hash: manifest_hash.clone(),
720 usage: None,
721 receipt: Box::new(receipt),
722 })?;
723 }
724 return Err(anyhow::Error::new(error));
725 }
726 };
727 match event {
728 AgentEvent::ProviderInput(_) => {}
729 AgentEvent::UsageUpdated(_) => {}
730 AgentEvent::ToolCall(native) => {
731 used_tool = true;
732 if capturing {
733 respond_or_record(
734 &mut turn,
735 host,
736 request.parent_operation_id,
737 round + 1,
738 &manifest_hash,
739 &native.call_id,
740 ToolResult::failure(
741 "No application tool is available while complete freeform output is being captured.",
742 ),
743 )
744 .await?;
745 continue;
746 }
747 if pending_capture.is_some() {
748 respond_or_record(
749 &mut turn,
750 host,
751 request.parent_operation_id,
752 round + 1,
753 &manifest_hash,
754 &native.call_id,
755 ToolResult::failure(
756 "A freeform output capture is pending; no other tool can run first.",
757 ),
758 )
759 .await?;
760 continue;
761 }
762 if requires_rerender {
763 respond_or_record(
764 &mut turn,
765 host,
766 request.parent_operation_id,
767 round + 1,
768 &manifest_hash,
769 &native.call_id,
770 ToolResult::failure(
771 "A state update is waiting to be re-rendered. End this slice before calling another tool.",
772 ),
773 )
774 .await?;
775 continue;
776 }
777 let call = match parse_ktool_call(&native) {
778 Ok(call) => call,
779 Err(error) => {
780 let text = format!("Invalid application tool call: {error}");
781 projection.push_history(format!("Ktool result:\n{text}"));
782 respond_or_record(
783 &mut turn,
784 host,
785 request.parent_operation_id,
786 round + 1,
787 &manifest_hash,
788 &native.call_id,
789 ToolResult::failure(text),
790 )
791 .await?;
792 continue;
793 }
794 };
795 host.record(AuditEvent::ToolCall {
796 parent_operation_id: request.parent_operation_id,
797 name: call.name.clone(),
798 arguments: call.arguments.clone(),
799 })?;
800 projection.push_history(format!(
801 "Ktool call:\n{}",
802 host.render_tool_call(&call)?
803 ));
804 let budget = ContextBudget {
805 projection: projection.clone(),
806 max_input_tokens: selected.max_input_tokens,
807 };
808 let mut outcome = host
809 .execute_tool(call.clone(), child_operation_id, budget)
810 .await
811 .unwrap_or_else(|error| {
812 ToolOutcome::failure(format!("{} failed: {error}", call.name))
813 });
814 let exact_result = outcome.text.clone();
815 let initially_ok = outcome.ok;
816 let has_state_updates = initially_ok && !outcome.state_updates.is_empty();
817 let mut provider_result =
818 compact_tool_result(&outcome.text, successful_state_updates(&outcome));
819 let mut candidate = projection.clone();
820 candidate.apply_updates(successful_state_updates(&outcome));
821 candidate.push_history_displaying(
822 format!("Ktool result:\n{provider_result}"),
823 if initially_ok {
824 outcome.displayed_state_keys.clone()
825 } else {
826 Default::default()
827 },
828 );
829 let accepted = candidate.estimated_tokens() <= selected.max_input_tokens;
830 if accepted {
831 projection = candidate;
832 requires_rerender = has_state_updates;
833 } else {
834 outcome.ok = false;
835 outcome.capture = None;
836 provider_result = "The tool ran, but its result or updated state could not fit in the subagent context. Do not retry it; report the capacity failure to Kennedy.".into();
837 projection.push_history(format!("Ktool result:\n{provider_result}"));
838 }
839 host.record(AuditEvent::ToolResult {
840 parent_operation_id: request.parent_operation_id,
841 name: call.name.clone(),
842 ok: initially_ok,
843 projection_accepted: accepted,
844 result: exact_result,
845 })?;
846 pending_capture = outcome.capture.take();
847 respond_or_record(
848 &mut turn,
849 host,
850 request.parent_operation_id,
851 round + 1,
852 &manifest_hash,
853 &native.call_id,
854 if outcome.ok {
855 ToolResult::success(provider_result)
856 } else {
857 ToolResult::failure(provider_result)
858 },
859 )
860 .await?;
861 }
862 AgentEvent::Completed(completed) => break completed,
863 }
864 };
865 let receipt = turn
866 .receipt()
867 .context("subagent provider completed without a usage receipt")?
868 .clone();
869 host.record(AuditEvent::ProviderReceipt {
870 parent_operation_id: request.parent_operation_id,
871 round: round + 1,
872 manifest_hash: manifest_hash.clone(),
873 usage: completed.usage.clone(),
874 receipt: Box::new(receipt),
875 })?;
876
877 let capture = deferred_capture.take().or(pending_capture);
878 if let Some(capture) = capture {
879 if !capturing && completed.answer.is_empty() {
880 deferred_capture = Some(capture);
881 continue;
882 }
883 let budget = ContextBudget {
884 projection: projection.clone(),
885 max_input_tokens: selected.max_input_tokens,
886 };
887 let outcome = host
888 .complete_capture(capture, completed.answer, budget)
889 .await?;
890 let mut candidate = projection.clone();
891 candidate.apply_updates(successful_state_updates(&outcome));
892 candidate.push_history(format!("Ktool result:\n{}", outcome.text));
893 ensure_capacity(&candidate, selected.max_input_tokens)?;
894 projection = candidate;
895 continue;
896 }
897 if requires_rerender {
898 let draft = completed.answer.trim();
899 if !draft.is_empty() {
900 projection.push_history(format!(
901 "Assistant draft produced before the state refresh:\n{draft}"
902 ));
903 }
904 continue;
905 }
906 let answer = completed.answer.trim().to_owned();
907 if !answer.is_empty() {
908 host.record(AuditEvent::Completed {
909 parent_operation_id: request.parent_operation_id,
910 model: request.model.clone(),
911 response: answer.clone(),
912 })?;
913 return Ok(RunResult {
914 answer,
915 model: selected,
916 });
917 }
918 ensure!(
919 used_tool,
920 "subagent provider completed without a response or tool call"
921 );
922 }
923 anyhow::bail!(
924 "subagent exceeded the {}-round tool-loop safety limit",
925 self.round_limit
926 )
927 }
928}
929
930async fn respond_or_record<H: Host>(
931 turn: &mut kcode_intelligence_router::AgentTurn,
932 host: &mut H,
933 parent_operation_id: Uuid,
934 round: u64,
935 manifest_hash: &str,
936 call_id: &str,
937 result: ToolResult,
938) -> anyhow::Result<()> {
939 if let Err(error) = turn.respond(call_id, result).await {
940 let receipt = turn.finish_unavailable()?.clone();
941 host.record(AuditEvent::ProviderReceipt {
942 parent_operation_id,
943 round,
944 manifest_hash: manifest_hash.into(),
945 usage: None,
946 receipt: Box::new(receipt),
947 })?;
948 return Err(anyhow::Error::new(error));
949 }
950 Ok(())
951}
952
953async fn respond_session_or_record<H: SessionHost>(
954 turn: &mut kcode_intelligence_router::AgentTurn,
955 host: &mut H,
956 round: u64,
957 call_id: &str,
958 result: ToolResult,
959) -> anyhow::Result<()> {
960 if let Err(error) = turn.respond(call_id, result).await {
961 let receipt = turn.finish_unavailable()?.clone();
962 host.record(SessionEvent::ProviderReceipt {
963 round,
964 usage: None,
965 receipt: Box::new(receipt),
966 })
967 .await?;
968 return Err(anyhow::Error::new(error));
969 }
970 Ok(())
971}
972
973#[derive(Clone)]
974struct Projection {
975 context: Vec<String>,
976 task: String,
977 history: Vec<ProjectedHistory>,
978 states: Vec<ProjectedState>,
979}
980
981#[derive(Clone)]
982struct ProjectedHistory {
983 text: String,
984 displayed_state_keys: Vec<String>,
985}
986
987#[derive(Clone)]
988struct ProjectedState {
989 key: String,
990 text: String,
991}
992
993impl Projection {
994 fn new(context: Vec<String>, task: String) -> Self {
995 Self {
996 context,
997 task,
998 history: Vec::new(),
999 states: Vec::new(),
1000 }
1001 }
1002
1003 fn render(&self) -> String {
1004 self.context
1005 .iter()
1006 .map(String::as_str)
1007 .chain(std::iter::once(self.task.as_str()))
1008 .chain(self.history.iter().map(|entry| entry.text.as_str()))
1009 .chain(
1010 self.states
1011 .iter()
1012 .filter(|state| {
1013 !self.history.iter().any(|entry| {
1014 entry
1015 .displayed_state_keys
1016 .iter()
1017 .any(|key| key == &state.key)
1018 })
1019 })
1020 .map(|state| state.text.as_str()),
1021 )
1022 .filter(|section| !section.is_empty())
1023 .collect::<Vec<_>>()
1024 .join("\n\n")
1025 }
1026
1027 fn push_history(&mut self, text: impl Into<String>) {
1028 self.push_history_displaying(text, Vec::new());
1029 }
1030
1031 fn push_history_displaying(
1032 &mut self,
1033 text: impl Into<String>,
1034 displayed_state_keys: Vec<String>,
1035 ) {
1036 self.history.push(ProjectedHistory {
1037 text: text.into(),
1038 displayed_state_keys,
1039 });
1040 }
1041
1042 fn update_state(&mut self, key: String, text: Option<String>) {
1043 self.states.retain(|state| state.key != key);
1044 if let Some(text) = text {
1045 self.states.push(ProjectedState { key, text });
1046 }
1047 }
1048
1049 fn apply_updates(&mut self, updates: &[StateUpdate]) {
1050 for update in updates {
1051 let marker = if update.text.is_some() {
1052 SUPERSEDED_TOOL_OUTPUT
1053 } else {
1054 REMOVED_TOOL_OUTPUT
1055 };
1056 for entry in &mut self.history {
1057 if entry
1058 .displayed_state_keys
1059 .iter()
1060 .any(|key| key == &update.key)
1061 {
1062 entry.text = marker.into();
1063 entry.displayed_state_keys.clear();
1064 }
1065 }
1066 self.update_state(update.key.clone(), update.text.clone());
1067 }
1068 }
1069
1070 fn estimated_tokens(&self) -> u64 {
1071 (self.render().chars().count() as u64)
1072 .div_ceil(4)
1073 .saturating_add(PROTOCOL_TOKEN_RESERVE)
1074 }
1075}
1076
1077fn compact_tool_result(text: &str, states: &[StateUpdate]) -> String {
1078 if states.is_empty() {
1079 return text.to_owned();
1080 }
1081 let result = if text.chars().count() <= INLINE_TOOL_RESULT_CHARACTERS {
1082 text
1083 } else {
1084 "Tool completed successfully."
1085 };
1086 format!(
1087 "{result}\n\nThe updated state will be rendered in the next fresh context slice; end this slice now."
1088 )
1089}
1090
1091fn successful_state_updates(outcome: &ToolOutcome) -> &[StateUpdate] {
1092 if outcome.ok {
1093 &outcome.state_updates
1094 } else {
1095 &[]
1096 }
1097}
1098
1099fn ensure_capacity(projection: &Projection, max_input_tokens: u64) -> anyhow::Result<()> {
1100 let estimated = projection.estimated_tokens();
1101 ensure!(
1102 estimated <= max_input_tokens,
1103 "subagent context requires approximately {estimated} input tokens, over the selected model's {max_input_tokens}-token input limit"
1104 );
1105 Ok(())
1106}
1107
1108fn ktool_definition(description: &str) -> DynamicTool {
1109 DynamicTool::new(
1110 "call_ktool",
1111 description,
1112 json!({
1113 "type": "object",
1114 "additionalProperties": false,
1115 "required": ["name", "arguments"],
1116 "properties": {
1117 "name": {"type": "string"},
1118 "arguments": {"type": "object"}
1119 }
1120 }),
1121 )
1122}
1123
1124fn parse_ktool_call(call: &DynamicToolCall) -> anyhow::Result<ToolCall> {
1125 ensure!(call.tool == "call_ktool", "unknown provider tool");
1126 let arguments = call
1127 .arguments
1128 .as_object()
1129 .context("call_ktool arguments must be an object")?;
1130 ensure!(
1131 arguments
1132 .keys()
1133 .all(|key| matches!(key.as_str(), "name" | "arguments")),
1134 "call_ktool contains unknown arguments"
1135 );
1136 let name = arguments
1137 .get("name")
1138 .and_then(Value::as_str)
1139 .map(str::trim)
1140 .filter(|name| !name.is_empty() && name.chars().count() <= 100)
1141 .context("call_ktool.name must be a non-empty bounded string")?
1142 .to_owned();
1143 let arguments = arguments
1144 .get("arguments")
1145 .filter(|value| value.is_object())
1146 .context("call_ktool.arguments must be an object")?
1147 .clone();
1148 Ok(ToolCall { name, arguments })
1149}
1150
1151fn reasoning_effort(value: &str) -> anyhow::Result<ReasoningEffort> {
1152 Ok(match value {
1153 "none" => ReasoningEffort::None,
1154 "minimal" => ReasoningEffort::Minimal,
1155 "low" => ReasoningEffort::Low,
1156 "medium" => ReasoningEffort::Medium,
1157 "high" => ReasoningEffort::High,
1158 "xhigh" => ReasoningEffort::XHigh,
1159 "max" => ReasoningEffort::Max,
1160 _ => anyhow::bail!("unsupported reasoning effort {value:?}"),
1161 })
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167
1168 #[test]
1169 fn projection_replaces_state_and_budget_accounts_for_reserve() {
1170 let mut projection = Projection::new(vec!["context".into()], "task".into());
1171 projection.update_state("file".into(), Some("old".into()));
1172 projection.update_state("file".into(), Some("new".into()));
1173 assert_eq!(projection.states.len(), 1);
1174 assert!(projection.render().contains("new"));
1175 assert!(!projection.render().contains("old"));
1176 assert!(projection.estimated_tokens() >= PROTOCOL_TOKEN_RESERVE);
1177 }
1178
1179 #[test]
1180 fn state_update_supersedes_earlier_display_and_moves_current_value_after_history() {
1181 let mut projection = Projection::new(vec!["context".into()], "task".into());
1182 projection.push_history("Ktool call: open");
1183 projection.push_history_displaying(
1184 "Ktool result:\ncomplete old source",
1185 vec!["tool-state:7".into()],
1186 );
1187 projection.push_history("Ktool call: write");
1188 projection.apply_updates(&[StateUpdate {
1189 key: "tool-state:7".into(),
1190 text: Some("complete new source".into()),
1191 }]);
1192 projection.push_history("Ktool result: write completed");
1193
1194 let rendered = projection.render();
1195 assert!(!rendered.contains("complete old source"));
1196 assert_eq!(rendered.matches(SUPERSEDED_TOOL_OUTPUT).count(), 1);
1197 assert_eq!(rendered.matches("complete new source").count(), 1);
1198 assert!(
1199 rendered.find(SUPERSEDED_TOOL_OUTPUT).unwrap()
1200 < rendered.find("Ktool call: write").unwrap()
1201 );
1202 assert!(
1203 rendered.find("Ktool result: write completed").unwrap()
1204 < rendered.find("complete new source").unwrap()
1205 );
1206 }
1207
1208 #[test]
1209 fn current_state_is_not_duplicated_when_the_latest_result_displays_it() {
1210 let mut projection = Projection::new(vec!["context".into()], "task".into());
1211 projection.apply_updates(&[StateUpdate {
1212 key: "tool-state:7".into(),
1213 text: Some("complete source".into()),
1214 }]);
1215 projection.push_history_displaying(
1216 "Ktool result:\ncomplete source",
1217 vec!["tool-state:7".into()],
1218 );
1219
1220 assert_eq!(projection.render().matches("complete source").count(), 1);
1221 }
1222
1223 #[test]
1224 fn state_update_only_supersedes_displays_with_the_same_identity() {
1225 let mut projection = Projection::new(vec!["context".into()], "task".into());
1226 projection.push_history_displaying("first output", vec!["first".into()]);
1227 projection.push_history_displaying("second output", vec!["second".into()]);
1228 projection.apply_updates(&[StateUpdate {
1229 key: "first".into(),
1230 text: Some("current first output".into()),
1231 }]);
1232
1233 let rendered = projection.render();
1234 assert!(
1235 !rendered
1236 .split("\n\n")
1237 .any(|section| section == "first output")
1238 );
1239 assert!(rendered.contains(SUPERSEDED_TOOL_OUTPUT));
1240 assert!(rendered.contains("second output"));
1241 assert!(rendered.contains("current first output"));
1242 }
1243
1244 #[test]
1245 fn removed_state_uses_a_truthful_supersession_marker() {
1246 let mut projection = Projection::new(vec!["context".into()], "task".into());
1247 projection.push_history_displaying("retired output", vec!["state".into()]);
1248 projection.apply_updates(&[StateUpdate {
1249 key: "state".into(),
1250 text: None,
1251 }]);
1252
1253 let rendered = projection.render();
1254 assert!(!rendered.contains("retired output"));
1255 assert!(rendered.contains(REMOVED_TOOL_OUTPUT));
1256 }
1257
1258 #[test]
1259 fn failed_outcomes_cannot_update_or_supersede_state() {
1260 let mut projection = Projection::new(vec!["context".into()], "task".into());
1261 projection.push_history_displaying("current output", vec!["state".into()]);
1262 let failed = ToolOutcome {
1263 text: "write failed".into(),
1264 ok: false,
1265 state_updates: vec![StateUpdate {
1266 key: "state".into(),
1267 text: Some("invalid update".into()),
1268 }],
1269 displayed_state_keys: Vec::new(),
1270 capture: None,
1271 };
1272
1273 projection.apply_updates(successful_state_updates(&failed));
1274
1275 let rendered = projection.render();
1276 assert!(rendered.contains("current output"));
1277 assert!(!rendered.contains("invalid update"));
1278 assert!(!rendered.contains(SUPERSEDED_TOOL_OUTPUT));
1279 }
1280
1281 #[test]
1282 fn state_changes_compact_large_tool_results() {
1283 let compacted = compact_tool_result(
1284 &"x".repeat(INLINE_TOOL_RESULT_CHARACTERS + 1),
1285 &[StateUpdate {
1286 key: "state".into(),
1287 text: Some("current".into()),
1288 }],
1289 );
1290 assert!(compacted.starts_with("Tool completed successfully."));
1291 assert!(compacted.contains("fresh context slice"));
1292 }
1293
1294 #[test]
1295 fn native_tool_wrapper_is_strict() {
1296 let call = parse_ktool_call(&DynamicToolCall {
1297 call_id: "1".into(),
1298 tool: "call_ktool".into(),
1299 arguments: json!({"name": "Read", "arguments": {"id": 1}}),
1300 })
1301 .unwrap();
1302 assert_eq!(call.name, "Read");
1303 assert_eq!(call.arguments["id"], 1);
1304 }
1305}