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}