1use super::*;
2
3impl InternalGenericAgentProvider {
4 pub fn with_checkpoint_store(
7 mut self,
8 checkpoint_store: Arc<dyn GenericAgentCheckpointStore>,
9 ) -> Result<Self, AgentProtocolError> {
10 let inner = Arc::get_mut(&mut self.inner).ok_or_else(|| {
11 AgentProtocolError::new(
12 AgentProtocolErrorCode::InvalidTransition,
13 "Generic Agent checkpoint store must be bound before the Provider is shared",
14 )
15 })?;
16 inner.checkpoint_store = checkpoint_store;
17 Ok(self)
18 }
19
20 pub fn with_session_compaction(
25 mut self,
26 summarizer: Arc<dyn AgentSessionSummarizer>,
27 policy: SessionCompactionPolicy,
28 ) -> Result<Self, AgentProtocolError> {
29 policy.validate().map_err(|error| {
30 AgentProtocolError::new(AgentProtocolErrorCode::InvalidSpec, error.to_string())
31 })?;
32 let inner = Arc::get_mut(&mut self.inner).ok_or_else(|| {
33 AgentProtocolError::new(
34 AgentProtocolErrorCode::InvalidTransition,
35 "Session compaction must be bound before the Generic Agent Provider is shared",
36 )
37 })?;
38 if inner.session_compactor.is_some() {
39 return Err(AgentProtocolError::new(
40 AgentProtocolErrorCode::InvalidTransition,
41 "Session compaction is already bound",
42 ));
43 }
44 let compactor =
45 AgentSessionCompactor::new(inner.session_journal.clone(), summarizer, policy).map_err(
46 |error| {
47 AgentProtocolError::new(AgentProtocolErrorCode::InvalidSpec, error.to_string())
48 },
49 )?;
50 let config_digest = bind_session_compaction_config_digest(
51 &inner.config_digest,
52 compactor.policy(),
53 compactor.summarizer_descriptor(),
54 )?;
55 inner.config_digest = config_digest;
56 inner.session_compactor = Some(Arc::new(compactor));
57 Ok(self)
58 }
59
60 pub fn new(
61 backend: Arc<dyn ModelBackend>,
62 config: GenericAgentConfig,
63 ) -> Result<Self, AgentProtocolError> {
64 Self::build(
65 backend,
66 config,
67 None,
68 None,
69 Arc::new(InMemoryAgentSessionJournalStore::default()),
70 Arc::new(JsonSizeTokenMeter::default()),
71 )
72 }
73
74 pub fn new_with_session_journal(
75 backend: Arc<dyn ModelBackend>,
76 config: GenericAgentConfig,
77 session_journal: Arc<dyn AgentSessionJournalStore>,
78 token_meter: Arc<dyn ModelTokenMeter>,
79 ) -> Result<Self, AgentProtocolError> {
80 Self::build(backend, config, None, None, session_journal, token_meter)
81 }
82
83 pub fn new_with_tools(
84 backend: Arc<dyn ModelBackend>,
85 config: GenericAgentConfig,
86 runtime: Arc<dyn AgentToolRuntime>,
87 run_grant: RunToolGrant,
88 ) -> Result<Self, AgentProtocolError> {
89 Self::build(
90 backend,
91 config,
92 Some(configure_tools(runtime, run_grant, None, None)?),
93 None,
94 Arc::new(InMemoryAgentSessionJournalStore::default()),
95 Arc::new(JsonSizeTokenMeter::default()),
96 )
97 }
98
99 pub fn new_with_tools_and_session_journal(
100 backend: Arc<dyn ModelBackend>,
101 config: GenericAgentConfig,
102 runtime: Arc<dyn AgentToolRuntime>,
103 run_grant: RunToolGrant,
104 session_journal: Arc<dyn AgentSessionJournalStore>,
105 token_meter: Arc<dyn ModelTokenMeter>,
106 ) -> Result<Self, AgentProtocolError> {
107 Self::build(
108 backend,
109 config,
110 Some(configure_tools(runtime, run_grant, None, None)?),
111 None,
112 session_journal,
113 token_meter,
114 )
115 }
116
117 pub fn new_with_tools_approval_and_session_journal(
120 backend: Arc<dyn ModelBackend>,
121 config: GenericAgentConfig,
122 runtime: Arc<dyn AgentToolRuntime>,
123 run_grant: RunToolGrant,
124 approval_bridge: Arc<dyn AgentApprovalBridge>,
125 session_journal: Arc<dyn AgentSessionJournalStore>,
126 token_meter: Arc<dyn ModelTokenMeter>,
127 ) -> Result<Self, AgentProtocolError> {
128 Self::build(
129 backend,
130 config,
131 Some(configure_tools(
132 runtime,
133 run_grant,
134 None,
135 Some(approval_bridge),
136 )?),
137 None,
138 session_journal,
139 token_meter,
140 )
141 }
142
143 pub fn new_with_workflow_and_session_journal(
146 backend: Arc<dyn ModelBackend>,
147 config: GenericAgentConfig,
148 runtime: Arc<dyn AgentToolRuntime>,
149 run_grant: RunToolGrant,
150 workflow: Arc<WorkflowExecutionStrategy>,
151 session_journal: Arc<dyn AgentSessionJournalStore>,
152 token_meter: Arc<dyn ModelTokenMeter>,
153 ) -> Result<Self, AgentProtocolError> {
154 if !workflow.uses_tool_runtime(&runtime) {
155 return Err(AgentProtocolError::new(
156 AgentProtocolErrorCode::InvalidSpec,
157 "Generic Agent and Workflow must share one guarded Tool Runtime",
158 ));
159 }
160 Self::build(
161 backend,
162 config,
163 Some(configure_tools(runtime, run_grant, Some(workflow), None)?),
164 None,
165 session_journal,
166 token_meter,
167 )
168 }
169
170 pub fn new_with_skills_and_session_journal(
173 backend: Arc<dyn ModelBackend>,
174 config: GenericAgentConfig,
175 skills: Arc<SkillRuntime>,
176 session_journal: Arc<dyn AgentSessionJournalStore>,
177 token_meter: Arc<dyn ModelTokenMeter>,
178 ) -> Result<Self, AgentProtocolError> {
179 Self::build(
180 backend,
181 config,
182 None,
183 Some(skills),
184 session_journal,
185 token_meter,
186 )
187 }
188
189 #[allow(clippy::too_many_arguments)]
193 pub fn new_with_tools_approval_skills_and_session_journal(
194 backend: Arc<dyn ModelBackend>,
195 config: GenericAgentConfig,
196 runtime: Arc<dyn AgentToolRuntime>,
197 run_grant: RunToolGrant,
198 approval_bridge: Arc<dyn AgentApprovalBridge>,
199 skills: Arc<SkillRuntime>,
200 session_journal: Arc<dyn AgentSessionJournalStore>,
201 token_meter: Arc<dyn ModelTokenMeter>,
202 ) -> Result<Self, AgentProtocolError> {
203 Self::build(
204 backend,
205 config,
206 Some(configure_tools(
207 runtime,
208 run_grant,
209 None,
210 Some(approval_bridge),
211 )?),
212 Some(skills),
213 session_journal,
214 token_meter,
215 )
216 }
217
218 fn build(
219 backend: Arc<dyn ModelBackend>,
220 config: GenericAgentConfig,
221 tools: Option<GenericTools>,
222 skills: Option<Arc<SkillRuntime>>,
223 session_journal: Arc<dyn AgentSessionJournalStore>,
224 token_meter: Arc<dyn ModelTokenMeter>,
225 ) -> Result<Self, AgentProtocolError> {
226 let model_descriptor = backend.descriptor();
227 model_descriptor.validate().map_err(model_protocol_error)?;
228 let token_meter_descriptor = token_meter.meter_descriptor();
229 token_meter_descriptor
230 .validate()
231 .map_err(model_protocol_error)?;
232 if (tools.is_some() || skills.is_some()) && !model_descriptor.capabilities.tool_calls {
233 return Err(AgentProtocolError::new(
234 AgentProtocolErrorCode::Unsupported,
235 "configured ModelBackend does not support model function calls",
236 ));
237 }
238 if let Some(conflict) = tools.as_ref().and_then(|tools| {
239 tools.model_definitions.iter().find_map(|definition| {
240 [SKILL_READ_TOOL_NAME, REQUEST_INPUT_TOOL_NAME]
241 .contains(&definition.name.as_str())
242 .then(|| definition.name.clone())
243 })
244 }) {
245 return Err(AgentProtocolError::new(
246 AgentProtocolErrorCode::InvalidSpec,
247 format!("reserved Generic Agent function name is already registered: {conflict}"),
248 ));
249 }
250 config.continuation.validate()?;
251 config
252 .model_retry
253 .validate()
254 .map_err(model_protocol_error)?;
255 if config.project_instructions.iter().any(|document| {
256 document.source.trim().is_empty()
257 || document.scope.trim().is_empty()
258 || document.content.trim().is_empty()
259 }) {
260 return Err(AgentProtocolError::new(
261 AgentProtocolErrorCode::InvalidSpec,
262 "project instructions require a source, directory scope, and nonempty content",
263 ));
264 }
265 if config.stream_buffer == 0
266 || config.history_limit == 0
267 || config.max_context_tokens == 0
268 || config.reserved_output_tokens >= config.max_context_tokens
269 {
270 return Err(AgentProtocolError::new(
271 AgentProtocolErrorCode::InvalidSpec,
272 "Generic Agent buffers and context limits must be non-zero",
273 ));
274 }
275 if let Some(policy) = &config.model_cost_policy {
276 policy.validate()?;
277 }
278 if config
279 .minimum_output_reserve_tokens
280 .is_some_and(|minimum| minimum == 0 || minimum > config.reserved_output_tokens)
281 {
282 return Err(AgentProtocolError::new(
283 AgentProtocolErrorCode::InvalidSpec,
284 "minimum_output_reserve_tokens must be positive and no greater than reserved_output_tokens",
285 ));
286 }
287 let has_tools = tools.is_some();
288 let has_input_requests =
289 config.input_requests_enabled && model_descriptor.capabilities.tool_calls;
290 let has_approval = tools
291 .as_ref()
292 .and_then(|tools| tools.approval_bridge.as_ref())
293 .is_some();
294 let mut supported_limits = BTreeSet::from([
295 RunLimitKind::Deadline,
296 RunLimitKind::ModelSteps,
297 RunLimitKind::InputTokens,
298 RunLimitKind::OutputTokens,
299 ]);
300 if has_tools {
301 supported_limits.insert(RunLimitKind::ToolCalls);
302 }
303 if config.model_cost_policy.is_some() {
304 supported_limits.insert(RunLimitKind::Cost);
305 }
306 let descriptor = AgentDescriptorEnvelope::seal(AgentDescriptor {
307 provider_id: config.provider_id.clone(),
308 agent_id: config.agent_id.clone(),
309 supported_protocol_versions: vec![AGENT_PROTOCOL_V1],
310 accepted_content_types: BTreeSet::from(["text/plain".to_owned()]),
311 capabilities: AgentCapabilities {
312 session_reuse: true,
313 structured_output: false,
314 controls: ControlCapabilities {
315 steer: true,
316 cancel: CancelSupport::Confirmed,
317 recover: true,
318 },
319 pending_request_kinds: {
320 let mut kinds = BTreeSet::new();
321 if has_input_requests {
322 kinds.insert(PendingRequestKind::Input);
323 }
324 if has_approval {
325 kinds.insert(PendingRequestKind::Approval);
326 }
327 kinds
328 },
329 supported_limits,
330 resources: skills
331 .as_ref()
332 .map(|_| {
333 vec![ResourceCapability {
334 kind: ResourceKind::new(
335 orchestral_core::skill_protocol::SKILL_CATALOG_RESOURCE_KIND_V1,
336 ),
337 modes: BTreeSet::from([ResourceBindingMode::Snapshot]),
338 max_bindings: Some(1),
339 }]
340 })
341 .unwrap_or_default(),
342 effect_mediation: if has_tools {
343 EffectMediation::HostMediated
344 } else {
345 EffectMediation::None
346 },
347 },
348 extensions:
349 orchestral_core::agent_protocol::wire::QueuedInputOperation::descriptor_extensions(),
350 })?;
351 let config_digest = generic_config_digest(
352 &config,
353 &model_descriptor,
354 &token_meter_descriptor,
355 tools.as_ref(),
356 skills.as_ref().map(|skills| skills.catalog()),
357 has_approval,
358 has_input_requests,
359 )?;
360 let context_engine = AgentSessionContextEngine::new(session_journal.clone(), token_meter);
361 Ok(Self {
362 inner: Arc::new(GenericInner {
363 backend,
364 descriptor,
365 config,
366 tools,
367 skills,
368 session_journal,
369 context_engine,
370 session_compactor: None,
371 checkpoint_store: Arc::new(InMemoryGenericAgentCheckpointStore::default()),
372 config_digest,
373 state: Mutex::new(GenericState::default()),
374 }),
375 })
376 }
377
378 pub(super) fn state(&self) -> MutexGuard<'_, GenericState> {
379 self.inner
380 .state
381 .lock()
382 .unwrap_or_else(std::sync::PoisonError::into_inner)
383 }
384
385 pub(super) fn stream_for(run: &GenericRun) -> AgentProviderStream {
386 let receiver = run.sender.subscribe();
387 let replay = run
388 .durable_events
389 .clone()
390 .into_iter()
391 .map(|draft| Ok(AgentProviderStreamItem::Event(Box::new(draft))));
392 let replay_stream = stream::iter(replay);
393 if run.terminal {
394 return replay_stream.boxed();
395 }
396 let live = stream::unfold(receiver, |mut receiver| async move {
397 match receiver.recv().await {
398 Ok(item) => Some((item, receiver)),
399 Err(broadcast::error::RecvError::Lagged(skipped)) => Some((
400 Err(AgentProtocolError::new(
401 AgentProtocolErrorCode::SequenceGap,
402 format!("Generic Agent stream subscriber lagged by {skipped}"),
403 )),
404 receiver,
405 )),
406 Err(broadcast::error::RecvError::Closed) => None,
407 }
408 });
409 replay_stream.chain(live).boxed()
410 }
411
412 pub(super) fn rejection(
413 code: AgentRejectionCode,
414 message: impl Into<String>,
415 ) -> AgentStartError {
416 AgentStartError::Rejected(AgentRejection::new(code, message))
417 }
418}