1use std::collections::{BTreeMap, BTreeSet, VecDeque};
7use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
8use std::sync::{Arc, Mutex, MutexGuard};
9use std::time::{Duration, SystemTime, UNIX_EPOCH};
10
11use async_trait::async_trait;
12use futures_util::{stream, StreamExt};
13use orchestral_core::agent_protocol::{
14 spi::{
15 AgentProvider, AgentProviderStream, AgentRecovery, AgentRecoveryRequest, AgentStart,
16 AgentStartError,
17 },
18 wire::{
19 AgentAdmission, AgentCapabilities, AgentCommand, AgentCommandEnvelope, AgentDelivery,
20 AgentDescriptor, AgentDescriptorEnvelope, AgentEvent, AgentEventDraft, AgentEventId,
21 AgentExecutionRef, AgentFailure, AgentId, AgentProtocolError, AgentProtocolErrorCode,
22 AgentProviderId, AgentProviderStreamItem, AgentRejection, AgentRejectionCode,
23 AgentStartRequest, AgentTelemetry, AgentTelemetryEnvelope, ApprovalDecision,
24 ArtifactRefWithDigest, BindingRequirement, CancelSupport, CommandId, Content, ContentBody,
25 ControlCapabilities, DeliveryId, Digest, EffectMediation, IncompleteReason, MoneyAmount,
26 OutputId, PartialDelivery, PartialDeliveryId, PendingRequest, PendingRequestKind,
27 PendingRequestPayload, Provenance, ProviderCommandDisposition, ProviderCommandOutcome,
28 RequestId, RequestResolution, ResourceBindingMode, ResourceBindingSkip,
29 ResourceBindingSkipCode, ResourceCapability, ResourceKind, RunId, RunLimitKind,
30 TelemetryId, ToolActivityErrorDetail, ToolActivityEvidence, ToolActivityId,
31 ToolActivityState, UsageReport,
32 },
33 AGENT_PROTOCOL_V1,
34};
35use orchestral_core::agent_session::{
36 AgentSessionError, AgentSessionEvent, AgentSessionEventDraft, AgentSessionEventId,
37 AgentSessionJournalStore, AgentSessionRecord, InMemoryAgentSessionJournalStore,
38};
39use orchestral_core::executor::{ExecutionProgressEvent, ExecutionProgressReporter};
40use orchestral_core::model_protocol::{
41 ModelBackend, ModelContent, ModelError, ModelErrorCode, ModelEvent, ModelFinishReason,
42 ModelMessage, ModelRequest, ModelRequestId, ModelRole, ModelToolCallId, ModelToolDefinition,
43 ModelUsage,
44};
45pub use orchestral_core::model_retry::{ContextRecoveryPolicy, ModelRetryPolicy};
46use orchestral_core::project_instructions::ProjectInstruction;
47use orchestral_core::skill_protocol::SkillLoad;
48use orchestral_core::tool_protocol::{
49 ApprovalBinding, ApprovalCapability, RunToolGrant, ToolCallId, ToolInvocation, ToolOutcome,
50 ToolOutput,
51};
52use orchestral_core::types::{Plan, WorkflowId};
53use serde::Deserialize;
54use tokio::sync::{broadcast, oneshot, watch};
55use tokio_util::sync::CancellationToken;
56
57use crate::approval_bridge::AgentApprovalBridge;
58use crate::generic_agent_checkpoint::{
59 AppendGenericCheckpointOutcome, CreateGenericRunOutcome, GenericAgentCheckpointStore,
60 GenericAgentRunRegistration, GenericCheckpointDraft, GenericCheckpointError,
61 GenericCheckpointEvent, GenericCheckpointEventId, GenericCheckpointPhase, GenericLoopBoundary,
62 GenericModelContextTrace, GenericModelObservation, GenericObservedToolCall,
63 InMemoryGenericAgentCheckpointStore, StoredGenericAgentRun,
64};
65use crate::skill::{LoadedSkillSet, SkillLoadOutcome, SkillRuntime};
66use crate::tool_runtime::{AgentToolRuntime, GuardedToolResult, ToolRuntimeError};
67use crate::workflow_strategy::{WorkflowExecutionRequest, WorkflowExecutionStrategy};
68use crate::{
69 AgentSessionCompactor, AgentSessionContextEngine, AgentSessionSummarizer, JsonSizeTokenMeter,
70 ModelTokenMeter, ModelTokenMeterDescriptor, SessionCompactionPolicy, SessionContextError,
71 SessionContextProjection, SessionContextRequest, SessionSummarizerDescriptor,
72};
73
74const WORKFLOW_TOOL_NAME: &str = "orchestral_workflow";
75const SKILL_READ_TOOL_NAME: &str = "skill_read";
76const REQUEST_INPUT_TOOL_NAME: &str = "orchestral_request_input";
77const RUN_STOP_RUNNING: u8 = 0;
78const RUN_STOP_HOST_CANCEL: u8 = 1;
79const RUN_STOP_DEADLINE: u8 = 2;
80const RUN_STOP_COMPLETING: u8 = 3;
81const TOKENS_PER_MILLION: u128 = 1_000_000;
82
83#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ModelCostPolicy {
89 pub currency: String,
90 pub input_microunits_per_million_tokens: u64,
91 pub output_microunits_per_million_tokens: u64,
92}
93
94impl ModelCostPolicy {
95 pub fn new(
96 currency: impl Into<String>,
97 input_microunits_per_million_tokens: u64,
98 output_microunits_per_million_tokens: u64,
99 ) -> Result<Self, AgentProtocolError> {
100 let policy = Self {
101 currency: currency.into(),
102 input_microunits_per_million_tokens,
103 output_microunits_per_million_tokens,
104 };
105 policy.validate()?;
106 Ok(policy)
107 }
108
109 fn validate(&self) -> Result<(), AgentProtocolError> {
110 if self.currency.len() != 3
111 || !self.currency.bytes().all(|byte| byte.is_ascii_uppercase())
112 || (self.input_microunits_per_million_tokens == 0
113 && self.output_microunits_per_million_tokens == 0)
114 {
115 return Err(AgentProtocolError::new(
116 AgentProtocolErrorCode::InvalidSpec,
117 "model cost policy requires an uppercase currency and at least one positive rate",
118 ));
119 }
120 Ok(())
121 }
122
123 pub fn quote(&self, input_tokens: u64, output_tokens: u64) -> MoneyAmount {
124 let input = u128::from(input_tokens)
125 .saturating_mul(u128::from(self.input_microunits_per_million_tokens));
126 let output = u128::from(output_tokens)
127 .saturating_mul(u128::from(self.output_microunits_per_million_tokens));
128 let microunits = input
129 .saturating_add(output)
130 .div_ceil(TOKENS_PER_MILLION)
131 .min(u128::from(u64::MAX)) as u64;
132 MoneyAmount {
133 currency: self.currency.clone(),
134 microunits,
135 }
136 }
137
138 fn max_output_tokens_within(
139 &self,
140 input_tokens: u64,
141 output_tokens: u64,
142 ceiling: &MoneyAmount,
143 ) -> Option<u64> {
144 if ceiling.currency != self.currency
145 || self.quote(input_tokens, 0).microunits > ceiling.microunits
146 {
147 return None;
148 }
149 if self.output_microunits_per_million_tokens == 0 {
150 return Some(output_tokens);
151 }
152 let mut low = 0_u64;
153 let mut high = output_tokens;
154 while low < high {
155 let candidate = low.saturating_add(high).saturating_add(1) / 2;
156 if self.quote(input_tokens, candidate).microunits <= ceiling.microunits {
157 low = candidate;
158 } else {
159 high = candidate.saturating_sub(1);
160 }
161 }
162 Some(low)
163 }
164}
165
166#[derive(Debug, Clone)]
167pub struct GenericAgentConfig {
168 pub provider_id: AgentProviderId,
169 pub agent_id: AgentId,
170 pub system_prompt: String,
171 pub input_requests_enabled: bool,
173 pub project_instructions: Vec<ProjectInstruction>,
175 pub model_retry: ModelRetryPolicy,
177 pub context_recovery: ContextRecoveryPolicy,
178 pub stream_buffer: usize,
179 pub continuation: ContinuationPolicy,
180 pub history_limit: usize,
181 pub max_context_tokens: u64,
182 pub reserved_output_tokens: u64,
183 pub minimum_output_reserve_tokens: Option<u64>,
187 pub model_cost_policy: Option<ModelCostPolicy>,
188}
189
190#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
198pub struct ContinuationPolicy {
199 pub max_model_steps: Option<u64>,
200 pub max_tool_calls: Option<u64>,
201}
202
203impl ContinuationPolicy {
204 pub fn effective_model_steps(self, requested: Option<u64>) -> Option<u64> {
205 intersect_limit(requested, self.max_model_steps)
206 }
207
208 pub fn effective_tool_calls(self, requested: Option<u64>) -> Option<u64> {
209 intersect_limit(requested, self.max_tool_calls)
210 }
211
212 fn validate(self) -> Result<(), AgentProtocolError> {
213 if self.max_model_steps == Some(0) || self.max_tool_calls == Some(0) {
214 return Err(AgentProtocolError::new(
215 AgentProtocolErrorCode::InvalidSpec,
216 "configured continuation ceilings must be positive when present",
217 ));
218 }
219 Ok(())
220 }
221}
222
223fn intersect_limit(requested: Option<u64>, host_ceiling: Option<u64>) -> Option<u64> {
224 match (requested, host_ceiling) {
225 (Some(requested), Some(host_ceiling)) => Some(requested.min(host_ceiling)),
226 (Some(limit), None) | (None, Some(limit)) => Some(limit),
227 (None, None) => None,
228 }
229}
230
231impl GenericAgentConfig {
232 pub fn new(provider_id: impl Into<String>, agent_id: impl Into<String>) -> Self {
233 Self {
234 provider_id: AgentProviderId::new(provider_id),
235 agent_id: AgentId::new(agent_id),
236 system_prompt: concat!(
237 "You are Orchestral, an agent running in a local application. ",
238 "You and the user share one or more Host-provided workspaces. Work toward the user's ",
239 "requested outcome using the supplied context and Tools. Tool definitions and ",
240 "Host policy are authoritative capability boundaries. Inspect available ",
241 "evidence before making claims, take relevant reversible actions when the ",
242 "request is clear, and ask only when a material choice or required fact cannot ",
243 "be derived. Treat explicit ordering, preconditions, and requested final states ",
244 "as acceptance constraints: establish them before dependent work and verify ",
245 "them before delivery. Do not broaden completed work with unrequested ",
246 "integration, publication, cleanup, or reversal. ",
247 "Batch independent observations whose arguments are supported by current context ",
248 "in one tool-call response. This can include inspections and an already-established ",
249 "validation command when neither needs the other's result. Wait when a result can ",
250 "change another call's arguments, necessity, or safety; keep edits and their ",
251 "verification ordered. ",
252 "Prefer a dedicated Tool over a shell equivalent when one is available. For ",
253 "multiple workspaces, use the exact Host-provided workspace selector on file ",
254 "Tools and the matching workdir on exec_command; do not fall back to grep, cat, ",
255 "or shell-based edits merely because the target is not in the primary workspace. ",
256 "For workspace text changes, prefer file_edit for exact, unique text replacements. ",
257 "Group currently known non-overlapping changes to one file in a single edits array; ",
258 "all old_text values match the original file. Use apply_patch for structured ",
259 "changes across files. ",
260 "Use file_write to create or intentionally replace a complete file. Inspect ",
261 "existing content before changing it and run relevant ",
262 "verification. Keep user-facing responses concise unless the user requests a ",
263 "detailed explanation. For completed work, briefly state the outcome, verification ",
264 "results, and any remaining gaps. Include changed code or raw Tool logs only when ",
265 "requested or needed to explain an unresolved issue. Avoid repeating explanations ",
266 "or checks that add no new evidence. Permission is owned by the Host, not inferred ",
267 "by you. Treat every Tool ",
268 "failure as an observation to correct or safely work around; report completion ",
269 "only from successful evidence."
270 )
271 .to_owned(),
272 stream_buffer: 128,
273 input_requests_enabled: true,
274 project_instructions: Vec::new(),
275 model_retry: ModelRetryPolicy::default(),
276 context_recovery: ContextRecoveryPolicy::default(),
277 continuation: ContinuationPolicy::default(),
278 history_limit: 128,
279 max_context_tokens: 128 * 1024,
280 reserved_output_tokens: 4 * 1024,
281 minimum_output_reserve_tokens: None,
282 model_cost_policy: None,
283 }
284 }
285}
286
287#[derive(Clone)]
288pub struct InternalGenericAgentProvider {
289 inner: Arc<GenericInner>,
290}
291
292struct GenericInner {
293 backend: Arc<dyn ModelBackend>,
294 descriptor: AgentDescriptorEnvelope,
295 config: GenericAgentConfig,
296 tools: Option<GenericTools>,
297 skills: Option<Arc<SkillRuntime>>,
298 session_journal: Arc<dyn AgentSessionJournalStore>,
299 context_engine: AgentSessionContextEngine,
300 session_compactor: Option<Arc<AgentSessionCompactor>>,
301 checkpoint_store: Arc<dyn GenericAgentCheckpointStore>,
302 config_digest: Digest,
303 state: Mutex<GenericState>,
304}
305
306struct GenericTools {
307 runtime: Arc<dyn AgentToolRuntime>,
308 runtime_contract_digest: Digest,
309 run_grant: RunToolGrant,
310 model_definitions: Vec<ModelToolDefinition>,
311 workflow: Option<Arc<WorkflowExecutionStrategy>>,
312 approval_bridge: Option<Arc<dyn AgentApprovalBridge>>,
313}
314
315#[derive(Default)]
316struct GenericState {
317 runs: BTreeMap<RunId, GenericRun>,
318 sessions: BTreeMap<orchestral_core::agent_protocol::wire::AgentSessionId, GenericSession>,
319}
320
321#[derive(Default)]
322struct GenericSession {
323 active_run: Option<RunId>,
324}
325
326struct GenericRun {
327 request: AgentStartRequest,
328 execution: AgentExecutionRef,
329 admission: AgentAdmission,
330 durable_events: Vec<AgentEventDraft>,
331 sender: broadcast::Sender<Result<AgentProviderStreamItem, AgentProtocolError>>,
332 terminal: bool,
333 cancellation: CancellationToken,
334 stop_cause: Arc<AtomicU8>,
335 cancel_command: Option<(CommandId, String)>,
336 commands: BTreeMap<CommandId, StoredCommand>,
337 queued_steers: VecDeque<QueuedSteer>,
338 steer_signal: watch::Sender<u64>,
339 pending_inputs: BTreeMap<RequestId, PendingInput>,
340 pending_approvals: BTreeMap<RequestId, PendingApproval>,
341 checkpoint_seq: u64,
342}
343
344struct QueuedSteer {
345 command_id: CommandId,
346 content: Vec<Content>,
347 message: ModelMessage,
348 deferred: bool,
349}
350
351struct PendingInput {
352 responder: Option<oneshot::Sender<InputResponse>>,
353}
354
355#[derive(Clone)]
356struct InputResponse {
357 command_id: CommandId,
358 resolution: RequestResolution,
359}
360
361struct PendingApproval {
362 binding: ApprovalBinding,
363 responder: Option<oneshot::Sender<ApprovalResponse>>,
364}
365
366#[derive(Clone)]
367struct ApprovalResponse {
368 command_id: CommandId,
369 resolution: RequestResolution,
370 capability: Option<ApprovalCapability>,
371}
372
373struct RecoveredResolution {
374 command_id: CommandId,
375 resolution: RequestResolution,
376 capability: Option<ApprovalCapability>,
377}
378
379struct RecoveredApprovalWaiter {
380 request_id: RequestId,
381 binding: ApprovalBinding,
382 replayed_outcome: Option<ToolOutcome>,
383 responder: Option<oneshot::Sender<ApprovalResponse>>,
384 response: Option<oneshot::Receiver<ApprovalResponse>>,
385 bridge: Arc<dyn AgentApprovalBridge>,
386}
387
388struct StoredCommand {
389 digest: Digest,
390 outcome: ProviderCommandOutcome,
391}
392
393struct GenericExecutionSeed {
394 run_started: bool,
395 next_model_round: u64,
396 total_usage: ModelUsage,
397 tool_call_count: u64,
398 last_response: String,
399 supporting_event_ids: Vec<AgentEventId>,
400}
401
402#[allow(clippy::large_enum_variant)]
406enum GenericRecoveryContinuation {
407 ModelLoop {
408 restore_initial_input: bool,
409 },
410 Input {
411 round: u64,
412 request_id: ModelRequestId,
413 request_digest: Digest,
414 observation: GenericModelObservation,
415 call: GenericObservedToolCall,
416 arguments: serde_json::Value,
417 prompt: String,
418 request_open: bool,
419 committed_response: Option<InputResponse>,
420 resolved_response: Option<InputResponse>,
421 response: Option<oneshot::Receiver<InputResponse>>,
422 },
423 Approval {
424 round: u64,
425 request_id: ModelRequestId,
426 request_digest: Digest,
427 observation: GenericModelObservation,
428 call: GenericObservedToolCall,
429 arguments: serde_json::Value,
430 request: PendingRequest,
431 binding: Option<ApprovalBinding>,
432 committed_response: Option<ApprovalResponse>,
433 resolved_response: Option<ApprovalResponse>,
434 response: Option<oneshot::Receiver<ApprovalResponse>>,
435 },
436 Skill {
437 round: u64,
438 request_id: ModelRequestId,
439 request_digest: Digest,
440 observation: GenericModelObservation,
441 call: GenericObservedToolCall,
442 arguments: serde_json::Value,
443 recovered_observation: Option<SkillCallObservation>,
444 },
445 Workflow {
446 round: u64,
447 request_id: ModelRequestId,
448 request_digest: Digest,
449 observation: GenericModelObservation,
450 call: GenericObservedToolCall,
451 arguments: serde_json::Value,
452 recovery_replay: bool,
453 },
454 WorkflowOutput {
455 round: u64,
456 request_id: ModelRequestId,
457 request_digest: Digest,
458 observation: GenericModelObservation,
459 call: GenericObservedToolCall,
460 arguments: serde_json::Value,
461 outcome: WorkflowCallObservation,
462 workflow_event_id: AgentEventId,
463 },
464 Tool {
465 round: u64,
466 request_id: ModelRequestId,
467 request_digest: Digest,
468 observation: GenericModelObservation,
469 call: GenericObservedToolCall,
470 arguments: serde_json::Value,
471 },
472}
473
474impl GenericExecutionSeed {
475 fn fresh() -> Self {
476 Self {
477 run_started: false,
478 next_model_round: 1,
479 total_usage: ModelUsage::default(),
480 tool_call_count: 0,
481 last_response: String::new(),
482 supporting_event_ids: Vec::new(),
483 }
484 }
485}
486
487mod command;
488mod context_anchor;
489mod coordinator;
490mod provider;
491mod provider_spi;
492mod recovery_activate;
493use context_anchor::*;
494mod recovery_approval;
495mod recovery_dispatch;
496mod recovery_entry;
497mod recovery_stage;
498use recovery_activate::*;
499use recovery_approval::*;
500use recovery_dispatch::*;
501use recovery_stage::*;
502mod recovery_loop;
503use recovery_loop::*;
504mod recovery_projection;
505use recovery_projection::*;
506mod context;
507use context::*;
508mod context_recovery;
509mod model_retry;
510mod model_step;
511use model_step::*;
512mod tool_step;
513use tool_step::*;
514mod recovery_resume;
515mod recovery_tool;
516use recovery_resume::*;
517use recovery_tool::*;
518mod control;
519use control::*;
520mod skills;
521use skills::*;
522mod workflow;
523use workflow::*;
524mod state_flow;
525use state_flow::*;
526mod completion;
527use completion::*;
528mod setup;
529use setup::*;
530
531#[cfg(test)]
532mod tests;