1use std::collections::HashMap;
4use std::sync::Arc;
5
6use awaken_runtime_contract::contract::commit_coordinator::CommitCoordinator;
7use awaken_runtime_contract::contract::run::{
8 RunInput, RunInputSnapshot, RunIntent, RunKind, RunOptions, RunTraceContext,
9};
10use awaken_runtime_contract::contract::storage::{RunRecord, RunRequestOrigin, StorageError};
11use awaken_runtime_contract::contract::suspension::ToolCallResume;
12use awaken_runtime_contract::contract::tool_intercept::{AdapterKind, RunMode};
13use awaken_runtime_contract::contract::{
14 inference::InferenceOverride, message::Message, tool::ToolDescriptor,
15};
16use futures::channel::mpsc;
17
18use crate::cancellation::CancellationToken;
19use crate::inbox::{InboxReceiver, InboxSender};
20use crate::loop_runner::PendingBoundaryHandler;
21use crate::registry::RegistrySet;
22use crate::resolution::Resolver;
23
24#[non_exhaustive]
26pub struct ThreadContextSnapshot {
27 pub messages: Vec<Message>,
28 pub latest_run: Option<RunRecord>,
29 pub run_cache: HashMap<String, RunRecord>,
30}
31
32impl ThreadContextSnapshot {
33 #[must_use]
34 pub fn new(
35 messages: Vec<Message>,
36 latest_run: Option<RunRecord>,
37 run_cache: HashMap<String, RunRecord>,
38 ) -> Self {
39 Self {
40 messages,
41 latest_run,
42 run_cache,
43 }
44 }
45}
46
47pub struct RunInbox {
49 pub sender: InboxSender,
50 pub receiver: InboxReceiver,
51}
52
53#[derive(Default)]
55pub struct RunControl {
56 pub cancellation_token: Option<CancellationToken>,
57 pub decision_rx: Option<mpsc::UnboundedReceiver<Vec<(String, ToolCallResume)>>>,
58 pub inbox: Option<RunInbox>,
59 pub pending_boundary: Option<Arc<dyn PendingBoundaryHandler>>,
60 pub seeded_decisions: Vec<(String, ToolCallResume)>,
61 pub commit_coordinator_override: Option<Arc<dyn CommitCoordinator>>,
66}
67
68#[derive(Default)]
73pub struct CaptureWiring {
74 pub thread_context_cache: Option<Arc<ThreadContextSnapshot>>,
75}
76
77#[derive(Default)]
88pub struct PersistenceHints {
89 pub is_continuation: bool,
90 pub messages_already_persisted: bool,
91 pub run_id_hint: Option<String>,
92 pub dispatch_id_hint: Option<String>,
93 pub resolution_id_hint: Option<String>,
95}
96
97#[derive(Default)]
102pub struct ResolverInheritance {
103 pub pinned_registry_set: Option<RegistrySet>,
104 pub run_resolver: Option<Arc<dyn Resolver>>,
105}
106
107pub struct RunActivation {
109 pub intent: RunIntent,
110 pub input: RunInput,
111 pub options: RunOptions,
112 pub trace: RunTraceContext,
113 pub control: RunControl,
114 pub capture: CaptureWiring,
117 pub persistence: PersistenceHints,
120 pub inherited: ResolverInheritance,
123}
124
125impl RunActivation {
126 #[must_use]
128 pub fn new(thread_id: impl Into<String>, messages: Vec<Message>) -> Self {
129 let thread_id = thread_id.into();
130 Self {
131 intent: RunIntent::new(thread_id),
132 input: RunInput::NewMessages(messages),
133 options: RunOptions::default(),
134 trace: RunTraceContext::default(),
135 control: RunControl::default(),
136 capture: CaptureWiring::default(),
137 persistence: PersistenceHints::default(),
138 inherited: ResolverInheritance::default(),
139 }
140 }
141
142 #[must_use]
143 pub fn thread_id(&self) -> &str {
144 &self.intent.thread_id
145 }
146
147 #[must_use]
148 pub fn messages(&self) -> &[Message] {
149 match &self.input {
150 RunInput::NewMessages(messages) => messages,
151 RunInput::AlreadyPersisted(_) => &[],
152 }
153 }
154
155 #[must_use]
156 pub fn messages_already_persisted(&self) -> bool {
157 self.persistence.messages_already_persisted
158 || matches!(self.input, RunInput::AlreadyPersisted(_))
159 }
160
161 #[must_use]
162 pub fn agent_id(&self) -> Option<&str> {
163 self.intent.agent_id.as_deref()
164 }
165
166 #[must_use]
167 pub fn run_id_hint(&self) -> Option<&str> {
168 self.persistence.run_id_hint.as_deref()
169 }
170
171 #[must_use]
172 pub fn dispatch_id_hint(&self) -> Option<&str> {
173 self.persistence.dispatch_id_hint.as_deref()
174 }
175
176 #[must_use]
177 pub fn resume_run_id(&self) -> Option<&str> {
178 match &self.intent.kind {
179 RunKind::HitlResume { run_id } | RunKind::ContinuationFromRun { run_id } => {
180 Some(run_id)
181 }
182 RunKind::NewIntent => None,
183 }
184 }
185
186 #[must_use]
187 pub fn with_agent_id(mut self, agent_id: impl Into<String>) -> Self {
188 self.intent.agent_id = Some(agent_id.into());
189 self
190 }
191
192 #[must_use]
193 pub fn with_overrides(mut self, overrides: InferenceOverride) -> Self {
194 self.options.overrides = Some(overrides);
195 self
196 }
197
198 #[must_use]
199 pub fn with_decisions(mut self, decisions: Vec<(String, ToolCallResume)>) -> Self {
200 self.control.seeded_decisions = decisions;
201 self
202 }
203
204 #[must_use]
205 pub fn with_frontend_tools(mut self, tools: Vec<ToolDescriptor>) -> Self {
206 self.options.frontend_tools = tools;
207 self
208 }
209
210 #[must_use]
211 pub fn with_legacy_origin(mut self, origin: RunRequestOrigin) -> Self {
212 self.trace.origin = origin.into();
213 self
214 }
215
216 #[must_use]
217 pub fn with_origin(self, origin: RunRequestOrigin) -> Self {
218 self.with_legacy_origin(origin)
219 }
220
221 #[must_use]
222 pub fn with_run_mode(mut self, run_mode: RunMode) -> Self {
223 self.trace.run_mode = run_mode;
224 self
225 }
226
227 #[must_use]
228 pub fn with_adapter(mut self, adapter: AdapterKind) -> Self {
229 self.trace.adapter = adapter;
230 self
231 }
232
233 #[must_use]
234 pub fn with_parent_run_id(mut self, parent_run_id: impl Into<String>) -> Self {
235 self.trace.parent_run_id = Some(parent_run_id.into());
236 self
237 }
238
239 #[must_use]
240 pub fn with_parent_thread_id(mut self, parent_thread_id: impl Into<String>) -> Self {
241 self.trace.parent_thread_id = Some(parent_thread_id.into());
242 self
243 }
244
245 #[must_use]
246 pub fn with_hitl_resume_run_id(mut self, run_id: impl Into<String>) -> Self {
247 self.intent.kind = RunKind::HitlResume {
248 run_id: run_id.into(),
249 };
250 self.persistence.is_continuation = true;
251 self.trace.run_mode = RunMode::Resume;
252 self
253 }
254
255 #[must_use]
256 pub fn with_continue_run_id(self, run_id: impl Into<String>) -> Self {
257 self.with_hitl_resume_run_id(run_id)
258 }
259
260 #[must_use]
261 pub fn with_continuation_run_id(mut self, run_id: impl Into<String>) -> Self {
262 self.intent.kind = RunKind::ContinuationFromRun {
263 run_id: run_id.into(),
264 };
265 self.persistence.is_continuation = true;
266 self
267 }
268
269 #[must_use]
270 pub fn with_dispatch_id(mut self, dispatch_id: impl Into<String>) -> Self {
271 self.trace.dispatch_id = Some(dispatch_id.into());
272 self
273 }
274
275 #[must_use]
276 pub fn with_trace_dispatch_id(self, dispatch_id: impl Into<String>) -> Self {
277 self.with_dispatch_id(dispatch_id)
278 }
279
280 #[must_use]
281 pub fn with_run_id_hint(mut self, run_id_hint: impl Into<String>) -> Self {
282 self.persistence.run_id_hint = Some(run_id_hint.into());
283 self
284 }
285
286 #[must_use]
287 pub fn with_dispatch_id_hint(mut self, dispatch_id_hint: impl Into<String>) -> Self {
288 self.persistence.dispatch_id_hint = Some(dispatch_id_hint.into());
289 self
290 }
291
292 #[must_use]
293 pub fn with_resolution_id_hint(mut self, resolution_id_hint: impl Into<String>) -> Self {
294 self.persistence.resolution_id_hint = Some(resolution_id_hint.into());
295 self
296 }
297
298 #[must_use]
299 pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
300 self.trace.session_id = Some(session_id.into());
301 self
302 }
303
304 #[must_use]
305 pub fn with_transport_request_id(mut self, id: impl Into<String>) -> Self {
306 self.trace.transport_request_id = Some(id.into());
307 self
308 }
309
310 #[must_use]
311 pub fn with_inbox(mut self, sender: InboxSender, receiver: InboxReceiver) -> Self {
312 self.control.inbox = Some(RunInbox { sender, receiver });
313 self
314 }
315
316 #[must_use]
317 pub fn with_pending_boundary_handler(
318 mut self,
319 handler: Arc<dyn PendingBoundaryHandler>,
320 ) -> Self {
321 self.control.pending_boundary = Some(handler);
322 self
323 }
324
325 #[must_use]
326 pub fn with_already_persisted_input(mut self, input: RunInputSnapshot) -> Self {
327 self.input = RunInput::AlreadyPersisted(input);
328 self.persistence.messages_already_persisted = true;
329 self
330 }
331
332 #[must_use]
333 pub fn with_messages_already_persisted(mut self, value: bool) -> Self {
334 self.persistence.messages_already_persisted = value;
335 self
336 }
337
338 #[must_use]
339 pub fn with_pinned_registry_set(mut self, registry_set: RegistrySet) -> Self {
340 self.inherited.pinned_registry_set = Some(registry_set);
341 self
342 }
343
344 #[must_use]
345 pub fn with_run_resolver(mut self, resolver: Arc<dyn Resolver>) -> Self {
346 self.inherited.run_resolver = Some(resolver);
347 self
348 }
349
350 #[must_use]
355 pub fn with_commit_coordinator_override(
356 mut self,
357 coordinator: Arc<dyn CommitCoordinator>,
358 ) -> Self {
359 self.control.commit_coordinator_override = Some(coordinator);
360 self
361 }
362
363 #[must_use]
364 pub fn with_thread_context_cache(mut self, cache: Arc<ThreadContextSnapshot>) -> Self {
365 self.capture.thread_context_cache = Some(cache);
366 self
367 }
368
369 pub fn validate(&self) -> Result<(), RunActivationError> {
375 self.intent.validate()?;
376 self.trace.validate()?;
377
378 match self.intent.kind {
379 RunKind::NewIntent if self.persistence.is_continuation => {
380 return Err(RunActivationError::Validation(
381 "persistence.is_continuation requires a resume or continuation run kind"
382 .to_string(),
383 ));
384 }
385 RunKind::HitlResume { .. } | RunKind::ContinuationFromRun { .. }
386 if !self.persistence.is_continuation =>
387 {
388 return Err(RunActivationError::Validation(
389 "resume and continuation run kinds require persistence.is_continuation"
390 .to_string(),
391 ));
392 }
393 _ => {}
394 }
395
396 if let RunInput::AlreadyPersisted(snapshot) = &self.input {
397 snapshot.validate()?;
398 if snapshot.thread_id != self.intent.thread_id {
399 return Err(RunActivationError::Validation(format!(
400 "run activation intent.thread_id '{}' must match input.thread_id '{}'",
401 self.intent.thread_id, snapshot.thread_id
402 )));
403 }
404 }
405
406 for (idx, (call_id, _)) in self.control.seeded_decisions.iter().enumerate() {
407 require_non_empty(
408 &format!("run activation seeded_decisions[{idx}].call_id"),
409 call_id,
410 )?;
411 }
412 require_optional_non_empty(
413 "run activation run_id_hint",
414 self.persistence.run_id_hint.as_deref(),
415 )?;
416 require_optional_non_empty(
417 "run activation dispatch_id_hint",
418 self.persistence.dispatch_id_hint.as_deref(),
419 )?;
420 Ok(())
421 }
422}
423
424#[derive(Debug, thiserror::Error)]
425pub enum RunActivationError {
426 #[error("run activation is missing thread_id")]
427 MissingThreadId,
428 #[error("run activation validation failed: {0}")]
429 Validation(String),
430 #[error(transparent)]
431 Contract(#[from] StorageError),
432}
433
434fn require_non_empty(field: &str, value: &str) -> Result<(), RunActivationError> {
435 if value.trim().is_empty() {
436 return Err(RunActivationError::Validation(format!(
437 "{field} must not be empty"
438 )));
439 }
440 Ok(())
441}
442
443fn require_optional_non_empty(field: &str, value: Option<&str>) -> Result<(), RunActivationError> {
444 if let Some(value) = value {
445 require_non_empty(field, value)?;
446 }
447 Ok(())
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::resolution::{ResolutionRequest, ResolveError, ResolvedRunPlan};
454
455 fn assert_send_static<T: Send + 'static>(_: T) {}
456
457 struct NoopResolver;
458
459 #[async_trait::async_trait]
460 impl Resolver for NoopResolver {
461 async fn resolve(
462 &self,
463 _request: ResolutionRequest,
464 ) -> Result<ResolvedRunPlan, ResolveError> {
465 Err(ResolveError::Runtime("noop".into()))
466 }
467 }
468
469 struct StubCoordinator;
470
471 #[async_trait::async_trait]
472 impl CommitCoordinator for StubCoordinator {
473 fn scope(
474 &self,
475 ) -> awaken_runtime_contract::contract::commit_coordinator::TransactionScopeId {
476 awaken_runtime_contract::contract::commit_coordinator::TransactionScopeId::new("test")
477 .unwrap()
478 }
479 fn reader(
480 &self,
481 ) -> Arc<dyn awaken_runtime_contract::contract::storage::RuntimeCheckpointStore> {
482 unreachable!("routing test does not commit")
483 }
484 async fn commit_checkpoint(
485 &self,
486 _plan: awaken_runtime_contract::contract::commit_coordinator::ThreadCommit,
487 ) -> Result<
488 awaken_runtime_contract::contract::commit_coordinator::ThreadCommitOutcome,
489 awaken_runtime_contract::contract::commit_coordinator::CommitError,
490 > {
491 unreachable!("routing test does not commit")
492 }
493 }
494
495 #[test]
496 fn activation_is_owned_send_static() {
497 let activation = RunActivation::new("thread", vec![Message::user("hi")]);
498 assert_send_static(activation);
499 }
500
501 #[test]
502 fn activation_validation_rejects_blank_identity_fields() {
503 let activation = RunActivation::new("thread", vec![Message::user("hi")])
504 .with_agent_id(" ")
505 .with_run_id_hint("run-1");
506
507 let err = activation.validate().unwrap_err();
508 assert!(err.to_string().contains("agent_id"));
509 }
510
511 #[test]
512 fn activation_validation_rejects_mismatched_persisted_input_thread() {
513 let activation = RunActivation::new("thread-a", Vec::new()).with_already_persisted_input(
514 RunInputSnapshot {
515 thread_id: "thread-b".into(),
516 ..RunInputSnapshot::default()
517 },
518 );
519
520 let err = activation.validate().unwrap_err();
521 assert!(err.to_string().contains("intent.thread_id"));
522 }
523
524 #[test]
525 fn activation_validation_rejects_orphaned_continuation_hint() {
526 let mut activation = RunActivation::new("thread", Vec::new());
527 activation.persistence.is_continuation = true;
528
529 let err = activation.validate().unwrap_err();
530 assert!(err.to_string().contains("is_continuation"));
531 }
532
533 #[test]
534 fn hitl_resume_builder_sets_continuation_hint() {
535 let activation = RunActivation::new("thread", Vec::new()).with_hitl_resume_run_id("run-1");
536
537 assert!(activation.persistence.is_continuation);
538 activation.validate().unwrap();
539 }
540
541 #[test]
542 fn activation_validation_rejects_resume_without_continuation_hint() {
543 let mut activation = RunActivation::new("thread", Vec::new());
544 activation.intent.kind = RunKind::HitlResume {
545 run_id: "run-1".into(),
546 };
547
548 let err = activation.validate().unwrap_err();
549 assert!(err.to_string().contains("is_continuation"));
550 }
551
552 #[test]
558 fn builder_methods_route_to_correct_wiring_sub_struct() {
559 use crate::registry::RegistrySet;
560 use crate::registry::memory::{
561 MapAgentSpecRegistry, MapModelRegistry, MapPluginSource, MapProviderRegistry,
562 MapToolRegistry,
563 };
564
565 let coordinator: Arc<dyn CommitCoordinator> = Arc::new(StubCoordinator);
566 let cache = Arc::new(ThreadContextSnapshot::new(
567 Vec::new(),
568 None,
569 Default::default(),
570 ));
571 let registry_set = RegistrySet {
572 agents: Arc::new(MapAgentSpecRegistry::new()),
573 tools: Arc::new(MapToolRegistry::new()),
574 models: Arc::new(MapModelRegistry::new()),
575 providers: Arc::new(MapProviderRegistry::new()),
576 plugins: Arc::new(MapPluginSource::new()),
577 #[cfg(feature = "a2a")]
578 backends: Arc::new(crate::registry::memory::MapBackendRegistry::new()),
579 };
580
581 let activation = RunActivation::new("thread", vec![Message::user("hi")])
582 .with_commit_coordinator_override(Arc::clone(&coordinator))
583 .with_thread_context_cache(Arc::clone(&cache))
584 .with_run_id_hint("hinted-run-id")
585 .with_dispatch_id_hint("hinted-dispatch-id")
586 .with_resolution_id_hint("hinted-resolution-id")
587 .with_continuation_run_id("parent-run")
588 .with_messages_already_persisted(true)
589 .with_pinned_registry_set(registry_set)
590 .with_run_resolver(Arc::new(NoopResolver));
591
592 assert!(
594 activation.control.commit_coordinator_override.is_some(),
595 "with_commit_coordinator_override routes to RunControl"
596 );
597 assert!(
599 activation.capture.thread_context_cache.is_some(),
600 "with_thread_context_cache routes to CaptureWiring"
601 );
602 assert_eq!(
606 activation.persistence.run_id_hint.as_deref(),
607 Some("hinted-run-id"),
608 "with_run_id_hint routes to PersistenceHints"
609 );
610 assert_eq!(
611 activation.persistence.dispatch_id_hint.as_deref(),
612 Some("hinted-dispatch-id"),
613 "with_dispatch_id_hint routes to PersistenceHints"
614 );
615 assert_eq!(
616 activation.persistence.resolution_id_hint.as_deref(),
617 Some("hinted-resolution-id"),
618 "with_resolution_id_hint routes to PersistenceHints"
619 );
620 assert!(
621 activation.persistence.is_continuation,
622 "with_continuation_run_id sets PersistenceHints::is_continuation"
623 );
624 assert!(
625 activation.persistence.messages_already_persisted,
626 "with_messages_already_persisted routes to PersistenceHints"
627 );
628
629 assert!(
631 activation.inherited.pinned_registry_set.is_some(),
632 "with_pinned_registry_set routes to ResolverInheritance"
633 );
634 assert!(
635 activation.inherited.run_resolver.is_some(),
636 "with_run_resolver routes to ResolverInheritance"
637 );
638
639 let neutral = RunActivation::new("t", Vec::new()).with_run_id_hint("x");
642 assert!(neutral.control.commit_coordinator_override.is_none());
643 assert!(neutral.inherited.pinned_registry_set.is_none());
644 assert!(neutral.inherited.run_resolver.is_none());
645 assert_eq!(neutral.persistence.run_id_hint.as_deref(), Some("x"));
646 }
647}