1use super::services::*;
2use super::types::*;
3use crate::{
4 flow::FlowStack,
5 state::State,
6 tool::{ToolContext, ToolDispatcher},
7};
8use futures_util::FutureExt;
9use serde_json::Value;
10use std::panic::AssertUnwindSafe;
11use std::{
12 collections::{BTreeMap, BTreeSet, HashMap},
13 sync::{
14 Arc,
15 atomic::{AtomicU8, Ordering},
16 },
17};
18use tokio_util::sync::CancellationToken;
19
20const MAX_TASKS: usize = 64;
21const MAX_OPERATIONS: usize = 2048;
22const QUEUED: u8 = 0;
23const STARTED: u8 = 1;
24const REVOKED: u8 = 2;
25
26#[derive(Clone)]
27struct Receipt {
28 result: Value,
29 writes: BTreeMap<String, Option<Value>>,
30 already_applied: bool,
31}
32
33struct Task {
34 id: TaskId,
35 parent: Option<TaskId>,
36 skill: Arc<CompiledSkill>,
37 revision: u64,
38 status: TaskStatus,
39 state: State,
40 flow: Option<FlowStack>,
41 pending: Option<OperationId>,
42 output: Option<Value>,
43 deferred: Vec<DeferredEvent>,
44 services: Option<ServiceState>,
45}
46enum DeferredEvent {
47 Turn,
48 Interrupted,
49}
50#[derive(Clone)]
51struct ExecutionControl {
52 cancel: CancellationToken,
53 permit: Arc<AtomicU8>,
54}
55
56struct Operation {
57 snapshot: OperationSnapshot,
58 commit: bool,
59 control: ExecutionControl,
60 writes: BTreeMap<String, Option<Value>>,
61 baseline: State,
62 receipt_observed: bool,
63}
64
65pub struct OwnedInvocation {
68 owner: TaskOwner,
69 tool: String,
70 args: Value,
71 state: State,
72 dispatcher: ToolDispatcher,
73 control: ExecutionControl,
74 cached: Option<Receipt>,
75 scope: Arc<()>,
76}
77#[derive(Clone)]
80pub struct InvocationCompletion {
81 owner: TaskOwner,
82 state: State,
83 result: Result<Value, String>,
84 before: HashMap<String, Value>,
85 cached_writes: Option<BTreeMap<String, Option<Value>>>,
86 already_applied: bool,
87 scope: Arc<()>,
88}
89impl OwnedInvocation {
90 pub fn operation_id(&self) -> &OperationId {
92 &self.owner.operation
93 }
94 pub fn owner(&self) -> &TaskOwner {
96 &self.owner
97 }
98 pub async fn execute(self) -> InvocationCompletion {
100 let before = self.state.to_hashmap();
101 let cached_writes = self.cached.as_ref().map(|receipt| receipt.writes.clone());
102 let already_applied = self
103 .cached
104 .as_ref()
105 .is_some_and(|receipt| receipt.already_applied);
106 let result = if self
107 .control
108 .permit
109 .compare_exchange(QUEUED, STARTED, Ordering::AcqRel, Ordering::Acquire)
110 .is_err()
111 {
112 Err("operation cancelled before execution".into())
113 } else if let Some(receipt) = self.cached {
114 for (key, value) in receipt.writes.into_iter().filter(|_| !already_applied) {
115 match value {
116 Some(value) => {
117 let _ = self.state.set(&key, value);
118 }
119 None => {
120 self.state.remove(&key);
121 }
122 }
123 }
124 Ok(receipt.result)
125 } else {
126 let ctx = ToolContext::new(self.state.clone())
127 .with_call_id(self.owner.operation.0.clone())
128 .with_cancel(self.control.cancel)
129 .with_task(self.owner.clone());
130 match AssertUnwindSafe(self.dispatcher.call_function_in(&self.tool, self.args, ctx))
131 .catch_unwind()
132 .await
133 {
134 Ok(result) => result.map_err(|error| error.to_string()),
135 Err(_) => Err("tool panicked before returning a receipt".into()),
136 }
137 };
138 InvocationCompletion {
139 owner: self.owner,
140 state: self.state,
141 result,
142 before,
143 cached_writes,
144 already_applied,
145 scope: self.scope,
146 }
147 }
148}
149
150pub struct TaskRuntime {
154 skills: BTreeMap<String, Arc<CompiledSkill>>,
155 tasks: BTreeMap<TaskId, Task>,
156 operations: BTreeMap<OperationId, Operation>,
157 foreground: Option<TaskId>,
158 next_task: u64,
159 next_operation: u64,
160 next_service: u64,
161 ready_services: Vec<OwnedTaskServiceWork>,
162 service_effects: Vec<AcceptedTaskEffect>,
163 clock: crate::clock::SharedClock,
164 scope: Arc<()>,
165}
166impl TaskRuntime {
167 pub fn new(skills: Vec<CompiledSkill>) -> Result<Self, TaskError> {
169 let mut catalog = BTreeMap::new();
170 for skill in skills {
171 validate_skill_identity(&skill.name, &skill.version)?;
172 let mut names = BTreeSet::new();
173 for tool in &skill.tools {
174 tool.validate_for_skill(&skill.name)?;
175 if !names.insert(&tool.name) {
176 return Err(err(format!(
177 "skill '{}' has duplicate tool names",
178 skill.name
179 )));
180 }
181 }
182 if let Some(code) = &skill.code {
183 if let Some(missing) = code
184 .function_list()
185 .iter()
186 .find(|f| !skill.tools.iter().any(|t| t.name == f.name))
187 {
188 return Err(err(format!(
189 "skill '{}' code tool exposes '{}', which is not one of its tools",
190 skill.name, missing.name
191 )));
192 }
193 if skill.name.len() + 2 + code.tool().len() > 64 {
194 return Err(err(format!(
195 "skill '{}' code tool name '{}__{}' must fit 64 characters",
196 skill.name,
197 skill.name,
198 code.tool()
199 )));
200 }
201 if skill.tools.iter().any(|t| t.name == code.tool()) {
202 return Err(err(format!(
203 "skill '{}' code tool '{}' is also one of its tools",
204 skill.name,
205 code.tool()
206 )));
207 }
208 }
209 let name = skill.name.clone();
210 if catalog.insert(name.clone(), Arc::new(skill)).is_some() {
211 return Err(err(format!("duplicate installed skill '{name}'")));
212 }
213 }
214 Ok(Self {
215 skills: catalog,
216 tasks: BTreeMap::new(),
217 operations: BTreeMap::new(),
218 foreground: None,
219 next_task: 1,
220 next_operation: 1,
221 next_service: 1,
222 ready_services: Vec::new(),
223 service_effects: Vec::new(),
224 clock: Arc::new(crate::clock::SystemClock),
225 scope: Arc::new(()),
226 })
227 }
228
229 pub fn with_clock(mut self, clock: crate::clock::SharedClock) -> Self {
231 self.clock = clock;
232 self
233 }
234
235 pub fn command(&mut self, command: TaskCommand) -> Result<Option<OwnedInvocation>, TaskError> {
238 match command {
239 TaskCommand::Start {
240 skill,
241 input,
242 parent,
243 } => self.start(&skill, input, parent)?,
244 TaskCommand::Suspend { task } => {
245 self.require_live(&task)?;
246 self.suspend(&task);
247 }
248 TaskCommand::Resume { task } => {
249 self.require_live(&task)?;
250 self.focus(&task);
251 }
252 TaskCommand::Revise {
253 task,
254 expected_revision,
255 input,
256 } => self.revise(&task, expected_revision, input)?,
257 TaskCommand::Cancel { task } => self.cancel(&task)?,
258 TaskCommand::Complete { task, output } => self.finish_task(&task, output)?,
259 TaskCommand::Invoke {
260 task,
261 tool,
262 args,
263 idempotency_key,
264 } => return self.invoke(&task, &tool, args, idempotency_key),
265 TaskCommand::Decide { operation, approve } => return self.decide(&operation, approve),
266 TaskCommand::Reconcile { operation, outcome } => self.reconcile(&operation, outcome)?,
267 }
268 Ok(None)
269 }
270
271 fn start(&mut self, name: &str, input: Value, parent: Option<TaskId>) -> Result<(), TaskError> {
272 if self.tasks.len() >= MAX_TASKS {
273 return Err(err("session task limit reached"));
274 }
275 if let Some(parent) = &parent {
276 self.require_live(parent)?;
277 }
278 let skill = self
279 .skills
280 .get(name)
281 .cloned()
282 .ok_or_else(|| err(format!("unknown skill '{name}'")))?;
283 let (state, flow) = instantiate(&skill, input)?;
284 state.set_clock(self.clock.clone());
285 let services = ServiceState::create(&skill.services, &state).map_err(err)?;
286 let id = TaskId(format!("task-{}", self.next_task));
287 self.next_task += 1;
288 self.tasks.insert(
289 id.clone(),
290 Task {
291 id: id.clone(),
292 parent,
293 skill,
294 revision: 1,
295 status: TaskStatus::Suspended,
296 state,
297 flow,
298 pending: None,
299 output: None,
300 deferred: Vec::new(),
301 services,
302 },
303 );
304 self.focus(&id);
305 Ok(())
306 }
307 fn require_live(&self, id: &TaskId) -> Result<&Task, TaskError> {
308 let task = self
309 .tasks
310 .get(id)
311 .ok_or_else(|| err(format!("unknown task '{}'", id.0)))?;
312 if !matches!(task.status, TaskStatus::Running | TaskStatus::Suspended) {
313 return Err(err(format!("task '{}' is terminal", id.0)));
314 }
315 Ok(task)
316 }
317 fn suspend(&mut self, id: &TaskId) {
318 self.invalidate_approval(id, "approval invalidated by suspension");
319 if let Some(task) = self.tasks.get_mut(id) {
320 if let Some(services) = &mut task.services {
321 services.advance_clock(
322 &task.state,
323 task.status == TaskStatus::Running && services.pending.is_none(),
324 );
325 }
326 task.status = TaskStatus::Suspended;
327 }
328 if self.foreground.as_ref() == Some(id) {
329 self.foreground = None;
330 }
331 }
332 fn focus(&mut self, id: &TaskId) {
333 if self.foreground.as_ref() == Some(id) {
334 return;
335 }
336 if let Some(previous) = self.foreground.clone() {
337 self.suspend(&previous);
338 }
339 if let Some(task) = self.tasks.get_mut(id) {
340 if let Some(services) = &mut task.services {
341 services.advance_clock(&task.state, false);
342 if services.plan.memory.is_some()
343 && !services
344 .queue
345 .iter()
346 .any(|event| matches!(event, ServiceEvent::Project))
347 {
348 services.queue.push_back(ServiceEvent::Project);
349 }
350 }
351 task.status = TaskStatus::Running;
352 }
353 self.foreground = Some(id.clone());
354 self.drive_services_for(id);
355 }
356 fn invalidate_approval(&mut self, id: &TaskId, reason: &str) {
357 let pending = self.tasks.get(id).and_then(|task| task.pending.clone());
358 if let Some(pending) = pending
359 && let Some(operation) = self.operations.get_mut(&pending)
360 && operation.snapshot.status == OperationStatus::AwaitingApproval
361 {
362 operation.snapshot.status = OperationStatus::Declined;
363 operation.snapshot.error = Some(reason.into());
364 self.tasks.get_mut(id).expect("task owns proposal").pending = None;
365 }
366 }
367 fn invalidate_work(&mut self, id: &TaskId) {
368 if let Some(services) = self
369 .tasks
370 .get_mut(id)
371 .and_then(|task| task.services.as_mut())
372 {
373 services.invalidate();
374 }
375 self.invalidate_approval(id, "approval invalidated by cancellation or revision");
376 let pending = self.tasks.get_mut(id).and_then(|task| task.pending.take());
377 if let Some(pending) = pending
378 && let Some(operation) = self.operations.get_mut(&pending)
379 {
380 operation.snapshot.cancellation_requested = true;
381 let revoked = operation
382 .control
383 .permit
384 .compare_exchange(QUEUED, REVOKED, Ordering::AcqRel, Ordering::Acquire)
385 .is_ok();
386 if !operation.commit || revoked {
387 operation.control.cancel.cancel();
388 operation.snapshot.status = OperationStatus::Cancelled;
389 }
390 }
391 }
392 fn revise(&mut self, id: &TaskId, revision: u64, input: Value) -> Result<(), TaskError> {
393 let task = self.require_live(id)?;
394 if task.revision != revision {
395 return Err(err("task revision changed"));
396 }
397 let (state, flow) = instantiate(&task.skill, input)?;
398 state.set_clock(self.clock.clone());
399 let services = ServiceState::create(&task.skill.services, &state).map_err(err)?;
400 self.invalidate_work(id);
401 let task = self.tasks.get_mut(id).expect("validated task");
402 task.revision += 1;
403 task.state = state;
404 task.flow = flow;
405 task.services = services;
406 task.output = None;
407 task.deferred.clear();
408 self.drive_services_for(id);
409 Ok(())
410 }
411 fn cancel(&mut self, id: &TaskId) -> Result<(), TaskError> {
412 let task = self.tasks.get(id).ok_or_else(|| err("unknown task"))?;
413 if task.status == TaskStatus::Cancelled {
414 return Ok(());
415 }
416 self.require_live(id)?;
417 self.invalidate_work(id);
418 let task = self.tasks.get_mut(id).expect("validated task");
419 task.revision += 1;
420 task.status = TaskStatus::Cancelled;
421 task.deferred.clear();
422 self.resume_parent(id);
423 Ok(())
424 }
425 fn finish_task(&mut self, id: &TaskId, output: Value) -> Result<(), TaskError> {
426 let task = self.require_live(id)?;
427 if task
428 .services
429 .as_ref()
430 .is_some_and(|s| s.pending.is_some() || !s.queue.is_empty())
431 {
432 return Err(err("task has pending services"));
433 }
434 if task.pending.is_some() {
435 return Err(err("task has a pending operation"));
436 }
437 if task.flow.as_ref().is_some_and(|flow| !flow.is_complete()) {
438 return Err(err("task governance has not completed"));
439 }
440 if self.operations.values().any(|op| {
441 op.snapshot.owner.task == *id
442 && matches!(
443 op.snapshot.status,
444 OperationStatus::Running | OperationStatus::Unknown
445 )
446 }) {
447 return Err(err("task has an unresolved external operation"));
448 }
449 let output = match &task.skill.project_output {
450 Some(project) => project(&task.state).map_err(err)?,
451 None => output,
452 };
453 (task.skill.validate_output)(&output).map_err(err)?;
454 let task = self.tasks.get_mut(id).expect("validated task");
455 task.output = Some(output);
456 task.status = TaskStatus::Completed;
457 self.resume_parent(id);
458 Ok(())
459 }
460 fn resume_parent(&mut self, id: &TaskId) {
461 if self.foreground.as_ref() != Some(id) {
462 return;
463 }
464 self.foreground = None;
465 let parent = self.tasks.get(id).and_then(|task| task.parent.clone());
466 if let Some(parent) = parent
467 && self
468 .tasks
469 .get(&parent)
470 .is_some_and(|task| task.status == TaskStatus::Suspended)
471 {
472 self.focus(&parent);
473 }
474 }
475
476 fn invoke(
477 &mut self,
478 id: &TaskId,
479 name: &str,
480 mut args: Value,
481 key: Option<String>,
482 ) -> Result<Option<OwnedInvocation>, TaskError> {
483 if self.operations.len() >= MAX_OPERATIONS {
484 return Err(err("session operation limit reached"));
485 }
486 let task = self.require_live(id)?;
487 if self.foreground.as_ref() != Some(id) {
488 return Err(err("only the foreground task admits new operations"));
489 }
490 if task
491 .services
492 .as_ref()
493 .is_some_and(|s| s.pending.is_some() || !s.queue.is_empty())
494 {
495 return Err(err("task has pending services"));
496 }
497 if task.pending.is_some() {
498 return Err(err("task already has a pending operation"));
499 }
500 let tool = task
501 .skill
502 .tools
503 .iter()
504 .find(|tool| tool.name == name)
505 .ok_or_else(|| err(format!("unknown task tool '{name}'")))?;
506 let mut cached = None;
507 let key = match &tool.effect {
508 TaskToolEffect::Read => {
509 if key.is_some() {
510 return Err(err("idempotency key is only valid for commit tools"));
511 }
512 None
513 }
514 TaskToolEffect::Commit {
515 idempotency_argument,
516 } => {
517 if self.operations.values().any(|operation| {
518 operation.commit
519 && operation.snapshot.owner.task == *id
520 && matches!(
521 operation.snapshot.status,
522 OperationStatus::Running | OperationStatus::Unknown
523 )
524 }) {
525 return Err(err(
526 "task has an unresolved commit; await or reconcile its outcome before another commit",
527 ));
528 }
529 let object = args
530 .as_object_mut()
531 .ok_or_else(|| err("commit arguments must be an object"))?;
532 let argument_key = object
533 .get(idempotency_argument)
534 .and_then(Value::as_str)
535 .map(str::to_owned);
536 let key = match (key, argument_key) {
537 (Some(key), Some(arg)) if key != arg => {
538 return Err(err("idempotency key disagrees with the tool argument"));
539 }
540 (Some(key), _) | (None, Some(key)) if !key.trim().is_empty() => key,
541 _ => return Err(err("commit requires a nonempty idempotency key")),
542 };
543 object.insert(idempotency_argument.clone(), Value::String(key.clone()));
544 for previous in self.operations.values() {
545 let owner_skill = &self.tasks[&previous.snapshot.owner.task].skill.name;
546 if owner_skill == &task.skill.name
547 && previous.snapshot.tool == name
548 && previous.snapshot.idempotency_key.as_ref() == Some(&key)
549 {
550 if previous.snapshot.args != args {
551 return Err(err(
552 "idempotency key was already used with different arguments",
553 ));
554 }
555 match previous.snapshot.status {
556 OperationStatus::Succeeded => {
557 let already_applied = cached
558 .as_ref()
559 .is_some_and(|receipt: &Receipt| receipt.already_applied)
560 || (previous.snapshot.owner.task == *id
561 && previous.snapshot.owner.revision == task.revision);
562 cached = previous.snapshot.result.clone().map(|result| Receipt {
563 result,
564 writes: previous.writes.clone(),
565 already_applied,
566 });
567 }
568 OperationStatus::AwaitingApproval
569 | OperationStatus::Running
570 | OperationStatus::Unknown => {
571 return Err(err(
572 "operation key has a pending or unknown outcome; reconcile before retrying",
573 ));
574 }
575 _ => {}
576 }
577 }
578 }
579 Some(key)
580 }
581 };
582 if cached.is_none()
583 && let Some(flow) = &task.flow
584 {
585 flow.admits_tool(name, &task.state).map_err(err)?;
586 }
587 let operation_id = OperationId(format!("operation-{}", self.next_operation));
588 let owner = TaskOwner {
589 task: id.clone(),
590 revision: task.revision,
591 operation: operation_id.clone(),
592 };
593 let commit = matches!(tool.effect, TaskToolEffect::Commit { .. });
594 let approval = commit && cached.is_none();
595 let control = ExecutionControl {
596 cancel: CancellationToken::new(),
597 permit: Arc::new(AtomicU8::new(QUEUED)),
598 };
599 let invocation = if approval {
600 None
601 } else {
602 Some(make_invocation(
603 task,
604 owner.clone(),
605 name,
606 args.clone(),
607 control.clone(),
608 cached,
609 self.scope.clone(),
610 )?)
611 };
612 let baseline = independent_state(&task.state);
613 self.next_operation += 1;
614 self.operations.insert(
615 operation_id.clone(),
616 Operation {
617 snapshot: OperationSnapshot {
618 id: operation_id.clone(),
619 owner,
620 tool: name.into(),
621 args,
622 idempotency_key: key,
623 status: if approval {
624 OperationStatus::AwaitingApproval
625 } else {
626 OperationStatus::Running
627 },
628 cancellation_requested: false,
629 result: None,
630 error: None,
631 },
632 commit,
633 control,
634 writes: BTreeMap::new(),
635 baseline,
636 receipt_observed: false,
637 },
638 );
639 self.tasks.get_mut(id).expect("validated task").pending = Some(operation_id);
640 Ok(invocation)
641 }
642 fn decide(
643 &mut self,
644 id: &OperationId,
645 approve: bool,
646 ) -> Result<Option<OwnedInvocation>, TaskError> {
647 let operation = self
648 .operations
649 .get(id)
650 .ok_or_else(|| err("unknown approval operation"))?;
651 if operation.snapshot.status != OperationStatus::AwaitingApproval {
652 return Err(err("approval is no longer pending"));
653 }
654 let owner = operation.snapshot.owner.clone();
655 let task = self.require_live(&owner.task)?;
656 if task.revision != owner.revision
657 || task.pending.as_ref() != Some(id)
658 || self.foreground.as_ref() != Some(&owner.task)
659 {
660 return Err(err("approval owner is no longer current"));
661 }
662 if !approve {
663 let operation = self.operations.get_mut(id).expect("validated proposal");
664 operation.snapshot.status = OperationStatus::Declined;
665 operation.snapshot.error = Some("user declined the operation".into());
666 self.tasks
667 .get_mut(&owner.task)
668 .expect("validated task")
669 .pending = None;
670 return Ok(None);
671 }
672 if task
673 .services
674 .as_ref()
675 .is_some_and(|s| s.pending.is_some() || !s.queue.is_empty())
676 {
677 return Err(err("task has pending services before approval"));
678 }
679 if let Some(flow) = &task.flow {
680 flow.admits_tool(&operation.snapshot.tool, &task.state)
681 .map_err(err)?;
682 }
683 let invocation = make_invocation(
684 task,
685 owner,
686 &operation.snapshot.tool,
687 operation.snapshot.args.clone(),
688 operation.control.clone(),
689 None,
690 self.scope.clone(),
691 )?;
692 let operation = self.operations.get_mut(id).expect("validated proposal");
693 operation.snapshot.status = OperationStatus::Running;
694 operation.baseline = independent_state(&invocation.state);
695 Ok(Some(invocation))
696 }
697
698 pub fn complete(&mut self, completion: InvocationCompletion) -> TaskDelivery {
701 let InvocationCompletion {
702 owner,
703 state,
704 result,
705 before,
706 cached_writes,
707 already_applied,
708 scope,
709 } = completion;
710 let mut delivery = TaskDelivery {
711 operation_id: owner.operation.clone(),
712 task_id: owner.task.clone(),
713 revision: owner.revision,
714 result: result.clone(),
715 foreground: false,
716 stale: true,
717 };
718 if !Arc::ptr_eq(&self.scope, &scope) {
719 return delivery;
720 }
721 let Some(operation) = self.operations.get_mut(&owner.operation) else {
722 return delivery;
723 };
724 if operation.receipt_observed
725 || operation.snapshot.owner != owner
726 || !matches!(
727 operation.snapshot.status,
728 OperationStatus::Running | OperationStatus::Unknown
729 )
730 {
731 return delivery;
732 }
733 operation.receipt_observed = true;
734 if result.is_ok() {
735 operation.writes =
736 cached_writes.unwrap_or_else(|| state_diff(&before, &state.to_hashmap()));
737 }
738 match &result {
739 Ok(value) => {
740 operation.snapshot.status = OperationStatus::Succeeded;
741 operation.snapshot.result = Some(value.clone());
742 operation.snapshot.error = None;
743 }
744 Err(error) => {
745 operation.snapshot.status = if operation.commit {
746 OperationStatus::Unknown
747 } else {
748 OperationStatus::Failed
749 };
750 operation.snapshot.error = Some(error.clone());
751 }
752 }
753 let Some(task) = self.tasks.get_mut(&owner.task) else {
754 return delivery;
755 };
756 if task.revision != owner.revision
757 || task.pending.as_ref() != Some(&owner.operation)
758 || !matches!(task.status, TaskStatus::Running | TaskStatus::Suspended)
759 {
760 return delivery;
761 }
762 let unknown_commit = operation.commit && result.is_err();
763 if !unknown_commit {
764 task.pending = None;
765 }
766 let service_before = task.state.to_hashmap();
767 if result.is_ok() {
768 task.state = independent_state(&state);
769 }
770 let service_owner = TaskOwner {
771 task: owner.task.clone(),
772 revision: owner.revision,
773 operation: OperationId(format!("service-{}", self.next_service)),
774 };
775 self.next_service += 1;
776 self.service_effects
777 .extend(settle_task_services(task, service_before, None, service_owner).0);
778 if let Some(flow) = &mut task.flow {
779 if result.is_ok() && !already_applied {
780 let _ = task.state.set(crate::flow::TOOL_RESULT_KEY, serde_json::json!({"tool": operation.snapshot.tool, "id": owner.operation.0, "ok": true}));
781 flow.observe_tool(&operation.snapshot.tool, true, &task.state);
782 } else if result.is_err() && !operation.commit {
783 flow.observe_tool(&operation.snapshot.tool, false, &task.state);
784 }
785 }
786 if !unknown_commit {
787 drain_turns(task);
788 }
789 if let Some(services) = &mut task.services {
790 if result.is_ok()
791 && services.plan.memory.is_some()
792 && matches!(
793 operation.snapshot.tool.as_str(),
794 "recall_context" | "manage_memory"
795 )
796 {
797 services.queue.push_back(ServiceEvent::Project);
798 }
799 services.queue.push_back(ServiceEvent::AfterTool {
800 tool: operation.snapshot.tool.clone(),
801 args: operation.snapshot.args.clone(),
802 result: result
803 .clone()
804 .unwrap_or_else(|error| serde_json::json!({"error":error})),
805 });
806 }
807 delivery.foreground = self.foreground.as_ref() == Some(&owner.task);
808 delivery.stale = false;
809 self.drive_services_for(&owner.task);
810 delivery
811 }
812 fn reconcile(&mut self, id: &OperationId, outcome: ReconciledOutcome) -> Result<(), TaskError> {
813 let operation = self
814 .operations
815 .get(id)
816 .ok_or_else(|| err("unknown operation"))?;
817 if operation.snapshot.status != OperationStatus::Unknown {
818 return Err(err("only an unknown outcome can be reconciled"));
819 }
820 let owner = operation.snapshot.owner.clone();
821 let tool = operation.snapshot.tool.clone();
822 let task = self
823 .tasks
824 .get(&owner.task)
825 .expect("operation retains its owner");
826 let current = task.revision == owner.revision
827 && task.pending.as_ref() == Some(id)
828 && matches!(task.status, TaskStatus::Running | TaskStatus::Suspended);
829 if current
830 && task
831 .services
832 .as_ref()
833 .is_some_and(|services| services.pending.is_some() || !services.queue.is_empty())
834 {
835 return Err(err("task has pending services before reconciliation"));
836 }
837 let state = independent_state(if current {
838 &task.state
839 } else {
840 &operation.baseline
841 });
842 let before = state.to_hashmap();
843 let application = match &outcome {
844 ReconciledOutcome::Succeeded { result } => task
845 .skill
846 .receipt_applier
847 .as_ref()
848 .map_or(Ok(()), |apply| apply(&tool, result, &state)),
849 ReconciledOutcome::Failed { .. } => Ok(()),
850 };
851 let succeeded = matches!(outcome, ReconciledOutcome::Succeeded { .. });
852 let operation = self.operations.get_mut(id).expect("validated operation");
853 match outcome {
854 ReconciledOutcome::Succeeded { result } => {
855 operation.snapshot.status = OperationStatus::Succeeded;
856 operation.snapshot.result = Some(result);
857 operation.snapshot.error = application.as_ref().err().map(|error| {
858 format!("external success verified; state reconstruction failed: {error}")
859 });
860 if application.is_ok() {
861 operation.writes = state_diff(&before, &state.to_hashmap());
862 }
863 }
864 ReconciledOutcome::Failed { error } => {
865 operation.snapshot.status = OperationStatus::Failed;
866 operation.snapshot.error = Some(error);
867 }
868 }
869 if current {
870 let task = self.tasks.get_mut(&owner.task).expect("validated task");
871 task.pending = None;
872 if application.is_ok() {
873 let before = task.state.to_hashmap();
874 if succeeded {
875 task.state = state;
876 }
877 let service_owner = TaskOwner {
878 task: owner.task.clone(),
879 revision: owner.revision,
880 operation: OperationId(format!("service-{}", self.next_service)),
881 };
882 self.next_service += 1;
883 self.service_effects
884 .extend(settle_task_services(task, before, None, service_owner).0);
885 if let Some(flow) = &mut task.flow {
886 flow.observe_tool(&tool, succeeded, &task.state);
887 }
888 drain_turns(task);
889 } else {
890 task.status = TaskStatus::Failed;
891 task.deferred.clear();
892 }
893 }
894 if current && application.is_err() {
895 self.resume_parent(&owner.task);
896 }
897 self.drive_services_for(&owner.task);
898 application.map_err(|error| {
899 err(format!(
900 "external outcome recorded, but task state reconstruction failed: {error}"
901 ))
902 })
903 }
904
905 pub fn catalog(&self) -> Vec<SkillSummary> {
907 self.skills
908 .values()
909 .map(|skill| SkillSummary {
910 key: SkillKey {
911 name: skill.name.clone(),
912 version: skill.version.clone(),
913 },
914 description: skill.description.clone(),
915 input_schema: skill.input_schema.clone(),
916 output_schema: skill.output_schema.clone(),
917 tools: skill.tools.clone(),
918 })
919 .collect()
920 }
921 pub fn snapshot(&self) -> TaskSessionSnapshot {
923 TaskSessionSnapshot {
924 foreground: self.foreground.clone(),
925 skills: self.catalog(),
926 tasks: self
927 .tasks
928 .values()
929 .map(|task| TaskSnapshot {
930 services_pending: task
931 .services
932 .as_ref()
933 .is_some_and(|s| s.pending.is_some() || !s.queue.is_empty()),
934 service_errors: task
935 .services
936 .as_ref()
937 .map(|s| s.errors.clone())
938 .unwrap_or_default(),
939 id: task.id.clone(),
940 parent: task.parent.clone(),
941 skill: SkillKey {
942 name: task.skill.name.clone(),
943 version: task.skill.version.clone(),
944 },
945 revision: task.revision,
946 status: task.status,
947 flow: task.flow.as_ref().map(|flow| flow.snapshot(&task.state)),
948 pending: task.pending.clone(),
949 output: task.output.clone(),
950 })
951 .collect(),
952 operations: self
953 .operations
954 .values()
955 .map(|operation| operation.snapshot.clone())
956 .collect(),
957 }
958 }
959 pub fn foreground_instruction(&self) -> Option<String> {
963 let brief = self.foreground_brief()?;
964 Some(match self.foreground_context() {
965 Some(context) if !context.is_empty() => format!("{brief}\n{context}"),
966 _ => brief,
967 })
968 }
969
970 pub fn foreground_brief(&self) -> Option<String> {
976 let task = self.tasks.get(self.foreground.as_ref()?)?;
977 Some(format!(
978 "Task {} / skill {}@{} / revision {}\n{}",
979 task.id.0, task.skill.name, task.skill.version, task.revision, task.skill.instruction
980 ))
981 }
982
983 #[cfg(all(test, feature = "code-mode"))]
985 pub(crate) fn set_skill_code(&mut self, name: &str, code: crate::code_mode::CodeMode) {
986 if let Some(skill) = self.skills.get_mut(name) {
987 Arc::make_mut(skill).code = Some(code);
988 }
989 }
990
991 pub fn skill_code(&self, name: &str) -> Option<&crate::code_mode::CodeMode> {
993 self.skills.get(name)?.code.as_ref()
994 }
995
996 pub fn foreground_offer(&self) -> Option<(String, Vec<String>)> {
1001 let task = self.tasks.get(self.foreground.as_ref()?)?;
1002 let tools = task
1003 .skill
1004 .tools
1005 .iter()
1006 .map(|tool| tool.name.clone())
1007 .filter(|name| {
1008 task.flow
1009 .as_ref()
1010 .is_none_or(|flow| flow.offers_tool(name, &task.state))
1011 })
1012 .collect();
1013 Some((task.skill.name.clone(), tools))
1014 }
1015
1016 pub fn foreground_context(&self) -> Option<String> {
1020 let task = self.tasks.get(self.foreground.as_ref()?)?;
1021 let refreshing = task
1022 .services
1023 .as_ref()
1024 .is_some_and(ServiceState::refreshing_memory);
1025 let visible = if refreshing {
1026 let state = independent_state(&task.state);
1027 if let Some(services) = &task.services {
1028 for (key, previous) in &services.memory_values {
1029 if state.get_raw(key).as_ref() == Some(previous) {
1030 state.remove(key);
1031 }
1032 }
1033 for computed in services.plan.computed.describe() {
1034 state.remove(&format!("derived:{}", computed.key));
1035 }
1036 services.plan.computed.recompute(&state);
1037 }
1038 Some(state)
1039 } else {
1040 None
1041 };
1042 let state = visible.as_ref().unwrap_or(&task.state);
1043 let mut parts = Vec::new();
1044 if let Some(services) = &task.services {
1045 if refreshing {
1046 parts.push(
1047 "Remembered context is refreshing; wait for task services before acting."
1048 .into(),
1049 );
1050 } else {
1051 parts.extend(services.contexts.clone());
1052 }
1053 let keys = services
1054 .plan
1055 .extractors
1056 .iter()
1057 .flat_map(|extractor| {
1058 if extractor.promotion_rules().is_empty() {
1059 state
1060 .get_raw(extractor.name())
1061 .and_then(|value| {
1062 value
1063 .as_object()
1064 .map(|fields| fields.keys().cloned().collect::<Vec<_>>())
1065 })
1066 .unwrap_or_default()
1067 } else {
1068 extractor
1069 .promotion_rules()
1070 .iter()
1071 .map(|rule| rule.state_key.clone())
1072 .collect::<Vec<_>>()
1073 }
1074 })
1075 .chain(services.memory_keys.iter().cloned())
1076 .chain(
1077 services
1078 .plan
1079 .computed
1080 .describe()
1081 .into_iter()
1082 .map(|c| format!("derived:{}", c.key)),
1083 );
1084 let values: BTreeMap<_, _> = keys
1085 .filter_map(|key| {
1086 state
1087 .get_raw(&key)
1088 .map(|value| (key.clone(), state.redact_value(&key, &value)))
1089 })
1090 .collect();
1091 if !values.is_empty() {
1092 parts.push(format!(
1093 "This task's authoritative extracted, computed and remembered values: {}",
1094 serde_json::json!(values)
1095 ));
1096 }
1097 }
1098 if let Some(flow) = &task.flow {
1099 parts.extend(flow.active_postures(state));
1100 parts.extend(flow.active_grounds(state));
1101 }
1102 if let Some(input) = state.get_raw("task:input") {
1103 parts.push(format!("Task input data: {}", state.redact_fields(&input)));
1104 }
1105 let results: Vec<_> = self
1106 .operations
1107 .values()
1108 .filter(|operation| {
1109 operation.snapshot.owner.task == task.id
1110 && operation.snapshot.owner.revision == task.revision
1111 && operation.snapshot.status == OperationStatus::Succeeded
1112 && !matches!(
1113 operation.snapshot.tool.as_str(),
1114 "recall_context" | "manage_memory"
1115 )
1116 })
1117 .filter_map(|operation| {
1118 operation.snapshot.result.as_ref().map(|result| {
1119 serde_json::json!({
1120 "operation": operation.snapshot.id,
1121 "tool": operation.snapshot.tool,
1122 "result": state.redact_fields(result),
1123 })
1124 })
1125 })
1126 .collect();
1127 if !results.is_empty() {
1128 parts.push(format!(
1129 "This task's completed operation data: {}",
1130 Value::Array(results)
1131 ));
1132 }
1133 Some(parts.join("\n"))
1134 }
1135 pub fn on_turn(&mut self) {
1138 if let Some(id) = self.foreground.clone() {
1139 let revision = self.tasks[&id].revision;
1140 self.on_turn_for(&id, revision);
1141 }
1142 }
1143 pub fn on_turn_for(&mut self, id: &TaskId, revision: u64) {
1147 if self
1148 .tasks
1149 .get(id)
1150 .is_some_and(|task| task.services.is_some())
1151 {
1152 let _ = self.observe(
1153 id,
1154 revision,
1155 TaskObservation::Turn {
1156 user: String::new(),
1157 model: String::new(),
1158 },
1159 );
1160 return;
1161 }
1162 let Some(task) = self.tasks.get_mut(id) else {
1163 return;
1164 };
1165 if task.revision != revision
1166 || !matches!(task.status, TaskStatus::Running | TaskStatus::Suspended)
1167 {
1168 return;
1169 }
1170 if is_executing(task, &self.operations) {
1171 task.deferred.push(DeferredEvent::Turn);
1172 } else if let Some(flow) = &mut task.flow {
1173 flow.on_turn(&task.state);
1174 }
1175 }
1176 pub fn on_interrupted(&mut self) {
1178 if let Some(id) = self.foreground.clone() {
1179 let revision = self.tasks[&id].revision;
1180 self.on_interrupted_for(&id, revision);
1181 }
1182 }
1183 pub fn on_interrupted_for(&mut self, id: &TaskId, revision: u64) {
1186 if self
1187 .tasks
1188 .get(id)
1189 .is_some_and(|task| task.services.is_some())
1190 {
1191 let _ = self.observe(id, revision, TaskObservation::Interrupted);
1192 return;
1193 }
1194 let Some(task) = self.tasks.get_mut(id) else {
1195 return;
1196 };
1197 if task.revision != revision
1198 || !matches!(task.status, TaskStatus::Running | TaskStatus::Suspended)
1199 {
1200 return;
1201 }
1202 if is_executing(task, &self.operations) {
1203 task.deferred.push(DeferredEvent::Interrupted);
1204 } else if let Some(flow) = &mut task.flow {
1205 flow.on_interrupted(&task.state);
1206 }
1207 }
1208 pub fn needs_service_timer(&self) -> bool {
1210 self.foreground
1211 .as_ref()
1212 .and_then(|id| self.tasks.get(id))
1213 .and_then(|task| task.services.as_ref())
1214 .is_some_and(|services| {
1215 services
1216 .plan
1217 .patterns
1218 .iter()
1219 .any(|p| p.detector.needs_timer())
1220 })
1221 }
1222 pub fn observe(
1224 &mut self,
1225 id: &TaskId,
1226 revision: u64,
1227 event: TaskObservation,
1228 ) -> Result<(), TaskError> {
1229 let task = self.require_live(id)?;
1230 if task.revision != revision {
1231 return Err(err("task observation revision changed"));
1232 }
1233 if task.services.is_none() {
1234 match event {
1235 TaskObservation::Turn { .. } => self.on_turn_for(id, revision),
1236 TaskObservation::Interrupted => self.on_interrupted_for(id, revision),
1237 _ => {}
1238 }
1239 return Ok(());
1240 }
1241 let task = self.tasks.get_mut(id).expect("validated task");
1242 let services = task.services.as_mut().expect("services checked");
1243 if services.queue.len() >= 128 {
1244 return Err(err("task service observation queue is full"));
1245 }
1246 services.advance_clock(
1247 &task.state,
1248 task.status == TaskStatus::Running
1249 && services.pending.is_none()
1250 && task.pending.is_none(),
1251 );
1252 services.queue.push_back(ServiceEvent::Observe(event));
1253 self.drive_services_for(id);
1254 Ok(())
1255 }
1256 pub fn take_ready_services(&mut self) -> Vec<OwnedTaskServiceWork> {
1258 std::mem::take(&mut self.ready_services)
1259 }
1260 pub fn take_service_effects(&mut self) -> Vec<AcceptedTaskEffect> {
1262 std::mem::take(&mut self.service_effects)
1263 }
1264 pub fn complete_services(&mut self, completion: TaskServiceCompletion) -> bool {
1266 let TaskServiceCompletion {
1267 owner,
1268 scope,
1269 result,
1270 } = completion;
1271 if !Arc::ptr_eq(&scope, &self.scope) {
1272 return false;
1273 }
1274 let Some(task) = self.tasks.get_mut(&owner.task) else {
1275 return false;
1276 };
1277 if task.revision != owner.revision
1278 || !matches!(task.status, TaskStatus::Running | TaskStatus::Suspended)
1279 {
1280 return false;
1281 }
1282 let Some(services) = &mut task.services else {
1283 return false;
1284 };
1285 if !services
1286 .pending
1287 .as_ref()
1288 .is_some_and(|pending| pending.owner == owner)
1289 {
1290 return false;
1291 }
1292 let pending = services.pending.take().expect("matched service ticket");
1293 services.advance_clock(&task.state, false);
1294 let mut errors = Vec::new();
1295 match result {
1296 ServiceResult::Extractions(results) => {
1297 for (extractor, result) in results {
1298 match result {
1299 Ok(value) => {
1300 if extractor.promotion_rules().is_empty()
1301 && value.as_object().is_some_and(|fields| {
1302 fields.keys().any(|key| reserved_service_key(key))
1303 })
1304 {
1305 errors.push(format!(
1306 "extraction '{}' returned a reserved runtime field",
1307 extractor.name()
1308 ));
1309 continue;
1310 }
1311 let _ = task.state.set(extractor.name(), value.clone());
1312 crate::live::extractor::promote_fields(
1313 extractor.as_ref(),
1314 extractor.name(),
1315 &value,
1316 &task.state,
1317 );
1318 }
1319 Err(error) => {
1320 errors.push(format!("extraction '{}': {error}", extractor.name()));
1321 }
1322 }
1323 }
1324 }
1325 ServiceResult::Memory(Ok(Value::Object(slots))) => {
1326 services
1328 .memory_values
1329 .retain(|key, previous| task.state.get_raw(key).as_ref() == Some(previous));
1330 let removed: Vec<_> = services
1331 .memory_values
1332 .keys()
1333 .filter(|key| slots.get(*key).is_none_or(Value::is_null))
1334 .cloned()
1335 .collect();
1336 for key in removed {
1337 task.state.remove(&key);
1338 services.memory_values.remove(&key);
1339 }
1340 for (key, value) in slots {
1341 if reserved_service_key(&key) {
1342 errors.push(format!("memory projection uses reserved field '{key}'"));
1343 } else if !value.is_null() {
1344 services.memory_keys.insert(key.clone());
1345 if !task.state.contains(&key) || services.memory_values.contains_key(&key) {
1346 match task.state.set(&key, value.clone()) {
1347 Ok(()) => {
1348 services.memory_values.insert(key, value);
1349 }
1350 Err(error) => errors.push(error.to_string()),
1351 }
1352 }
1353 }
1354 }
1355 }
1356 ServiceResult::Memory(Ok(_)) => {
1357 errors.push("task memory projection must be an object".into());
1358 }
1359 ServiceResult::Memory(Err(error)) | ServiceResult::Failed(error) => errors.push(error),
1360 }
1361 let (mut effects, changed) =
1362 settle_task_services(task, pending.before, Some(&pending.event), owner.clone());
1363 for error in errors {
1364 effects.extend(record_service_effect(
1365 task,
1366 owner.clone(),
1367 TaskEffect::Error(error),
1368 ));
1369 }
1370 self.service_effects.extend(effects);
1371 if changed {
1372 self.invalidate_approval(
1373 &owner.task,
1374 "approval invalidated by accepted service state",
1375 );
1376 }
1377 self.drive_services_for(&owner.task);
1378 true
1379 }
1380 fn drive_services_for(&mut self, id: &TaskId) {
1381 loop {
1382 let Some(task) = self.tasks.get(id) else {
1383 return;
1384 };
1385 if !matches!(task.status, TaskStatus::Running | TaskStatus::Suspended)
1386 || is_executing(task, &self.operations)
1387 {
1388 return;
1389 }
1390 let Some(services) = &task.services else {
1391 return;
1392 };
1393 if services.pending.is_some() || services.queue.is_empty() {
1394 return;
1395 }
1396 let task = self.tasks.get_mut(id).expect("task checked");
1397 let services = task.services.as_mut().expect("services checked");
1398 let event = services.queue.pop_front().expect("queue checked");
1399 let owner = TaskOwner {
1400 task: id.clone(),
1401 revision: task.revision,
1402 operation: OperationId(format!("service-{}", self.next_service)),
1403 };
1404 self.next_service += 1;
1405 let before = task.state.to_hashmap();
1406 if let Some(work) = services.prepare(
1407 &event,
1408 owner.clone(),
1409 independent_state(&task.state),
1410 self.scope.clone(),
1411 ) {
1412 self.ready_services.push(work);
1413 return;
1414 }
1415 let (effects, changed) = settle_task_services(task, before, Some(&event), owner);
1416 self.service_effects.extend(effects);
1417 if changed {
1418 self.invalidate_approval(id, "approval invalidated by accepted service state");
1419 }
1420 }
1421 }
1422
1423 pub fn shutdown(&mut self) {
1426 let ids: Vec<_> = self.tasks.keys().cloned().collect();
1427 for id in ids {
1428 if self.require_live(&id).is_ok() {
1429 let _ = self.cancel(&id);
1430 }
1431 }
1432 self.foreground = None;
1433 for operation in self.operations.values_mut() {
1434 if operation.snapshot.status == OperationStatus::Running {
1435 operation.control.cancel.cancel();
1436 operation.snapshot.cancellation_requested = true;
1437 operation.snapshot.status = if operation.commit {
1438 OperationStatus::Unknown
1439 } else {
1440 OperationStatus::Cancelled
1441 };
1442 operation.snapshot.error =
1443 Some("session ended before an operation receipt was accepted".into());
1444 }
1445 }
1446 }
1447}
1448
1449fn settle_task_services(
1450 task: &mut Task,
1451 before: HashMap<String, Value>,
1452 event: Option<&ServiceEvent>,
1453 owner: TaskOwner,
1454) -> (Vec<AcceptedTaskEffect>, bool) {
1455 let Some(services) = &mut task.services else {
1456 return (Vec::new(), false);
1457 };
1458 services
1459 .memory_values
1460 .retain(|key, previous| task.state.get_raw(key).as_ref() == Some(previous));
1461 let effects = services.stabilize(&task.state, before.clone(), event);
1462 services
1463 .memory_values
1464 .retain(|key, previous| task.state.get_raw(key).as_ref() == Some(previous));
1465 let after = task.state.to_hashmap();
1466 let changed = before
1467 .keys()
1468 .chain(after.keys())
1469 .any(|key| !key.starts_with("state_meta:") && before.get(key) != after.get(key));
1470 if let Some(ServiceEvent::Observe(TaskObservation::Turn { user, .. })) = event
1471 && !user.is_empty()
1472 && services.plan.memory.is_some()
1473 {
1474 services
1475 .queue
1476 .push_back(ServiceEvent::Ingest { user: user.clone() });
1477 }
1478 if let Some(flow) = &mut task.flow {
1479 match event {
1480 Some(ServiceEvent::Observe(TaskObservation::Turn { .. })) => flow.on_turn(&task.state),
1481 Some(ServiceEvent::Observe(TaskObservation::Interrupted)) => {
1482 flow.on_interrupted(&task.state);
1483 }
1484 _ => flow.relatch(&task.state),
1485 }
1486 }
1487 (
1488 effects
1489 .into_iter()
1490 .flat_map(|effect| record_service_effect(task, owner.clone(), effect))
1491 .collect(),
1492 changed,
1493 )
1494}
1495fn record_service_effect(
1496 task: &mut Task,
1497 owner: TaskOwner,
1498 effect: TaskEffect,
1499) -> Vec<AcceptedTaskEffect> {
1500 let services = task.services.as_mut().expect("task has services");
1501 match &effect {
1502 TaskEffect::Remember(note) => {
1503 if services.plan.memory.is_some() {
1504 services
1505 .queue
1506 .push_back(ServiceEvent::Remember { note: note.clone() });
1507 } else {
1508 return record_service_effect(
1509 task,
1510 owner,
1511 TaskEffect::Error("remember effect requires task memory".into()),
1512 );
1513 }
1514 Vec::new()
1515 }
1516 TaskEffect::Context(text) => {
1517 services.contexts.push(text.clone());
1518 if services.contexts.len() > 32 {
1519 services.contexts.remove(0);
1520 }
1521 vec![AcceptedTaskEffect { owner, effect }]
1522 }
1523 TaskEffect::Error(error) => {
1524 services.errors.push(error.clone());
1525 if services.errors.len() > 16 {
1526 services.errors.remove(0);
1527 }
1528 vec![AcceptedTaskEffect { owner, effect }]
1529 }
1530 _ => vec![AcceptedTaskEffect { owner, effect }],
1531 }
1532}
1533
1534fn err(message: impl Into<String>) -> TaskError {
1535 TaskError(message.into())
1536}
1537fn independent_state(source: &State) -> State {
1538 let state = State::new();
1539 state.from_hashmap(source.to_hashmap());
1540 state.redact_keys(source.redacted_keys());
1541 state.set_clock(source.clock());
1542 state
1543}
1544fn instantiate(
1545 skill: &CompiledSkill,
1546 input: Value,
1547) -> Result<(State, Option<FlowStack>), TaskError> {
1548 (skill.validate_input)(&input).map_err(err)?;
1549 let state = State::new();
1550 state.from_hashmap(skill.initial_state.clone());
1551 if let Value::Object(fields) = &input {
1552 for (key, value) in fields {
1553 state
1554 .set(key, value.clone())
1555 .map_err(|error| err(error.to_string()))?;
1556 }
1557 }
1558 state
1559 .set("task:input", input)
1560 .map_err(|error| err(error.to_string()))?;
1561 let mut flow = skill
1562 .flow_factory
1563 .as_ref()
1564 .map(|factory| factory())
1565 .transpose()
1566 .map_err(err)?;
1567 if let Some(flow) = &mut flow {
1568 flow.relatch(&state);
1569 }
1570 Ok((state, flow))
1571}
1572fn make_invocation(
1573 task: &Task,
1574 owner: TaskOwner,
1575 tool: &str,
1576 args: Value,
1577 control: ExecutionControl,
1578 cached: Option<Receipt>,
1579 scope: Arc<()>,
1580) -> Result<OwnedInvocation, TaskError> {
1581 let state = independent_state(&task.state);
1582 let dispatcher = (task.skill.tool_factory)(state.clone()).map_err(err)?;
1583 if dispatcher.has_confirmation_provider() {
1584 return Err(err(
1585 "task factories cannot install a blocking confirmation provider; use task Decide",
1586 ));
1587 }
1588 if !dispatcher.names().any(|name| name == tool) {
1589 return Err(err(format!("task factory did not bind tool '{tool}'")));
1590 }
1591 if dispatcher.gated_tools().any(|name| name == tool)
1592 && task.skill.tools.iter().any(|declared| {
1593 declared.name == tool && matches!(declared.effect, TaskToolEffect::Read)
1594 })
1595 {
1596 return Err(err(
1597 "confirmation-gated tools must declare a commit policy in tasks",
1598 ));
1599 }
1600 Ok(OwnedInvocation {
1601 owner,
1602 tool: tool.into(),
1603 args,
1604 state,
1605 dispatcher,
1606 control,
1607 cached,
1608 scope,
1609 })
1610}
1611fn is_executing(task: &Task, operations: &BTreeMap<OperationId, Operation>) -> bool {
1612 task.pending
1613 .as_ref()
1614 .and_then(|id| operations.get(id))
1615 .is_some_and(|operation| {
1616 operation.snapshot.status == OperationStatus::Running
1617 || (operation.snapshot.status == OperationStatus::Unknown
1618 && !operation.receipt_observed)
1619 })
1620}
1621fn drain_turns(task: &mut Task) {
1622 for event in std::mem::take(&mut task.deferred) {
1623 if let Some(flow) = &mut task.flow {
1624 match event {
1625 DeferredEvent::Turn => flow.on_turn(&task.state),
1626 DeferredEvent::Interrupted => flow.on_interrupted(&task.state),
1627 }
1628 }
1629 }
1630}
1631fn state_diff(
1632 before: &HashMap<String, Value>,
1633 after: &HashMap<String, Value>,
1634) -> BTreeMap<String, Option<Value>> {
1635 before
1636 .keys()
1637 .chain(after.keys())
1638 .filter(|key| before.get(*key) != after.get(*key))
1639 .map(|key| (key.clone(), after.get(key).cloned()))
1640 .collect()
1641}
1642
1643impl Drop for TaskRuntime {
1644 fn drop(&mut self) {
1645 self.shutdown();
1646 }
1647}