fleetmesh-node 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,840 @@
1
+ """Autonomous ReAct agent harness: Reasoning + Action loop with tool dispatch and recursive delegation.
2
+
3
+ Architecture:
4
+ - AgentExecutionContext: State container holding task, budget caps, blob store, and delegation dispatcher.
5
+ - AgentToolProtocol & Standard Tools: Calculator, Regex, Sha256, FetchBlob, SubAsk, and Finish.
6
+ - AgentBackendProtocol: Decides next step (Thought, Action, Action Input) via LLM or scripted plan.
7
+ - ReAct Loop: Bounded by max_steps and fuel_cap, with step-level telemetry and honesty gate.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ import json
13
+ import operator
14
+ import re
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Callable, Mapping, Optional, Protocol, Sequence, TypedDict
17
+
18
+ from fleetmesh_core.crypto import crypto
19
+ from ..common import ComputeError, resolve_blob
20
+ from ..sandbox import ComputeLimits
21
+ from ..types import AgentSubAsk, AgentTraceStep
22
+ from .base import ComputeExecutionResult, ComputeExecutor, ComputeQuote
23
+ from .harness import (
24
+ AgentHarnessRegistry,
25
+ AgentProfileSpec,
26
+ )
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Context & Protocols
31
+ # ---------------------------------------------------------------------------
32
+
33
+ @dataclass
34
+ class AgentExecutionContext:
35
+ """Runtime execution context provided to tools during an agent reasoning step."""
36
+ task: str
37
+ delegation: Mapping[str, Any] = field(default_factory=dict)
38
+ budget_cap_msat: int = 0
39
+ budget_spent_msat: int = 0
40
+ blob_store: Optional[Any] = None
41
+ blossom_servers: list[str] = field(default_factory=list)
42
+ sub_ask_dispatcher: Optional[Callable[[Mapping[str, Any]], Mapping[str, Any]]] = None
43
+ fuel_cap: int = 100_000
44
+ fuel_used: int = 0
45
+ sub_asks_dispatched: list[AgentSubAsk] = field(default_factory=list)
46
+ depth: int = 0
47
+ harness_profile: Optional[AgentProfileSpec] = None
48
+ domain_context: Mapping[str, Any] = field(default_factory=dict)
49
+
50
+ def get_remaining_budget(self) -> int:
51
+ """Remaining msat budget permitted for child sub-delegation asks."""
52
+ return max(0, self.budget_cap_msat - self.budget_spent_msat)
53
+
54
+ def deduct_delegation(self, amount_msat: int) -> int:
55
+ """Atomically deduct allocated msat from remaining budget and return granted amount."""
56
+ granted = min(self.get_remaining_budget(), amount_msat)
57
+ if granted > 0:
58
+ self.budget_spent_msat += granted
59
+ return max(0, granted)
60
+
61
+ def record_sub_ask(self, sub_ask: AgentSubAsk) -> None:
62
+ """Record dispatched child ask into session telemetry."""
63
+ self.sub_asks_dispatched.append(sub_ask)
64
+
65
+
66
+ class AgentToolResult(TypedDict, total=False):
67
+ """Result returned by an agent tool execution."""
68
+ output: Any
69
+ error: Optional[str]
70
+ fuel_consumed: int
71
+ sub_ask_id: Optional[str]
72
+
73
+
74
+ class AgentToolProtocol(Protocol):
75
+ """Interface for agent-invocable tools."""
76
+ name: str
77
+ description: str
78
+ parameters_schema: Mapping[str, Any]
79
+
80
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
81
+ ...
82
+
83
+
84
+ @dataclass
85
+ class AgentDecision:
86
+ """Action decision formulated by an agent reasoning backend."""
87
+ thought: str
88
+ action: str
89
+ action_input: Any = field(default_factory=dict)
90
+ is_finish: bool = False
91
+ final_answer: Optional[str] = None
92
+
93
+
94
+ class AgentBackendProtocol(Protocol):
95
+ """Reasoning backend providing Thought and Action generation for the ReAct loop."""
96
+ def is_live_runnable(self) -> bool:
97
+ ...
98
+
99
+ def decide_step(
100
+ self,
101
+ task: str,
102
+ trace: Sequence[AgentTraceStep],
103
+ tools: Mapping[str, AgentToolProtocol],
104
+ context: AgentExecutionContext,
105
+ ) -> AgentDecision:
106
+ ...
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Standard Built-in Tools
111
+ # ---------------------------------------------------------------------------
112
+
113
+ class CalculatorTool:
114
+ """Safe arithmetic expression evaluator using AST inspection (no arbitrary code execution)."""
115
+ name: str = 'calc'
116
+ description: str = 'Safely evaluates basic arithmetic expressions (+, -, *, /, //, %, **, abs, round, min, max).'
117
+ parameters_schema: Mapping[str, Any] = {
118
+ 'type': 'object',
119
+ 'properties': {
120
+ 'expression': {'type': 'string', 'description': 'Mathematical expression to evaluate'},
121
+ },
122
+ 'required': ['expression'],
123
+ }
124
+
125
+ _ALLOWED_OPS: Mapping[type[Any], Callable[..., Any]] = {
126
+ ast.Add: operator.add,
127
+ ast.Sub: operator.sub,
128
+ ast.Mult: operator.mul,
129
+ ast.Div: operator.truediv,
130
+ ast.FloorDiv: operator.floordiv,
131
+ ast.Mod: operator.mod,
132
+ ast.Pow: operator.pow,
133
+ ast.USub: operator.neg,
134
+ ast.UAdd: operator.pos,
135
+ }
136
+
137
+ _ALLOWED_FUNCS: Mapping[str, Callable[..., Any]] = {
138
+ 'abs': abs,
139
+ 'round': round,
140
+ 'min': min,
141
+ 'max': max,
142
+ }
143
+
144
+ def _eval_node(self, node: ast.AST) -> float | int:
145
+ if isinstance(node, ast.Expression):
146
+ return self._eval_node(node.body)
147
+ if isinstance(node, ast.Constant):
148
+ if type(node.value) is bool:
149
+ raise ValueError("Boolean constants are not allowed in calculator expressions")
150
+ if isinstance(node.value, (int, float)):
151
+ return node.value
152
+ raise ValueError(f"Constant {node.value!r} is not a numeric value")
153
+ if isinstance(node, ast.UnaryOp):
154
+ unary_op_type = type(node.op)
155
+ if unary_op_type in self._ALLOWED_OPS:
156
+ return self._ALLOWED_OPS[unary_op_type](self._eval_node(node.operand))
157
+ raise ValueError(f"Unsupported unary operator: {unary_op_type.__name__}")
158
+ if isinstance(node, ast.BinOp):
159
+ bin_op_type = type(node.op)
160
+ if bin_op_type in self._ALLOWED_OPS:
161
+ left = self._eval_node(node.left)
162
+ right = self._eval_node(node.right)
163
+ if bin_op_type in (ast.Div, ast.FloorDiv, ast.Mod) and right == 0:
164
+ raise ZeroDivisionError("Division by zero")
165
+ if bin_op_type == ast.Pow:
166
+ if abs(right) > 500:
167
+ raise ValueError("Exponent too large (abs > 500)")
168
+ if abs(left) > 10_000 and right > 10:
169
+ raise ValueError("Base too large for power operation")
170
+ return self._ALLOWED_OPS[bin_op_type](left, right)
171
+ raise ValueError(f"Unsupported binary operator: {bin_op_type.__name__}")
172
+ if isinstance(node, ast.Call):
173
+ if isinstance(node.func, ast.Name) and node.func.id in self._ALLOWED_FUNCS:
174
+ args = [self._eval_node(a) for a in node.args]
175
+ return self._ALLOWED_FUNCS[node.func.id](*args)
176
+ raise ValueError(f"Function call not allowed: {getattr(node.func, 'id', 'unknown')}")
177
+ raise ValueError(f"Expression contains disallowed syntax: {type(node).__name__}")
178
+
179
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
180
+ if isinstance(params, str):
181
+ expr = params.strip()
182
+ elif isinstance(params, Mapping):
183
+ expr = str(params.get('expression') or params.get('expr') or params.get('formula') or params.get('math') or params.get('raw') or '').strip()
184
+ else:
185
+ expr = str(params).strip()
186
+
187
+ if not expr:
188
+ return {'error': 'Missing expression parameter', 'fuel_consumed': 10}
189
+ try:
190
+ tree = ast.parse(expr, mode='eval')
191
+ val = self._eval_node(tree)
192
+ return {'output': val, 'fuel_consumed': 50}
193
+ except Exception as e:
194
+ return {'error': f"Evaluation error: {e}", 'fuel_consumed': 20}
195
+
196
+
197
+ class Sha256Tool:
198
+ """Cryptographic SHA-256 hash calculator."""
199
+ name: str = 'sha256'
200
+ description: str = 'Computes SHA-256 digest of input text or binary hex.'
201
+ parameters_schema: Mapping[str, Any] = {
202
+ 'type': 'object',
203
+ 'properties': {
204
+ 'data': {'type': 'string', 'description': 'Input string or hex string'},
205
+ 'is_hex': {'type': 'boolean', 'description': 'True if data is hex-encoded'},
206
+ },
207
+ 'required': ['data'],
208
+ }
209
+
210
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
211
+ if isinstance(params, str):
212
+ raw_data = params
213
+ is_hex = False
214
+ elif isinstance(params, Mapping):
215
+ raw_data = params.get('data') or params.get('text') or params.get('input') or params.get('content') or params.get('raw') or ''
216
+ is_hex = bool(params.get('is_hex', False))
217
+ else:
218
+ raw_data = str(params)
219
+ is_hex = False
220
+
221
+ try:
222
+ b = bytes.fromhex(str(raw_data)) if is_hex else str(raw_data).encode('utf-8')
223
+ digest = crypto.sha256(b)
224
+ return {'output': digest, 'fuel_consumed': 25 + len(b) // 64}
225
+ except Exception as e:
226
+ return {'error': f"Hashing failed: {e}", 'fuel_consumed': 10}
227
+
228
+
229
+ class RegexSearchTool:
230
+ """Safe regular expression pattern search and extraction tool."""
231
+ name: str = 'regex_search'
232
+ description: str = 'Searches text using a regular expression pattern and returns matches.'
233
+ parameters_schema: Mapping[str, Any] = {
234
+ 'type': 'object',
235
+ 'properties': {
236
+ 'pattern': {'type': 'string', 'description': 'Regex pattern to match'},
237
+ 'text': {'type': 'string', 'description': 'Target text to search'},
238
+ },
239
+ 'required': ['pattern', 'text'],
240
+ }
241
+
242
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
243
+ if not isinstance(params, Mapping):
244
+ return {'error': 'Parameters must be an object with pattern and text', 'fuel_consumed': 10}
245
+ pattern = str(params.get('pattern') or params.get('regex') or '')
246
+ text = str(params.get('text') or params.get('target') or params.get('string') or params.get('input') or params.get('raw') or '')
247
+ if not pattern:
248
+ return {'error': 'Missing pattern parameter', 'fuel_consumed': 10}
249
+ if len(pattern) > 256 or len(text) > 65536:
250
+ return {'error': 'Regex pattern or text exceeds safety bounds', 'fuel_consumed': 20}
251
+ try:
252
+ matches = re.findall(pattern, text)
253
+ return {'output': matches, 'fuel_consumed': 40 + len(text) // 1024}
254
+ except Exception as e:
255
+ return {'error': f"Regex error: {e}", 'fuel_consumed': 15}
256
+
257
+
258
+ class FetchBlobTool:
259
+ """Blossom CAS blob retrieval tool."""
260
+ name: str = 'fetch_blob'
261
+ description: str = 'Retrieves a content-addressed blob by SHA-256 hash or CID from the local store or Blossom network.'
262
+ parameters_schema: Mapping[str, Any] = {
263
+ 'type': 'object',
264
+ 'properties': {
265
+ 'sha256': {'type': 'string', 'description': 'SHA-256 hex digest of the blob'},
266
+ },
267
+ 'required': ['sha256'],
268
+ }
269
+
270
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
271
+ if isinstance(params, str):
272
+ sha = params.strip().lower()
273
+ elif isinstance(params, Mapping):
274
+ sha = str(params.get('sha256') or params.get('cid') or params.get('hash') or params.get('digest') or params.get('blob_id') or params.get('raw') or '').strip().lower()
275
+ else:
276
+ sha = str(params).strip().lower()
277
+
278
+ if not sha:
279
+ return {'error': 'Missing sha256 or cid parameter', 'fuel_consumed': 10}
280
+ try:
281
+ blob_bytes = resolve_blob(
282
+ {'sha256': sha},
283
+ blob_store=context.blob_store,
284
+ blossom_servers=context.blossom_servers,
285
+ )
286
+ if blob_bytes is None:
287
+ return {'error': f"Blob not found: {sha}", 'fuel_consumed': 50}
288
+ preview = blob_bytes[:256].decode('utf-8', errors='replace')
289
+ return {
290
+ 'output': {
291
+ 'sha256': sha,
292
+ 'size_bytes': len(blob_bytes),
293
+ 'preview': preview,
294
+ },
295
+ 'fuel_consumed': 100 + len(blob_bytes) // 1024,
296
+ }
297
+ except Exception as e:
298
+ return {'error': f"Blob resolution error: {e}", 'fuel_consumed': 25}
299
+
300
+
301
+ class SubAskTool:
302
+ """Recursive child-Ask mesh delegation tool."""
303
+ name: str = 'sub_ask'
304
+ description: str = 'Dispatches a delegated sub-computation ask (Kind 30810) to peer nodes across the mesh.'
305
+ parameters_schema: Mapping[str, Any] = {
306
+ 'type': 'object',
307
+ 'properties': {
308
+ 'task': {'type': 'string', 'description': 'Sub-task description or prompt'},
309
+ 'engine': {'type': 'string', 'description': 'Target execution engine (e.g. wasm, llm, zkvm)'},
310
+ 'budget_msat': {'type': 'integer', 'description': 'Maximum msat budget allocated to child ask'},
311
+ },
312
+ 'required': ['task'],
313
+ }
314
+
315
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
316
+ if not context.delegation.get('allowSubAsks', False):
317
+ return {'error': 'Sub-ask delegation is not permitted for this job', 'fuel_consumed': 50}
318
+ if context.depth >= 3:
319
+ return {'error': 'Maximum recursive delegation depth exceeded (depth=3)', 'fuel_consumed': 50}
320
+
321
+ if isinstance(params, str):
322
+ sub_task = params.strip()
323
+ engine = 'wasm'
324
+ requested_budget = 1000
325
+ elif isinstance(params, Mapping):
326
+ sub_task = str(params.get('task') or params.get('prompt') or params.get('description') or params.get('goal') or params.get('raw') or '').strip()
327
+ engine = str(params.get('engine') or 'wasm')
328
+ requested_budget = int(params.get('budget_msat') or params.get('budgetCapMsat') or params.get('budget') or 1000)
329
+ else:
330
+ sub_task = str(params).strip()
331
+ engine = 'wasm'
332
+ requested_budget = 1000
333
+
334
+ if not sub_task:
335
+ return {'error': 'Missing task description for sub-ask', 'fuel_consumed': 10}
336
+
337
+ remaining_budget = context.get_remaining_budget()
338
+ if requested_budget > remaining_budget:
339
+ requested_budget = remaining_budget
340
+ if requested_budget <= 0:
341
+ return {'error': 'Delegation budget ceiling exhausted', 'fuel_consumed': 50}
342
+
343
+ granted = context.deduct_delegation(requested_budget)
344
+
345
+ ask_id = f"sub_ask_{crypto.sha256(sub_task.encode('utf-8'))[:16]}"
346
+ child_ask_payload = {
347
+ 'ask_id': ask_id,
348
+ 'task': sub_task,
349
+ 'engine': engine,
350
+ 'budget_msat': granted,
351
+ 'depth': context.depth + 1,
352
+ }
353
+
354
+ if context.sub_ask_dispatcher:
355
+ try:
356
+ dispatch_res = context.sub_ask_dispatcher(child_ask_payload)
357
+ status = dispatch_res.get('status', 'completed')
358
+ result_hash = dispatch_res.get('result_hash', crypto.sha256(sub_task.encode('utf-8')))
359
+ except Exception as e:
360
+ return {'error': f"Sub-ask dispatch failed: {e}", 'fuel_consumed': 100}
361
+ else:
362
+ # Standalone / simulated sub-computation
363
+ status = 'dispatched'
364
+ result_hash = crypto.sha256(f"sub_result:{ask_id}".encode('utf-8'))
365
+
366
+ sub_ask_record: AgentSubAsk = {
367
+ 'ask_id': ask_id,
368
+ 'task': sub_task,
369
+ 'budget_msat': granted,
370
+ 'status': status,
371
+ }
372
+ context.record_sub_ask(sub_ask_record)
373
+
374
+ return {
375
+ 'output': {
376
+ 'ask_id': ask_id,
377
+ 'status': status,
378
+ 'budget_allocated_msat': granted,
379
+ 'result_hash': result_hash,
380
+ 'summary': f"Child task {ask_id} dispatched",
381
+ },
382
+ 'sub_ask_id': ask_id,
383
+ 'fuel_consumed': 250,
384
+ }
385
+
386
+
387
+ class FinishTool:
388
+ """Terminal tool to conclude the ReAct loop and return the final synthesized answer."""
389
+ name: str = 'finish'
390
+ description: str = 'Concludes execution and provides the final synthesized answer.'
391
+ parameters_schema: Mapping[str, Any] = {
392
+ 'type': 'object',
393
+ 'properties': {
394
+ 'answer': {'type': 'string', 'description': 'Final synthesized answer or output'},
395
+ },
396
+ 'required': ['answer'],
397
+ }
398
+
399
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
400
+ if isinstance(params, str):
401
+ answer = params.strip()
402
+ elif isinstance(params, Mapping):
403
+ answer = str(params.get('answer') or params.get('final_answer') or params.get('output') or params.get('result') or params.get('summary') or params.get('response') or params.get('raw') or 'Agent workflow completed successfully').strip()
404
+ else:
405
+ answer = str(params).strip()
406
+ return {'output': answer or 'Agent workflow completed successfully', 'fuel_consumed': 50}
407
+
408
+
409
+ # ---------------------------------------------------------------------------
410
+ # Reasoning Backends
411
+ # ---------------------------------------------------------------------------
412
+
413
+ class ScriptedAgentBackend:
414
+ """Deterministic ReAct reasoning backend for testing, fixtures, and scripted workflows."""
415
+
416
+ def __init__(self, scripted_steps: Optional[Sequence[AgentDecision]] = None) -> None:
417
+ self.scripted_steps = list(scripted_steps) if scripted_steps else None
418
+ self._step_index = 0
419
+
420
+ def is_live_runnable(self) -> bool:
421
+ return True
422
+
423
+ def decide_step(
424
+ self,
425
+ task: str,
426
+ trace: Sequence[AgentTraceStep],
427
+ tools: Mapping[str, AgentToolProtocol],
428
+ context: AgentExecutionContext,
429
+ ) -> AgentDecision:
430
+ if self.scripted_steps and self._step_index < len(self.scripted_steps):
431
+ dec = self.scripted_steps[self._step_index]
432
+ self._step_index += 1
433
+ return dec
434
+
435
+ step_num = len(trace) + 1
436
+
437
+ # Heuristic 3-step loop maintaining full backwards compatibility
438
+ if step_num == 1:
439
+ return AgentDecision(
440
+ thought=f"Analyze task requirements: '{task[:64]}'",
441
+ action='decompose_task',
442
+ action_input={'task': task},
443
+ )
444
+
445
+ if step_num == 2:
446
+ allow_sub_asks = bool(context.delegation.get('allowSubAsks', False))
447
+ budget_cap = int(context.delegation.get('budgetCapMsat', 0))
448
+ if allow_sub_asks and budget_cap > 0:
449
+ child_budget = min(10000, budget_cap)
450
+ return AgentDecision(
451
+ thought='Sub-ask delegation permitted; dispatched child computation ask',
452
+ action='sub_ask',
453
+ action_input={'task': f"Sub-computation for: {task[:48]}", 'budget_msat': child_budget},
454
+ )
455
+ if 'calc' in task or any(c in task for c in ('+', '*', '/', '-')):
456
+ return AgentDecision(
457
+ thought='Task requires mathematical calculation',
458
+ action='calc',
459
+ action_input={'expression': '21 * 2'},
460
+ )
461
+ return AgentDecision(
462
+ thought='Synthesize final result from observations',
463
+ action='finish',
464
+ action_input={'answer': f"Completed execution of goal: '{task[:64]}'"},
465
+ is_finish=True,
466
+ )
467
+
468
+ # Final step
469
+ return AgentDecision(
470
+ thought='Synthesize final result from observations',
471
+ action='finish',
472
+ action_input={'answer': f"Completed execution of goal: '{task[:64]}'"},
473
+ is_finish=True,
474
+ )
475
+
476
+
477
+ class LlmAgentBackend:
478
+ """Live LLM-driven reasoning backend querying OpenAI/Anthropic/Routstr-compatible endpoints."""
479
+
480
+ def __init__(
481
+ self,
482
+ endpoint: str = 'http://127.0.0.1:8000/v1',
483
+ model: str = 'meta-llama/Llama-3.3-70B-Instruct',
484
+ timeout_seconds: float = 30.0,
485
+ api_key: str = '',
486
+ cashu_token: str = '',
487
+ provider_id: str = 'vllm',
488
+ provider_router: Optional[Any] = None,
489
+ ) -> None:
490
+ self.endpoint = endpoint.rstrip('/')
491
+ self.model = model
492
+ self.timeout_seconds = timeout_seconds
493
+ self.api_key = api_key
494
+ self.cashu_token = cashu_token
495
+ self.provider_id = provider_id
496
+ self.provider_router = provider_router
497
+
498
+ def _resolve_provider(self) -> Any:
499
+ from .providers import LinkedProviderConfig
500
+ if self.provider_router is not None:
501
+ resolved = self.provider_router.get_provider()
502
+ if resolved is not None:
503
+ return resolved
504
+ return LinkedProviderConfig(
505
+ provider_id=self.provider_id,
506
+ label=self.provider_id.upper(),
507
+ endpoint=self.endpoint,
508
+ default_model=self.model,
509
+ models=(self.model,),
510
+ api_key=self.api_key,
511
+ cashu_token=self.cashu_token,
512
+ auth_scheme='bearer' if self.api_key else ('cashu' if self.cashu_token else 'none'),
513
+ wire_format='anthropic_messages' if self.provider_id in ('claude', 'anthropic') else 'openai',
514
+ enabled=True,
515
+ )
516
+
517
+ def is_live_runnable(self) -> bool:
518
+ if self.provider_router is not None and self.provider_router.runnable_providers():
519
+ return True
520
+ return bool(self.endpoint and self.endpoint.startswith(('http://', 'https://')))
521
+
522
+ def decide_step(
523
+ self,
524
+ task: str,
525
+ trace: Sequence[AgentTraceStep],
526
+ tools: Mapping[str, AgentToolProtocol],
527
+ context: AgentExecutionContext,
528
+ ) -> AgentDecision:
529
+ tool_descriptions = [f"- {t.name}: {t.description}" for t in tools.values()]
530
+ react_instructions = (
531
+ "Given a goal, you think step-by-step, choose a tool, and inspect observations.\n"
532
+ f"Available tools:\n{chr(10).join(tool_descriptions)}\n\n"
533
+ "Format your response EXACTLY as:\n"
534
+ "Thought: <reasoning>\n"
535
+ "Action: <tool_name>\n"
536
+ "Action Input: <json_parameters_or_string>\n"
537
+ "When done, use Action: finish with Action Input: {\"answer\": \"...\"}."
538
+ )
539
+ if context.harness_profile is not None:
540
+ system_prompt = context.harness_profile.compile_system_prompt(
541
+ domain_context=context.domain_context,
542
+ extra_instructions=react_instructions,
543
+ )
544
+ temp = context.harness_profile.default_temperature
545
+ else:
546
+ system_prompt = "You are an autonomous ReAct reasoning agent on Fleetmesh.\n" + react_instructions
547
+ temp = 0.1
548
+
549
+ trace_lines = []
550
+ for s in trace:
551
+ trace_lines.append(f"Step {s['step']}:")
552
+ trace_lines.append(f"Thought: {s['thought']}")
553
+ trace_lines.append(f"Action: {s['action']}")
554
+ trace_lines.append(f"Observation: {s['observation']}")
555
+
556
+ user_content = f"Goal: {task}\n\nTrace so far:\n" + ("\n".join(trace_lines) if trace_lines else "None")
557
+
558
+ provider = self._resolve_provider()
559
+ api_route, req_body = provider.format_request(
560
+ messages=[
561
+ {'role': 'system', 'content': system_prompt},
562
+ {'role': 'user', 'content': user_content},
563
+ ],
564
+ model=provider.default_model or self.model,
565
+ max_tokens=512,
566
+ temperature=temp,
567
+ )
568
+ headers = provider.build_headers()
569
+
570
+ from node.transport import netfetch
571
+ try:
572
+ body_bytes, _ = netfetch.fetch(
573
+ api_route,
574
+ max_bytes=10 * 1024 * 1024,
575
+ headers=headers,
576
+ data=json.dumps(req_body).encode('utf-8'),
577
+ method='POST',
578
+ timeout=self.timeout_seconds,
579
+ allow_private=True,
580
+ )
581
+ data = json.loads(body_bytes.decode('utf-8'))
582
+ reply, _, _ = provider.parse_response(data)
583
+ except Exception as e:
584
+ raise ComputeError(f"Agent LLM backend request to {provider.endpoint} failed: {e}")
585
+
586
+ # Parse Thought / Action / Action Input from reply
587
+ thought = ''
588
+ action = 'finish'
589
+ action_input: Mapping[str, Any] = {}
590
+
591
+ t_m = re.search(r'Thought:\s*(.*?)(?=\nAction:|$)', reply, re.DOTALL)
592
+ if t_m:
593
+ thought = t_m.group(1).strip()
594
+ a_m = re.search(r'Action:\s*([a-zA-Z0-9_-]+)', reply)
595
+ if a_m:
596
+ action = a_m.group(1).strip()
597
+ i_m = re.search(r'Action Input:\s*(\{.*?\}|\[.*?\]|[^\n]+)', reply, re.DOTALL)
598
+ if i_m:
599
+ raw_in = i_m.group(1).strip()
600
+ if raw_in.startswith(('{', '[')):
601
+ try:
602
+ action_input = json.loads(raw_in)
603
+ except Exception:
604
+ action_input = {'raw': raw_in}
605
+ else:
606
+ action_input = {'raw': raw_in}
607
+
608
+ is_finish = action == 'finish'
609
+ return AgentDecision(
610
+ thought=thought or f"Proceeding with {action}",
611
+ action=action,
612
+ action_input=action_input,
613
+ is_finish=is_finish,
614
+ final_answer=action_input.get('answer') if is_finish else None,
615
+ )
616
+
617
+
618
+ # ---------------------------------------------------------------------------
619
+ # AgentExecutor
620
+ # ---------------------------------------------------------------------------
621
+
622
+ class AgentExecutor(ComputeExecutor):
623
+ """Autonomous ReAct agent harness (Reasoning + Action loop with tool dispatch)."""
624
+
625
+ engine_id: str = 'agent'
626
+ wasi_profile_digest: str = 'fleetmesh-react-v1'
627
+
628
+ def __init__(
629
+ self,
630
+ max_steps_default: int = 25,
631
+ mock_mode: bool = False,
632
+ backend: Optional[AgentBackendProtocol] = None,
633
+ tools: Optional[Sequence[AgentToolProtocol]] = None,
634
+ sub_ask_dispatcher: Optional[Callable[[Mapping[str, Any]], Mapping[str, Any]]] = None,
635
+ harness_registry: Optional[AgentHarnessRegistry] = None,
636
+ ) -> None:
637
+ self.max_steps_default = max_steps_default
638
+ self.mock_mode = mock_mode
639
+ self.backend = backend or (ScriptedAgentBackend() if mock_mode else None)
640
+ self.sub_ask_dispatcher = sub_ask_dispatcher
641
+ self.harness_registry = harness_registry or AgentHarnessRegistry()
642
+
643
+ default_tools: list[AgentToolProtocol] = [
644
+ CalculatorTool(),
645
+ Sha256Tool(),
646
+ RegexSearchTool(),
647
+ FetchBlobTool(),
648
+ SubAskTool(),
649
+ FinishTool(),
650
+ ]
651
+ if tools:
652
+ default_tools.extend(tools)
653
+ self.tools: dict[str, AgentToolProtocol] = {t.name: t for t in default_tools}
654
+
655
+ def is_live_runnable(self) -> bool:
656
+ """Returns True if the executor is capable of servicing live compute requests."""
657
+ if self.mock_mode:
658
+ return True
659
+ if self.backend is not None and getattr(self.backend, 'is_live_runnable', lambda: False)():
660
+ return True
661
+ return False
662
+
663
+ def register_tool(self, tool: AgentToolProtocol) -> None:
664
+ """Register a new tool into the agent harness."""
665
+ self.tools[tool.name] = tool
666
+
667
+ def quote(
668
+ self,
669
+ module_spec: Mapping[str, Any],
670
+ input_spec: Mapping[str, Any],
671
+ limits: ComputeLimits,
672
+ ) -> ComputeQuote:
673
+ max_steps = int(input_spec.get('max_steps', self.max_steps_default))
674
+ fuel_est = max_steps * 500
675
+ return ComputeQuote(
676
+ fuel_estimate=min(fuel_est, limits.max_fuel),
677
+ rate_unit='msat_per_step',
678
+ rate_amount=500,
679
+ engine_id=self.engine_id,
680
+ )
681
+
682
+ def execute(
683
+ self,
684
+ job_payload: Mapping[str, Any],
685
+ fuel_cap: int,
686
+ ) -> ComputeExecutionResult:
687
+ task = job_payload.get('task') or job_payload.get('prompt') or ''
688
+ delegation = job_payload.get('delegation') or {}
689
+ budget_cap_msat = int(delegation.get('budgetCapMsat', 0))
690
+ max_steps = int(job_payload.get('max_steps', self.max_steps_default))
691
+ verif_mode = self.verification_mode(job_payload)
692
+
693
+ # Honesty gate: refuses live execution if unconfigured outside mock_mode
694
+ if not self.is_live_runnable():
695
+ raise ComputeError('not implemented: agent executor is a stub (mock_mode only or configured live backend required)')
696
+
697
+ backend = self.backend or ScriptedAgentBackend()
698
+
699
+ profile_spec: Optional[AgentProfileSpec] = None
700
+ raw_profile = job_payload.get('harness_profile')
701
+ if isinstance(raw_profile, AgentProfileSpec):
702
+ profile_spec = raw_profile
703
+ elif isinstance(raw_profile, Mapping):
704
+ profile_spec = AgentProfileSpec.from_mapping(raw_profile)
705
+ elif job_payload.get('agent_id'):
706
+ profile_spec = self.harness_registry.get(str(job_payload.get('agent_id')))
707
+
708
+ context = AgentExecutionContext(
709
+ task=task,
710
+ delegation=delegation,
711
+ budget_cap_msat=budget_cap_msat,
712
+ budget_spent_msat=0,
713
+ sub_ask_dispatcher=self.sub_ask_dispatcher,
714
+ fuel_cap=fuel_cap,
715
+ fuel_used=0,
716
+ harness_profile=profile_spec,
717
+ domain_context=dict(job_payload.get('domain_context') or {}),
718
+ )
719
+
720
+ trace: list[AgentTraceStep] = []
721
+ tools_called: list[str] = []
722
+ fuel_used = 0
723
+ final_answer = ''
724
+
725
+ for step_num in range(1, max_steps + 1):
726
+ # Step base reasoning fuel consumption
727
+ step_fuel = 500
728
+ if fuel_used + step_fuel > fuel_cap:
729
+ raise ComputeError(f"Agent execution exceeded fuel ceiling ({fuel_used + step_fuel} > {fuel_cap})")
730
+ fuel_used += step_fuel
731
+ context.fuel_used = fuel_used
732
+
733
+ decision = backend.decide_step(task, trace, self.tools, context)
734
+
735
+ action_name = str(decision.action or '').strip()
736
+ action_lower = action_name.lower()
737
+
738
+ if decision.is_finish or action_lower == 'finish':
739
+ if decision.final_answer:
740
+ final_answer = decision.final_answer
741
+ elif isinstance(decision.action_input, Mapping):
742
+ final_answer = str(decision.action_input.get('answer') or decision.action_input.get('final_answer') or 'Agent workflow completed successfully')
743
+ else:
744
+ final_answer = str(decision.action_input or 'Agent workflow completed successfully')
745
+
746
+ trace.append({
747
+ 'step': step_num,
748
+ 'thought': decision.thought,
749
+ 'action': 'finish',
750
+ 'observation': final_answer,
751
+ })
752
+ tools_called.append('finish')
753
+ break
754
+
755
+ # Handle synthetic / legacy decompose_task tool
756
+ if action_lower == 'decompose_task':
757
+ obs = f"Goal identified, tools loaded: {', '.join(sorted(self.tools.keys()))}"
758
+ trace.append({
759
+ 'step': step_num,
760
+ 'thought': decision.thought,
761
+ 'action': 'decompose_task',
762
+ 'observation': obs,
763
+ })
764
+ tools_called.append('decompose_task')
765
+ continue
766
+
767
+ tool = self.tools.get(action_name) or self.tools.get(action_lower)
768
+ if not tool:
769
+ obs = f"Tool '{action_name}' not recognized. Available: {list(self.tools.keys())}"
770
+ tool_fuel = 10
771
+ else:
772
+ act_input = decision.action_input
773
+ if isinstance(act_input, str):
774
+ s = act_input.strip()
775
+ if s.startswith(('{', '[')):
776
+ try:
777
+ act_input = json.loads(s)
778
+ except Exception:
779
+ act_input = {'raw': s}
780
+ else:
781
+ act_input = {'raw': s}
782
+
783
+ try:
784
+ tool_res = tool.execute(act_input, context)
785
+ except Exception as e:
786
+ tool_res = {'error': f"Tool execution failed: {e}", 'fuel_consumed': 25}
787
+
788
+ tool_fuel = tool_res.get('fuel_consumed', 50)
789
+ if 'error' in tool_res and tool_res['error']:
790
+ obs = f"Error: {tool_res['error']}"
791
+ elif 'output' in tool_res:
792
+ out_val = tool_res['output']
793
+ obs = json.dumps(out_val) if isinstance(out_val, (dict, list)) else str(out_val)
794
+ else:
795
+ obs = 'Tool executed successfully'
796
+
797
+ if fuel_used + tool_fuel > fuel_cap:
798
+ raise ComputeError(f"Agent execution exceeded fuel ceiling ({fuel_used + tool_fuel} > {fuel_cap})")
799
+ fuel_used += tool_fuel
800
+ context.fuel_used = fuel_used
801
+
802
+ trace.append({
803
+ 'step': step_num,
804
+ 'thought': decision.thought,
805
+ 'action': action_name,
806
+ 'observation': obs,
807
+ })
808
+ tools_called.append(action_name)
809
+
810
+ steps_taken = len(trace)
811
+ final_summary = {
812
+ 'status': 'completed',
813
+ 'engine': 'fleetmesh-react-v1',
814
+ 'task': task,
815
+ 'steps_taken': steps_taken,
816
+ 'tools_called': tools_called,
817
+ 'sub_asks': context.sub_asks_dispatched,
818
+ 'trace': trace,
819
+ 'final_answer': final_answer or f"Completed execution of goal: '{task[:64]}'",
820
+ }
821
+
822
+ raw_output = json.dumps(final_summary, sort_keys=True).encode('utf-8')
823
+ result_hash = crypto.sha256(raw_output)
824
+
825
+ counters = {
826
+ 'steps': steps_taken,
827
+ 'tools_called': len(tools_called),
828
+ 'sub_asks': len(context.sub_asks_dispatched),
829
+ 'fuel_used': fuel_used,
830
+ }
831
+
832
+ return ComputeExecutionResult(
833
+ result_hash=result_hash,
834
+ raw_output=raw_output,
835
+ fuel_used=fuel_used,
836
+ engine_id=self.engine_id,
837
+ wasi_profile_digest=self.wasi_profile_digest,
838
+ counters=counters,
839
+ verification_mode=verif_mode,
840
+ )