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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- 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
|
+
)
|