Skip to main content

gemini_adk_rs/tasks/
runtime.rs

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
65/// The only executable ticket produced by task admission or trusted approval.
66/// Its state and tool closures belong to this invocation, never another task.
67pub 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/// A worker result carrying the original owner and private state.
78/// Fields are private so applications cannot manufacture accepted state writes.
79#[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    /// Invocation identity used by the host for protocol correlation.
91    pub fn operation_id(&self) -> &OperationId {
92        &self.owner.operation
93    }
94    /// Immutable task ownership, captured before execution.
95    pub fn owner(&self) -> &TaskOwner {
96        &self.owner
97    }
98    /// Execute without accessing session transport. Commits already passed approval.
99    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
150/// Task lifecycle reducer. Drive it from one control lane or a deterministic replay.
151/// At most one invocation is pending per activation; different tasks may execute
152/// concurrently. All commands finish synchronously, including approval requests.
153pub 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    /// Install a finite catalog. Duplicate names/tool names are rejected.
168    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    /// Use an injected clock for future task activations and deterministic replay.
230    pub fn with_clock(mut self, clock: crate::clock::SharedClock) -> Self {
231        self.clock = clock;
232        self
233    }
234
235    /// Apply a command. An executable ticket is returned only after admission
236    /// and, for a commit, an explicit trusted decision about its exact proposal.
237    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    /// Commit the result to its captured owner, never whichever task is now foreground.
699    /// External commit receipts survive cancellation/revision even when state is stale.
700    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    /// Observe the pinned catalog without exposing factories or raw state.
906    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    /// Observe all activation and operation lifecycles. Not a restore checkpoint.
922    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    /// Foreground skill instructions and current governed posture/grounding:
960    /// [`foreground_brief`](Self::foreground_brief) followed by
961    /// [`foreground_context`](Self::foreground_context).
962    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    /// The part of the foreground instruction that changes only when the
971    /// foreground task or its revision does: the task's identity and its
972    /// skill's instruction. Under `contextUpdate` it rides the system
973    /// instruction, which an update replaces, rather than a context turn,
974    /// which stays in the conversation.
975    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    /// Give the installed skill `name` a code tool, for tests.
984    #[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    /// The code tool of the skill `name`, if it has one.
992    pub fn skill_code(&self, name: &str) -> Option<&crate::code_mode::CodeMode> {
993        self.skills.get(name)?.code.as_ref()
994    }
995
996    /// The foreground task's skill and the tools it offers the model now:
997    /// those its flow would admit once their `never(..).until(..)` guards
998    /// hold, as [`FlowStack::offers_tool`] decides, or every tool of a skill
999    /// without a flow. `None` with no foreground task.
1000    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    /// The part of the foreground instruction that changes as the task runs:
1017    /// service contexts, extracted, computed and remembered values, active
1018    /// postures and grounds, input and results. Empty when there is none.
1019    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    /// Advance current foreground governance. Hosts with transcript ownership
1136    /// should prefer [`on_turn_for`](Self::on_turn_for).
1137    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    /// Advance a turn belonging to the captured task/revision, even if another
1144    /// task is now speaking. Stale and terminal owners are ignored. While work
1145    /// is unresolved, events queue so its snapshot cannot overwrite repair writes.
1146    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    /// Attribute an interruption to the current foreground task.
1177    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    /// Attribute an interruption to its captured owner without cancelling
1184    /// unrelated tasks or claiming an external commit has been undone.
1185    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    /// Whether the foreground task has a detector requiring timer observations.
1209    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    /// Enqueue an observation captured for this task revision, never the current speaker by inference.
1223    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    /// Take runnable service tickets prepared by the same reducer as tool invocations.
1257    pub fn take_ready_services(&mut self) -> Vec<OwnedTaskServiceWork> {
1258        std::mem::take(&mut self.ready_services)
1259    }
1260    /// Take accepted owner-labelled effects. Speaking delivery must recheck foreground ownership.
1261    pub fn take_service_effects(&mut self) -> Vec<AcceptedTaskEffect> {
1262        std::mem::take(&mut self.service_effects)
1263    }
1264    /// Accept a service result only once and only in its original live task revision.
1265    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                // A slot remains memory-owned only while no accepted local change has replaced it.
1327                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    /// Cancel workers on session shutdown. Dispatched commit outcomes become
1424    /// unknown until an actual receipt or trusted reconciliation resolves them.
1425    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}