Skip to main content

gemini_adk_rs/tasks/
types.rs

1//! Task definitions, commands, and observations shared by Live and replay.
2use crate::{
3    flow::{FlowSnapshot, FlowStack},
4    state::State,
5    tool::ToolDispatcher,
6};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::{collections::HashMap, sync::Arc};
11
12/// A session-local activation identity.
13#[derive(
14    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
15)]
16#[serde(transparent)]
17pub struct TaskId(pub String);
18/// A session-local invocation and approval identity.
19#[derive(
20    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
21)]
22#[serde(transparent)]
23pub struct OperationId(pub String);
24/// The installed definition pinned by an activation.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
26pub struct SkillKey {
27    /// Installed name.
28    pub name: String,
29    /// Installed version.
30    pub version: String,
31}
32/// Input/output validation supplied by the authoring layer.
33pub type ValueValidator = Arc<dyn Fn(&Value) -> Result<(), String> + Send + Sync>;
34/// Builds fresh governance for each activation.
35pub type FlowFactory = Arc<dyn Fn() -> Result<FlowStack, String> + Send + Sync>;
36/// Binds all captured state in tools to an isolated invocation state.
37pub type TaskToolFactory = Arc<dyn Fn(State) -> Result<ToolDispatcher, String> + Send + Sync>;
38/// Projects declared outputs from accepted task state.
39pub type OutputProjector = Arc<dyn Fn(&State) -> Result<Value, String> + Send + Sync>;
40/// Reconstructs declared state effects from a verified external receipt without executing a tool.
41pub type ReceiptApplier = Arc<dyn Fn(&str, &Value, &State) -> Result<(), String> + Send + Sync>;
42/// Builds fresh service registries, counters and resource bindings for an activation.
43pub type TaskServicesFactory = Arc<dyn Fn() -> Result<super::TaskServices, String> + Send + Sync>;
44/// Executable capability definition. Trusted closures must not capture unrelated mutable state.
45#[derive(Clone)]
46pub struct CompiledSkill {
47    /// Optional per-activation extraction, derivation, watchers, patterns and memory.
48    pub services: Option<TaskServicesFactory>,
49    /// Catalog lookup name.
50    pub name: String,
51    /// Version pinned on activation.
52    pub version: String,
53    /// Routing description.
54    pub description: String,
55    /// Foreground speaking instruction.
56    pub instruction: String,
57    /// JSON input contract.
58    pub input_schema: Value,
59    /// JSON output contract.
60    pub output_schema: Value,
61    /// Compiled input boundary check.
62    pub validate_input: ValueValidator,
63    /// Compiled output boundary check.
64    pub validate_output: ValueValidator,
65    /// Independent defaults copied at activation.
66    pub initial_state: HashMap<String, Value>,
67    /// Optional conversation governance.
68    pub flow_factory: Option<FlowFactory>,
69    /// Invocation-local tools.
70    pub tool_factory: TaskToolFactory,
71    /// Available tool metadata and effect policies.
72    pub tools: Vec<TaskTool>,
73    /// When present, completion exports these accepted state values instead of caller output.
74    pub project_output: Option<OutputProjector>,
75    /// Pure state reconstruction for trusted reconciliation. Custom stateful tools
76    /// must supply their own mapping; failed-worker writes are never trusted.
77    pub receipt_applier: Option<ReceiptApplier>,
78    /// The skill's code tool, declared as `{skill}__run_code`: a program whose
79    /// functions are the skill's own tools, by their local names. Each call
80    /// it makes is an invocation of the task, governed like a direct one.
81    pub code: Option<crate::code_mode::CodeMode>,
82}
83/// One declared operation. Names are local to a skill.
84#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
85pub struct TaskTool {
86    /// Local tool name.
87    pub name: String,
88    /// Model-facing purpose.
89    pub description: String,
90    /// Function argument schema.
91    pub parameters: Option<Value>,
92    /// Whether this operation can commit an external effect.
93    pub effect: TaskToolEffect,
94}
95/// Effects determine admission, approval, cancellation and reconciliation.
96#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
97#[serde(tag = "kind", rename_all = "snake_case")]
98pub enum TaskToolEffect {
99    /// Does not commit an external mutation.
100    Read,
101    /// Requires explicit approval and a complete key passed to the external adapter.
102    Commit {
103        /// String argument carrying the external idempotency key.
104        idempotency_argument: String,
105    },
106}
107/// Trusted application/UI command. Models must never receive Decide or Reconcile.
108#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
109#[serde(tag = "action", rename_all = "snake_case")]
110pub enum TaskCommand {
111    /// Start a new independent activation and make it foreground.
112    Start {
113        /// Installed skill name.
114        skill: String,
115        /// Input validated against that skill's contract.
116        #[serde(default = "empty_object")]
117        input: Value,
118        /// Optional parent resumed when this child finishes.
119        #[serde(default)]
120        parent: Option<TaskId>,
121    },
122    /// Suspend a task without losing its accepted progress.
123    Suspend {
124        #[doc = "Activation to suspend."]
125        task: TaskId,
126    },
127    /// Make an existing nonterminal activation foreground.
128    Resume {
129        #[doc = "Activation to resume."]
130        task: TaskId,
131    },
132    /// Replace inputs and governance, invalidating prior work and approvals.
133    Revise {
134        /// Activation to revise.
135        task: TaskId,
136        /// Revision the caller observed.
137        expected_revision: u64,
138        /// Complete replacement input.
139        input: Value,
140    },
141    /// Cancel this task; an already dispatched commit may still finish.
142    Cancel {
143        #[doc = "Activation to cancel."]
144        task: TaskId,
145    },
146    /// Validate and export a task's result; resume its suspended parent.
147    Complete {
148        /// Activation to finish.
149        task: TaskId,
150        /// Output used when the skill has no state projector; defaults to an empty object.
151        #[serde(default = "empty_object")]
152        output: Value,
153    },
154    /// Admit a tool for the foreground activation.
155    Invoke {
156        /// Activation whose tool should run.
157        task: TaskId,
158        /// Skill-local tool name.
159        tool: String,
160        /// Exact proposed arguments.
161        args: Value,
162        /// Stable external operation key for a commit.
163        #[serde(default)]
164        idempotency_key: Option<String>,
165    },
166    /// Trusted decision about an immutable pending proposal.
167    Decide {
168        /// Pending operation to approve or decline.
169        operation: OperationId,
170        /// Whether the exact proposal is approved.
171        approve: bool,
172    },
173    /// Trusted externally verified resolution of an unknown commit outcome.
174    Reconcile {
175        /// Operation whose external outcome was verified.
176        operation: OperationId,
177        /// Verified result.
178        outcome: ReconciledOutcome,
179    },
180}
181/// An externally verified commit result, not an inference from cancellation.
182#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
183#[serde(tag = "status", rename_all = "snake_case")]
184pub enum ReconciledOutcome {
185    /// External service confirms the operation succeeded.
186    Succeeded {
187        #[doc = "Verified external result."]
188        result: Value,
189    },
190    /// External service confirms no committed effect remains.
191    Failed {
192        #[doc = "Verified failure description."]
193        error: String,
194    },
195}
196/// Task lifecycle, separate from the lifecycle of its external operations.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
198#[serde(rename_all = "snake_case")]
199pub enum TaskStatus {
200    /// Owns the speaking foreground.
201    Running,
202    /// Retains progress while another task speaks.
203    Suspended,
204    /// Exported a validated output.
205    Completed,
206    /// Cancelled; external operations retain independent receipts.
207    Cancelled,
208    /// Could not continue.
209    Failed,
210}
211/// Operation lifecycle, including uncertainty after an interrupted commit.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
213#[serde(rename_all = "snake_case")]
214pub enum OperationStatus {
215    /// Waiting for a trusted decision about exact arguments.
216    AwaitingApproval,
217    /// Released to an owned worker.
218    Running,
219    /// Successful result was observed.
220    Succeeded,
221    /// Read failed, or an external non-commit was verified.
222    Failed,
223    /// User declined or a proposal was invalidated.
224    Declined,
225    /// Execution was revoked or a read was cancelled.
226    Cancelled,
227    /// A dispatched commit has no reliable external outcome yet.
228    Unknown,
229}
230/// Identity carried through contextual tool execution.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
232pub struct TaskOwner {
233    /// Owning activation.
234    pub task: TaskId,
235    /// Owning input revision.
236    pub revision: u64,
237    /// Owning invocation.
238    pub operation: OperationId,
239}
240/// Discoverable installed skill metadata.
241#[derive(Debug, Clone, Serialize)]
242pub struct SkillSummary {
243    /// Installed name/version.
244    pub key: SkillKey,
245    /// Routing description.
246    pub description: String,
247    /// Input contract.
248    pub input_schema: Value,
249    /// Output contract.
250    pub output_schema: Value,
251    /// Skill-local tool declarations.
252    pub tools: Vec<TaskTool>,
253}
254/// One activation's observable progress. Raw state is deliberately excluded.
255#[derive(Debug, Clone, Serialize)]
256pub struct TaskSnapshot {
257    /// Whether this task has running or queued service work.
258    #[serde(default)]
259    pub services_pending: bool,
260    /// Recent owned service failures, without raw provider payloads.
261    #[serde(default)]
262    pub service_errors: Vec<String>,
263    /// Activation ID.
264    pub id: TaskId,
265    /// Optional parent resumed after child completion.
266    pub parent: Option<TaskId>,
267    /// Pinned definition.
268    pub skill: SkillKey,
269    /// Current input revision.
270    pub revision: u64,
271    /// Activation lifecycle.
272    pub status: TaskStatus,
273    /// Current task-local flow observation.
274    pub flow: Option<FlowSnapshot>,
275    /// Current pending operation, including approval requests.
276    pub pending: Option<OperationId>,
277    /// Validated completion output.
278    pub output: Option<Value>,
279}
280/// An operation receipt or immutable approval proposal.
281#[derive(Debug, Clone, Serialize)]
282pub struct OperationSnapshot {
283    /// Stable invocation/approval identity.
284    pub id: OperationId,
285    /// Original owning activation and revision.
286    pub owner: TaskOwner,
287    /// Local tool name.
288    pub tool: String,
289    /// Exact admitted arguments; approval never substitutes them.
290    pub args: Value,
291    /// External operation key for commit tools.
292    pub idempotency_key: Option<String>,
293    /// Current operation status.
294    pub status: OperationStatus,
295    /// Cancellation requested after dispatch; not a claim that an effect was undone.
296    pub cancellation_requested: bool,
297    /// Successful result receipt, if known.
298    pub result: Option<Value>,
299    /// Failure or uncertainty description.
300    pub error: Option<String>,
301}
302/// Complete observable task session. This is not a durable restore checkpoint.
303#[derive(Debug, Clone, Serialize)]
304pub struct TaskSessionSnapshot {
305    /// Currently speaking activation.
306    pub foreground: Option<TaskId>,
307    /// Installed catalog.
308    pub skills: Vec<SkillSummary>,
309    /// All activation observations.
310    pub tasks: Vec<TaskSnapshot>,
311    /// Pending proposals and retained operation outcomes.
312    pub operations: Vec<OperationSnapshot>,
313}
314/// A tool completion's publication decision.
315#[derive(Debug)]
316pub struct TaskDelivery {
317    /// Operation the host correlates to its protocol call.
318    pub operation_id: OperationId,
319    /// Original task owner.
320    pub task_id: TaskId,
321    /// Original input revision.
322    pub revision: u64,
323    /// Tool result for the host to format.
324    pub result: Result<Value, String>,
325    /// Valid completion for the foreground task.
326    pub foreground: bool,
327    /// Completion did not update the current activation revision.
328    pub stale: bool,
329}
330/// Configuration, admission or lifecycle error.
331#[derive(Debug, thiserror::Error)]
332#[error("task runtime: {0}")]
333pub struct TaskError(pub String);
334
335fn empty_object() -> Value {
336    Value::Object(serde_json::Map::new())
337}
338
339/// The longest skill name: its entry point, `start_{skill}`, must fit the
340/// Live API's 64-character function names.
341pub const MAX_SKILL_NAME: usize = 64 - "start_".len();
342
343/// Validate identity shared by compiled catalogs and authoring documents.
344pub fn validate_skill_identity(name: &str, version: &str) -> Result<(), TaskError> {
345    if !valid_name(name) || version.trim().is_empty() {
346        return Err(TaskError(
347            "skill names must be ASCII identifiers without '__', and versions must be nonempty"
348                .into(),
349        ));
350    }
351    if name.len() > MAX_SKILL_NAME {
352        return Err(TaskError(format!(
353            "skill name '{name}' is {} characters; at most {MAX_SKILL_NAME} fit, so that \
354             start_{name} fits the 64-character function name limit",
355            name.len()
356        )));
357    }
358    Ok(())
359}
360impl TaskTool {
361    /// Validate the local name, qualified provider name and commit key policy.
362    pub fn validate_for_skill(&self, skill: &str) -> Result<(), TaskError> {
363        if !valid_name(&self.name) || skill.len() + 2 + self.name.len() > 64 {
364            return Err(TaskError(format!(
365                "tool '{}' must be an ASCII identifier without '__'; its qualified name must fit 64 characters",
366                self.name
367            )));
368        }
369        if let TaskToolEffect::Commit {
370            idempotency_argument,
371        } = &self.effect
372            && idempotency_argument.trim().is_empty()
373        {
374            return Err(TaskError(
375                "commit idempotency argument must be nonempty".into(),
376            ));
377        }
378        Ok(())
379    }
380}
381fn valid_name(name: &str) -> bool {
382    !name.contains("__")
383        && name.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
384        && name.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_')
385}