Skip to main content

tenzro_types/
agent_template.rs

1//! Agent marketplace types for Tenzro Network
2//!
3//! Defines types for the decentralized agent marketplace where
4//! providers can publish agent templates for others to discover,
5//! download, and deploy.
6//!
7//! ## Execution Spec
8//!
9//! In addition to the static metadata above, an `AgentTemplate` may carry an
10//! `execution_spec: Option<ExecutionSpec>` describing how an autonomous
11//! runtime (e.g. `tenzro-agent-kit`) should execute the agent without any
12//! hardcoded Rust code: which backend (EVM/SVM/DAML/MultiVm/Bridge/Mpp/X402),
13//! which delegation scope to provision, which tools and skills to
14//! auto-discover from the registry by tag, and a declarative list of
15//! `ExecutionStep`s that map 1:1 to existing subsystems.
16
17use crate::primitives::{Address, Timestamp, u128_serde};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Status of an agent template in the marketplace
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AgentTemplateStatus {
25    /// Template is published and available for download
26    Published,
27    /// Template is under review
28    Pending,
29    /// Template has been deprecated by its creator
30    Deprecated,
31    /// Template has been suspended (policy violation)
32    Suspended,
33}
34
35/// The type of agent template
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum AgentTemplateType {
39    /// Standalone autonomous agent
40    Autonomous,
41    /// Tool/function calling agent that wraps external APIs
42    ToolAgent,
43    /// Workflow orchestrator that coordinates other agents
44    Orchestrator,
45    /// Specialist agent optimized for a specific domain
46    Specialist,
47    /// Multi-modal agent handling text, image, audio, etc.
48    MultiModal,
49    /// Custom agent type
50    Custom(String),
51}
52
53/// A specific capability that an agent template provides
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct AgentCapability {
56    /// Capability identifier (e.g., "code_generation", "web_search")
57    pub id: String,
58
59    /// Human-readable name
60    pub name: String,
61
62    /// Description of what the capability does
63    pub description: String,
64
65    /// Input schema (JSON Schema string)
66    pub input_schema: Option<String>,
67
68    /// Output schema (JSON Schema string)
69    pub output_schema: Option<String>,
70}
71
72/// Runtime requirements for deploying the agent template
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
74pub struct AgentRuntimeRequirements {
75    /// Minimum model size required (e.g., "1b", "7b", "70b")
76    #[serde(default)]
77    pub min_model_size: Option<String>,
78
79    /// Preferred model ID for best results
80    #[serde(default)]
81    pub preferred_model_id: Option<String>,
82
83    /// Required MCP tool names
84    #[serde(default)]
85    pub required_mcp_tools: Vec<String>,
86
87    /// Required external API access (e.g., "web_search", "code_execution")
88    #[serde(default)]
89    pub required_permissions: Vec<String>,
90
91    /// Estimated TNZO cost per execution (in micro-units)
92    #[serde(default)]
93    pub estimated_cost_per_run: Option<u128>,
94
95    /// Whether TEE execution is required
96    #[serde(default)]
97    pub requires_tee: bool,
98
99    /// Minimum memory in MB (from execution manifests)
100    #[serde(default)]
101    pub min_memory_mb: Option<u64>,
102
103    /// Minimum storage in MB (from execution manifests)
104    #[serde(default)]
105    pub min_storage_mb: Option<u64>,
106
107    /// Whether GPU is required
108    #[serde(default)]
109    pub gpu_required: bool,
110
111    /// Whether network access is required
112    #[serde(default)]
113    pub network_access: bool,
114
115    /// Supported platforms (e.g., "linux-x86_64", "macos-aarch64")
116    #[serde(default)]
117    pub supported_platforms: Vec<String>,
118}
119
120
121
122/// Pricing model for using an agent template
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125#[derive(Default)]
126pub enum AgentPricingModel {
127    /// Free to use
128    #[default]
129    Free,
130    /// Fixed price per execution
131    PerExecution { price: u128 },
132    /// Price per token processed
133    PerToken { price_per_token: u128 },
134    /// Subscription-based (monthly rate in TNZO)
135    Subscription { monthly_rate: u128 },
136    /// Revenue sharing with the template creator
137    RevenueShare { creator_share_bps: u16 },
138}
139
140impl AgentPricingModel {
141    /// Returns true when this template is free to invoke.
142    pub fn is_free(&self) -> bool {
143        matches!(self, AgentPricingModel::Free)
144    }
145
146    /// Gross fee (in TNZO base units) charged for a single invocation.
147    /// For `PerToken`, `tokens` is multiplied by the unit price.
148    /// `Subscription` and `RevenueShare` return 0 here — they are
149    /// settled out-of-band (subscription channel, downstream revenue split).
150    pub fn fee_for_invocation(&self, tokens: u64) -> u128 {
151        match self {
152            AgentPricingModel::Free => 0,
153            AgentPricingModel::PerExecution { price } => *price,
154            AgentPricingModel::PerToken { price_per_token } => {
155                price_per_token.saturating_mul(tokens as u128)
156            }
157            AgentPricingModel::Subscription { .. } => 0,
158            AgentPricingModel::RevenueShare { .. } => 0,
159        }
160    }
161}
162
163/// An agent template published to the Tenzro Network marketplace
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct AgentTemplate {
166    /// Unique template identifier (UUID v4)
167    pub template_id: String,
168
169    /// Template name
170    pub name: String,
171
172    /// Detailed description
173    pub description: String,
174
175    /// Template type
176    pub template_type: AgentTemplateType,
177
178    /// Address of the template creator/publisher
179    pub creator: Address,
180
181    /// Optional DID binding — when present, the template is attributable to a
182    /// Tenzro identity (`did:tenzro:human:...` or `did:tenzro:machine:...`).
183    /// The DID is bound at registration time and cannot be changed; creators
184    /// who prefer wallet-only attribution may omit this.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub creator_did: Option<String>,
187
188    /// Wallet that receives creator payouts for paid invocations.
189    /// MUST be `Some(_)` whenever `pricing != Free`. For free templates
190    /// this may be `None` and the creator simply publishes under `creator`.
191    #[serde(default)]
192    pub creator_wallet: Option<Address>,
193
194    /// Cumulative count of successful paid invocations against this template.
195    #[serde(default)]
196    pub invocation_count: u64,
197
198    /// Cumulative gross TNZO revenue (in base units) routed to the creator
199    /// across all invocations — this is the amount that landed in
200    /// `creator_wallet`, i.e. the net-of-commission creator share.
201    #[serde(default)]
202    pub total_revenue: u128,
203
204    /// Version string (semver, e.g., "1.0.0")
205    pub version: String,
206
207    /// Current status
208    pub status: AgentTemplateStatus,
209
210    /// When the template was first published
211    pub created_at: Timestamp,
212
213    /// When the template was last updated
214    pub updated_at: Timestamp,
215
216    /// List of capabilities this agent provides
217    pub capabilities: Vec<AgentCapability>,
218
219    /// Runtime requirements for deployment
220    pub runtime_requirements: AgentRuntimeRequirements,
221
222    /// Pricing model
223    pub pricing: AgentPricingModel,
224
225    /// System prompt / agent instructions
226    pub system_prompt: String,
227
228    /// Example user messages and expected behavior (for documentation)
229    pub examples: Vec<AgentExample>,
230
231    /// Tags for discoverability (e.g., ["coding", "rust", "debugging"])
232    pub tags: Vec<String>,
233
234    /// Number of times this template has been downloaded/deployed
235    pub download_count: u64,
236
237    /// Average rating (0-100, weighted by stake)
238    pub rating: u8,
239
240    /// IPFS or content hash of the full template payload
241    pub content_hash: Option<String>,
242
243    /// Link to documentation or repository
244    pub docs_url: Option<String>,
245
246    /// Additional metadata
247    pub metadata: HashMap<String, String>,
248
249    /// Optional declarative execution spec consumed by autonomous runtimes
250    /// (e.g. `tenzro-agent-kit`). When present, a runtime can instantiate
251    /// the agent end-to-end (identity, wallet, delegation, tools, skills,
252    /// execution steps) from the spec alone — no Rust code required.
253    ///
254    /// Templates without this field still deserialize cleanly via
255    /// `#[serde(default)]`.
256    #[serde(default)]
257    pub execution_spec: Option<ExecutionSpec>,
258
259    /// Unix timestamp (seconds) of the last liveness signal — registration,
260    /// update, invocation, or explicit `tenzro_heartbeatTemplate` call.
261    /// The liveness sweeper flips `status` to `Deprecated` once the template
262    /// stays silent past the configured TTL (templates are kept on-record
263    /// rather than purged — they have audit value as a marketplace history).
264    /// Pre-upgrade rows hydrate with the current time (charitable default)
265    /// so they only appear stale once they actually go silent.
266    #[serde(default = "default_last_seen_ts")]
267    pub last_seen_at: Timestamp,
268}
269
270fn default_last_seen_ts() -> Timestamp {
271    Timestamp(chrono::Utc::now().timestamp())
272}
273
274/// An example interaction demonstrating agent capabilities
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct AgentExample {
277    /// Description of the example scenario
278    pub description: String,
279
280    /// Example user input
281    pub user_input: String,
282
283    /// Expected agent output (may be abbreviated)
284    pub expected_output: String,
285}
286
287impl AgentTemplate {
288    /// Creates a new agent template
289    pub fn new(
290        name: String,
291        description: String,
292        template_type: AgentTemplateType,
293        creator: Address,
294        system_prompt: String,
295    ) -> Self {
296        let template_id = uuid::Uuid::new_v4().to_string();
297        let now = Timestamp(chrono::Utc::now().timestamp());
298        Self {
299            template_id,
300            name,
301            description,
302            template_type,
303            creator,
304            creator_did: None,
305            creator_wallet: None,
306            invocation_count: 0,
307            total_revenue: 0,
308            version: "1.0.0".to_string(),
309            status: AgentTemplateStatus::Published,
310            created_at: now,
311            updated_at: now,
312            capabilities: Vec::new(),
313            runtime_requirements: AgentRuntimeRequirements::default(),
314            pricing: AgentPricingModel::Free,
315            system_prompt,
316            examples: Vec::new(),
317            tags: Vec::new(),
318            download_count: 0,
319            rating: 0,
320            content_hash: None,
321            docs_url: None,
322            metadata: HashMap::new(),
323            execution_spec: None,
324            last_seen_at: now,
325        }
326    }
327
328    /// Bumps `last_seen_at` to the current wall-clock time. Called by the
329    /// heartbeat RPC and any successful invocation that should keep the
330    /// template from being marked Deprecated by the liveness sweeper.
331    pub fn touch(&mut self) {
332        self.last_seen_at = default_last_seen_ts();
333    }
334
335    /// Attach an optional DID binding for creator attribution.
336    pub fn with_creator_did(mut self, did: impl Into<String>) -> Self {
337        self.creator_did = Some(did.into());
338        self
339    }
340
341    /// Attach a payout wallet — REQUIRED for any non-Free pricing.
342    pub fn with_creator_wallet(mut self, wallet: Address) -> Self {
343        self.creator_wallet = Some(wallet);
344        self
345    }
346
347    /// Attach a pricing model.
348    pub fn with_pricing(mut self, pricing: AgentPricingModel) -> Self {
349        self.pricing = pricing;
350        self
351    }
352
353    /// Returns true if the template is publicly available
354    pub fn is_available(&self) -> bool {
355        self.status == AgentTemplateStatus::Published
356    }
357
358    /// Validates publisher-side invariants required before persisting the
359    /// template in the marketplace: if `pricing != Free` then
360    /// `creator_wallet` must be set (so payouts have a destination).
361    ///
362    /// Returns `Err(<reason>)` when the invariant is violated. Called from
363    /// every `registerAgentTemplate` code path (RPC + MCP).
364    pub fn validate_marketplace_invariants(&self) -> Result<(), String> {
365        if !self.pricing.is_free() && self.creator_wallet.is_none() {
366            return Err(format!(
367                "creator_wallet is mandatory for non-Free pricing (pricing={:?})",
368                self.pricing
369            ));
370        }
371        Ok(())
372    }
373
374    /// Records a successful paid invocation against this template.
375    /// `creator_share` is the net-of-commission amount that was transferred
376    /// to `creator_wallet`.
377    pub fn record_invocation(&mut self, creator_share: u128) {
378        self.invocation_count = self.invocation_count.saturating_add(1);
379        self.total_revenue = self.total_revenue.saturating_add(creator_share);
380        self.updated_at = Timestamp(chrono::Utc::now().timestamp());
381    }
382}
383
384/// Filter parameters for listing agent templates
385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
386pub struct AgentTemplateFilter {
387    /// Filter by template type
388    pub template_type: Option<AgentTemplateType>,
389
390    /// Filter by creator address
391    pub creator: Option<String>,
392
393    /// Filter by tag (must have this tag)
394    pub tag: Option<String>,
395
396    /// Filter by required model capability
397    pub required_model: Option<String>,
398
399    /// Only show free templates
400    pub free_only: Option<bool>,
401
402    /// Filter by status
403    pub status: Option<AgentTemplateStatus>,
404
405    /// Maximum number of results
406    pub limit: Option<usize>,
407
408    /// Offset for pagination
409    pub offset: Option<usize>,
410}
411
412/// Summary of a downloaded/deployed agent template instance
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
414pub struct AgentTemplateInstance {
415    /// Instance identifier
416    pub instance_id: String,
417
418    /// The template this instance is based on
419    pub template_id: String,
420
421    /// Owner of this instance
422    pub owner: Address,
423
424    /// Optional custom name given by the deployer
425    pub custom_name: Option<String>,
426
427    /// Any configuration overrides applied at deploy time
428    pub config_overrides: HashMap<String, String>,
429
430    /// When this instance was deployed
431    pub deployed_at: Timestamp,
432}
433
434// =====================================================================
435// ExecutionSpec — declarative runtime spec for autonomous agent kits
436// =====================================================================
437//
438// Every type below maps 1:1 to an existing Tenzro subsystem
439// (`MultiVmRuntime`, `BridgeRouter`, `MppClient`, `X402Client`,
440// `DamlExecutor`, `IdentityRegistry`, registry RPCs). The agent-kit
441// crate adds NO new execution logic — it only interprets these structs
442// and dispatches to the existing subsystems.
443
444/// Top-level declarative execution spec for an autonomous agent template.
445///
446/// A runtime that consumes this spec must:
447///
448///   1. Provision identity + wallet + delegation per `delegation`
449///   2. Auto-discover tools whose tags match `required_tool_tags`
450///      from `CF_TOOLS` via `tenzro_searchTools`
451///   3. Auto-discover skills whose tags match `required_skill_tags`
452///      from `CF_SKILLS` via `tenzro_searchSkills`
453///   4. Walk `steps` in order, gating each step by
454///      `IdentityRegistry::enforce_operation()` against `hard_caps`
455///   5. Dispatch each step to the matching subsystem (`backend`)
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457pub struct ExecutionSpec {
458    /// Which Tenzro execution backend the agent dispatches against
459    pub backend: ExecutionBackend,
460
461    /// Delegation scope to provision when spawning the agent
462    pub delegation: DelegationSpec,
463
464    /// Tag queries used to auto-discover required tools from `CF_TOOLS`.
465    /// At spawn time, the agent-kit calls `tenzro_searchTools` for each
466    /// tag and binds the first matching `Active` tool.
467    #[serde(default)]
468    pub required_tool_tags: Vec<String>,
469
470    /// Tag queries used to auto-discover required skills from `CF_SKILLS`.
471    /// At spawn time, the agent-kit calls `tenzro_searchSkills` for each
472    /// tag and binds the first matching active skill.
473    #[serde(default)]
474    pub required_skill_tags: Vec<String>,
475
476    /// Declarative list of execution steps. Walked in order at run time.
477    pub steps: Vec<ExecutionStep>,
478
479    /// Hard caps enforced by the runtime in addition to the delegation
480    /// scope. These guarantee that even a misbehaving step body cannot
481    /// exceed the per-operation or per-day value limit.
482    pub hard_caps: HardCaps,
483}
484
485/// The execution backend an agent dispatches against. Each variant maps
486/// to an existing Tenzro subsystem.
487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
488pub enum ExecutionBackend {
489    /// Dispatch raw EVM transactions through `MultiVmRuntime`
490    Evm { chain_id: u64 },
491
492    /// Dispatch SVM transactions through `MultiVmRuntime`
493    Svm { cluster: String },
494
495    /// Submit DAML commands to a Canton participant via `DamlExecutor`
496    Daml {
497        participant_host: String,
498        participant_port: u16,
499    },
500
501    /// Cross-VM workflow that may touch EVM, SVM, and DAML in one run
502    MultiVm,
503
504    /// Pure machine-payment flow over MPP (no on-chain VM dispatch)
505    MppPayment { endpoint: String },
506
507    /// Pure machine-payment flow over x402 (no on-chain VM dispatch)
508    X402Payment { endpoint: String },
509
510    /// Cross-chain bridge workflow via `BridgeRouter`
511    Bridge {
512        /// Adapter ids registered with the router, e.g.
513        /// `["layer_zero", "chainlink_ccip", "debridge"]`
514        adapters: Vec<String>,
515    },
516}
517
518/// Delegation scope to provision for a spawned agent. Translated into
519/// `tenzro_identity::DelegationScope` by the spawner.
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521pub struct DelegationSpec {
522    /// Maximum value of a single transaction (raw u128, base units).
523    /// Accepts JSON numbers (≤ u64::MAX) or strings (any u128). Serialized
524    /// as a number when it fits u64 and as a decimal string otherwise.
525    #[serde(with = "u128_serde")]
526    pub max_transaction_value: u128,
527
528    /// Maximum cumulative spend per day (raw u128, base units).
529    /// Accepts JSON numbers (≤ u64::MAX) or strings (any u128). Serialized
530    /// as a number when it fits u64 and as a decimal string otherwise.
531    #[serde(with = "u128_serde")]
532    pub max_daily_spend: u128,
533
534    /// Operation labels the agent is allowed to perform
535    /// (e.g. `["bridge", "deposit", "rebalance"]`)
536    pub allowed_operations: Vec<String>,
537
538    /// Chain identifiers the agent is allowed to reach
539    /// (e.g. `["ethereum", "arbitrum", "base"]`)
540    #[serde(default)]
541    pub allowed_chains: Vec<String>,
542
543    /// Time bound for the delegation, in days from spawn time
544    pub time_bound_days: u32,
545
546    /// Required KYC tier on the controller identity:
547    /// `"Unverified" | "Basic" | "Enhanced" | "Full"`
548    pub kyc_tier: String,
549}
550
551/// Hard caps enforced by the runtime as a second line of defense
552/// independent of the delegation scope.
553#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
554pub struct HardCaps {
555    /// Per-operation value cap (raw u128, base units).
556    /// Accepts JSON numbers (≤ u64::MAX) or strings (any u128).
557    #[serde(with = "u128_serde")]
558    pub per_operation_value: u128,
559
560    /// Per-day cumulative value cap (raw u128, base units).
561    /// Accepts JSON numbers (≤ u64::MAX) or strings (any u128).
562    #[serde(with = "u128_serde")]
563    pub per_day_value: u128,
564}
565
566/// One account in an [`ExecutionStep::SvmDispatch`] instruction.
567///
568/// Mirrors `solana_sdk::instruction::AccountMeta` byte-for-byte:
569/// `(pubkey, is_signer, is_writable)`. Solana programs read accounts
570/// positionally — order is part of the program's ABI and the
571/// declaration order in the template must match the program's
572/// expectations.
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct SvmAccountMeta {
575    /// Base58 account address. Supports `{{var}}` substitution at
576    /// run time.
577    pub pubkey: String,
578
579    /// Whether this account must sign the transaction. The agent's
580    /// own wallet pubkey is supplied as the fee-payer signer by the
581    /// node handler and does NOT need to be listed here.
582    #[serde(default)]
583    pub is_signer: bool,
584
585    /// Whether the program may write to this account's data or
586    /// lamports. Accounts not declared writable are passed read-only.
587    #[serde(default)]
588    pub is_writable: bool,
589}
590
591/// A single declarative step in an `ExecutionSpec`. Each variant maps
592/// directly to an existing subsystem invocation.
593///
594/// Template variables: any `String` field that contains `{{name}}`
595/// substrings is interpolated at run time against the agent's context
596/// (spawn args + accumulated step output).
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
598pub enum ExecutionStep {
599    /// Hand-craft an EVM transaction and dispatch it through
600    /// `MultiVmRuntime::execute_transaction(VmType::Evm, ...)`.
601    EvmDispatch {
602        /// 20-byte hex `to` address (with or without `0x` prefix)
603        to: String,
604        /// Hex-encoded u128 value
605        value: String,
606        /// Hex-encoded calldata template with optional `{{var}}` substitutions
607        calldata_template: String,
608        gas_limit: u64,
609    },
610
611    /// Hand-craft a Solana-style instruction and dispatch it through
612    /// `MultiVmRuntime::execute_transaction(VmType::Svm, ...)`.
613    ///
614    /// Mirrors the canonical `solana_sdk::instruction::Instruction`
615    /// shape — a program id, a list of account metas, and an opaque
616    /// instruction-data byte buffer. The executor renders the data
617    /// buffer via the same `{{var}}` substitution rule used by
618    /// `EvmDispatch::calldata_template`, then routes the assembled
619    /// transaction to `tenzro_svmDispatch` (DPoP-authenticated).
620    SvmDispatch {
621        /// Base58 program id (Solana account address of the program
622        /// to invoke). Substituted against the agent context at run
623        /// time, so deployers may parameterize via `{{program_id}}`.
624        program_id: String,
625
626        /// Ordered account metas — order matters; the program reads
627        /// them positionally. Each meta names the account, whether it
628        /// signs the transaction, and whether the program may mutate
629        /// it. Each `pubkey` string supports `{{var}}` substitution.
630        accounts: Vec<SvmAccountMeta>,
631
632        /// Hex-encoded instruction-data template. `0x` prefix
633        /// optional. Substituted with `{{var}}` before being decoded
634        /// to bytes and handed to the program. The agent's wallet
635        /// public key is always implicitly the fee payer and is added
636        /// as the first signer account by the node's
637        /// `tenzro_svmDispatch` handler — the template does not need
638        /// to encode it.
639        instruction_data_template: String,
640    },
641
642    /// Invoke an arbitrary node JSON-RPC method with the agent's DPoP
643    /// credentials. Covers the saga `tenzro_workflow*` family, the task
644    /// lifecycle (`tenzro_postTask` / `tenzro_completeTask` / …),
645    /// `tenzro_verifyDidEnvelope`, and any other RPC the node exposes — the
646    /// runtime's general escape hatch so agent templates can drive newly-added
647    /// surfaces without a bespoke step kind. `params` is a JSON template with
648    /// `{{var}}` substitution against the agent context.
649    NodeRpc {
650        /// JSON-RPC method name (e.g. `"tenzro_workflowOpen"`).
651        method: String,
652        /// Params object/array template; `{{var}}`-substituted at run time.
653        #[serde(default)]
654        params: serde_json::Value,
655    },
656
657    /// Move tokens across chains via `BridgeRouter::compare_fees` →
658    /// pick the chosen route → call the winning adapter.
659    BridgeTransfer {
660        from_chain: String,
661        to_chain: String,
662        asset: String,
663        /// Decimal amount string (resolved against asset decimals at run time)
664        amount: String,
665        /// Selection strategy: `"cheapest" | "fastest" | "most_reliable"`
666        strategy: String,
667    },
668
669    /// Create an MPP payment challenge, sign it with the agent wallet,
670    /// and submit the credential via `MppClient`.
671    MppPay {
672        amount: String,
673        asset: String,
674        recipient: String,
675        /// Optional session TTL in seconds
676        #[serde(default)]
677        session_ttl_secs: Option<u64>,
678    },
679
680    /// Stateless x402 pay-resource call via `X402Client`.
681    X402Pay {
682        /// Resource URL to pay for (alternative to `recipient`)
683        #[serde(default)]
684        resource_url: Option<String>,
685        amount: String,
686        asset: String,
687        /// Recipient address (alternative to resource_url)
688        #[serde(default)]
689        recipient: Option<String>,
690        /// Facilitator address
691        #[serde(default)]
692        facilitator: Option<String>,
693    },
694
695    /// Submit a DAML command to a Canton participant via `DamlExecutor`.
696    DamlSubmit {
697        /// Template package (e.g. "com.tenzro.trade")
698        #[serde(default)]
699        template_package: Option<String>,
700        /// DAML module name
701        #[serde(default)]
702        module: Option<String>,
703        /// DAML entity/template name
704        #[serde(default)]
705        entity: Option<String>,
706        /// Command variant: "Create" | "Exercise" etc.
707        #[serde(default)]
708        command_variant: Option<String>,
709        /// JSON object with optional `{{var}}` substitutions
710        #[serde(default)]
711        fields_json: Option<String>,
712        /// Template ID (alternative to template_package+module+entity)
713        #[serde(default)]
714        template_id: Option<String>,
715        /// Choice name (alternative to command_variant for Exercise)
716        #[serde(default)]
717        choice: Option<String>,
718        /// Acting party
719        #[serde(default)]
720        party: Option<String>,
721        /// Args template with `{{var}}` substitutions (alternative to fields_json)
722        #[serde(default)]
723        args_template: Option<String>,
724    },
725
726    /// Look up a tool from `CF_TOOLS` (auto-discovered at spawn time
727    /// via `required_tool_tags`) and invoke it via `tenzro_useTool`.
728    ToolInvoke {
729        /// Tag used to look up the resolved tool id
730        tool_tag: String,
731        /// Method name on the tool (e.g. "get_balances", "quote_inference_pricing")
732        #[serde(default)]
733        method: Option<String>,
734        /// JSON object with optional `{{var}}` substitutions (alternative
735        /// to `params_template`)
736        #[serde(default)]
737        arguments_json: Option<String>,
738        /// JSON template with optional `{{var}}` substitutions (preferred)
739        #[serde(default)]
740        params_template: Option<String>,
741    },
742
743    /// Look up a skill from `CF_SKILLS` (auto-discovered at spawn time
744    /// via `required_skill_tags`) and invoke it via `tenzro_useSkill`.
745    SkillInvoke {
746        /// Tag used to look up the resolved skill id
747        skill_tag: String,
748        /// Method name on the skill
749        #[serde(default)]
750        method: Option<String>,
751        /// JSON object with optional `{{var}}` substitutions
752        #[serde(default)]
753        input_json: Option<String>,
754        /// JSON template with optional `{{var}}` substitutions (alternative)
755        #[serde(default)]
756        params_template: Option<String>,
757    },
758
759    /// Loop over a discovered opportunity set returned by a registered
760    /// tool. The body steps execute once per item, with the item bound
761    /// to the runtime context as `opportunity` (or whatever the agent-kit
762    /// chooses for binding).
763    ForEach {
764        /// Tool tag whose `tenzro_useTool` output is a JSON array
765        source_tool_tag: String,
766        body: Vec<ExecutionStep>,
767    },
768
769    /// Conditional gate: only run the body if the JMESPath expression
770    /// evaluated against the current context is truthy.
771    When {
772        /// JMESPath expression
773        jmespath_expr: String,
774        body: Vec<ExecutionStep>,
775    },
776}
777
778#[cfg(test)]
779mod execution_spec_tests {
780    use super::*;
781
782    #[test]
783    fn execution_spec_round_trips_through_json() {
784        let spec = ExecutionSpec {
785            backend: ExecutionBackend::Bridge {
786                adapters: vec!["layer_zero".to_string(), "debridge".to_string()],
787            },
788            delegation: DelegationSpec {
789                max_transaction_value: 500_000_000_000_000_000_000u128,
790                max_daily_spend: 2_000_000_000_000_000_000_000u128,
791                allowed_operations: vec!["bridge".to_string(), "deposit".to_string()],
792                allowed_chains: vec!["ethereum".to_string(), "arbitrum".to_string()],
793                time_bound_days: 7,
794                kyc_tier: "Full".to_string(),
795            },
796            required_tool_tags: vec!["yield-source".to_string()],
797            required_skill_tags: vec![],
798            hard_caps: HardCaps {
799                per_operation_value: 500_000_000_000_000_000_000u128,
800                per_day_value: 2_000_000_000_000_000_000_000u128,
801            },
802            steps: vec![ExecutionStep::ForEach {
803                source_tool_tag: "yield-source".to_string(),
804                body: vec![ExecutionStep::When {
805                    jmespath_expr: "apy > `5.0`".to_string(),
806                    body: vec![ExecutionStep::BridgeTransfer {
807                        from_chain: "ethereum".to_string(),
808                        to_chain: "{{opportunity.chain}}".to_string(),
809                        asset: "USDC".to_string(),
810                        amount: "{{opportunity.amount}}".to_string(),
811                        strategy: "cheapest".to_string(),
812                    }],
813                }],
814            }],
815        };
816
817        let json = serde_json::to_string(&spec).expect("serialize");
818        let back: ExecutionSpec = serde_json::from_str(&json).expect("deserialize");
819        assert_eq!(back, spec);
820    }
821
822    #[test]
823    fn agent_template_without_execution_spec_still_deserializes() {
824        // A template without execution_spec must round-trip cleanly through
825        // serde. We build it from a real `AgentTemplate::new()` (so the
826        // Address shape is correct), then serialize, strip the
827        // execution_spec field from the JSON, then deserialize the stripped
828        // JSON and confirm the field defaults to None.
829        let original = AgentTemplate::new(
830            "Spec-less".to_string(),
831            "no execution_spec field".to_string(),
832            AgentTemplateType::Autonomous,
833            crate::primitives::Address::default(),
834            "".to_string(),
835        );
836        let mut value: serde_json::Value = serde_json::to_value(&original).unwrap();
837        // Strip the new field as if this JSON came from a pre-execution-spec node.
838        let removed = value
839            .as_object_mut()
840            .unwrap()
841            .remove("execution_spec");
842        assert!(removed.is_some(), "execution_spec should have been present");
843        let stripped = serde_json::to_string(&value).unwrap();
844        // Confirm the key is no longer in the JSON object (the field is fully absent,
845        // not just `null`).
846        assert!(
847            !stripped.contains("\"execution_spec\""),
848            "execution_spec key should be absent from stripped JSON"
849        );
850
851        let tmpl: AgentTemplate =
852            serde_json::from_str(&stripped).expect("spec-less template should deserialize");
853        assert!(tmpl.execution_spec.is_none());
854        assert_eq!(tmpl.name, "Spec-less");
855    }
856
857    #[test]
858    fn new_template_initializes_execution_spec_to_none() {
859        let tmpl = AgentTemplate::new(
860            "Test".to_string(),
861            "desc".to_string(),
862            AgentTemplateType::Autonomous,
863            crate::primitives::Address::default(),
864            "system prompt".to_string(),
865        );
866        assert!(tmpl.execution_spec.is_none());
867    }
868
869    #[test]
870    fn validate_rejects_paid_template_without_wallet() {
871        let mut tmpl = AgentTemplate::new(
872            "Paid".to_string(),
873            "desc".to_string(),
874            AgentTemplateType::Autonomous,
875            crate::primitives::Address::default(),
876            "prompt".to_string(),
877        );
878        tmpl.pricing = AgentPricingModel::PerExecution { price: 1_000 };
879        // creator_wallet is None by default
880        assert!(tmpl.validate_marketplace_invariants().is_err());
881    }
882
883    #[test]
884    fn validate_accepts_paid_template_with_wallet() {
885        let mut tmpl = AgentTemplate::new(
886            "Paid".to_string(),
887            "desc".to_string(),
888            AgentTemplateType::Autonomous,
889            crate::primitives::Address::default(),
890            "prompt".to_string(),
891        );
892        tmpl.pricing = AgentPricingModel::PerExecution { price: 1_000 };
893        tmpl.creator_wallet = Some(crate::primitives::Address::default());
894        assert!(tmpl.validate_marketplace_invariants().is_ok());
895    }
896
897    #[test]
898    fn validate_accepts_free_template_without_wallet() {
899        let tmpl = AgentTemplate::new(
900            "Free".to_string(),
901            "desc".to_string(),
902            AgentTemplateType::Autonomous,
903            crate::primitives::Address::default(),
904            "prompt".to_string(),
905        );
906        // Free + no wallet is valid
907        assert!(tmpl.validate_marketplace_invariants().is_ok());
908    }
909
910    #[test]
911    fn record_invocation_increments_counters() {
912        let mut tmpl = AgentTemplate::new(
913            "Paid".to_string(),
914            "desc".to_string(),
915            AgentTemplateType::Autonomous,
916            crate::primitives::Address::default(),
917            "prompt".to_string(),
918        );
919        assert_eq!(tmpl.invocation_count, 0);
920        assert_eq!(tmpl.total_revenue, 0);
921        tmpl.record_invocation(950_000_000_000_000_000u128);
922        assert_eq!(tmpl.invocation_count, 1);
923        assert_eq!(tmpl.total_revenue, 950_000_000_000_000_000u128);
924        tmpl.record_invocation(950_000_000_000_000_000u128);
925        assert_eq!(tmpl.invocation_count, 2);
926        assert_eq!(tmpl.total_revenue, 1_900_000_000_000_000_000u128);
927    }
928
929    #[test]
930    fn fee_for_invocation_computes_correctly() {
931        let free = AgentPricingModel::Free;
932        assert_eq!(free.fee_for_invocation(1000), 0);
933
934        let per_exec = AgentPricingModel::PerExecution { price: 500_000_000_000_000_000u128 };
935        assert_eq!(per_exec.fee_for_invocation(1000), 500_000_000_000_000_000u128);
936
937        let per_token = AgentPricingModel::PerToken { price_per_token: 1_000_000_000u128 };
938        assert_eq!(per_token.fee_for_invocation(500), 500_000_000_000u128);
939
940        let subscription = AgentPricingModel::Subscription { monthly_rate: 1_000_000u128 };
941        assert_eq!(subscription.fee_for_invocation(1000), 0); // settled out-of-band
942    }
943}