Skip to content

Commit e31f698

Browse files
committed
Release v1.8.1 — ToolClad, CommunicationPolicyGate, agent lifecycle
New features: - ToolCladClient: list/validate/test/schema/execute/reload tool manifests - CommunicationPolicyGate: list/add/remove rules, evaluate communication - Agent lifecycle: delete_agent, re_execute_agent - ORGA-adaptive: get_tool_profiles, get_loop_diagnostics - 6 new Pydantic models for ToolClad and communication types
1 parent ba89766 commit e31f698

7 files changed

Lines changed: 316 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.8.1] - 2026-03-23
9+
10+
### Added
11+
12+
#### ToolClad Manifest Management
13+
- **ToolCladClient** — Full client for ToolClad manifest operations via `client.toolclad`
14+
- `list_tools()` — List all discovered `.clad.toml` manifests
15+
- `validate_manifest()` — Validate a manifest file
16+
- `test_tool()` — Dry-run a tool (no execution)
17+
- `get_schema()` — Get MCP-compatible JSON schema for a tool
18+
- `execute_tool()` — Execute a tool with validated arguments, returns evidence envelope
19+
- `get_tool_info()` — Get detailed tool manifest information
20+
- `reload_tools()` — Trigger hot-reload of tool manifests
21+
- **Pydantic models**: `ToolManifestInfo`, `ToolValidationResult`, `ToolTestResult`, `ToolExecutionResult`
22+
23+
#### Inter-Agent Communication Policy Gate
24+
- `list_communication_rules()` — List all communication policy rules
25+
- `add_communication_rule()` — Add a policy rule (allow/deny between agents)
26+
- `remove_communication_rule()` — Remove a rule by ID
27+
- `evaluate_communication()` — Evaluate whether a communication is allowed
28+
- **Pydantic models**: `CommunicationRule`, `CommunicationEvaluation`
29+
30+
#### Agent Lifecycle
31+
- `delete_agent()` — Delete an agent and its metadata
32+
- `re_execute_agent()` — Re-execute an agent with optional new input (resets ORGA loop)
33+
34+
#### ORGA-Adaptive Features
35+
- `reasoning.get_tool_profiles()` — Tool execution timing statistics and success rates
36+
- `reasoning.get_loop_diagnostics()` — Stuck-loop detection, iteration history, adaptive parameters
37+
38+
### Changed
39+
- Version alignment with Symbiont runtime v1.8.1
40+
41+
---
42+
843
## [1.6.1] - 2026-02-28
944

1045
### Added

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def read_requirements():
1515

1616
setup(
1717
name='symbiont-sdk',
18-
version='1.6.1',
18+
version='1.8.1',
1919
author='Jascha Wanger / ThirdKey.ai',
2020
author_email='oss@symbiont.dev',
2121
description='Python SDK for Symbiont platform with Tool Review and Runtime APIs',

symbiont/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
AgentState,
3535
AgentStatusResponse,
3636
AnalysisResults,
37+
# Communication Policy Models
38+
CommunicationEvaluation,
39+
CommunicationRule,
3740
ContextQuery,
3841
ContextResponse,
3942
# Agent DSL Models
@@ -82,8 +85,13 @@
8285
SystemMetrics,
8386
# Tool Review Models
8487
Tool,
88+
# ToolClad Models
89+
ToolExecutionResult,
90+
ToolManifestInfo,
8591
ToolProvider,
8692
ToolSchema,
93+
ToolTestResult,
94+
ToolValidationResult,
8795
VaultAuthMethod,
8896
VaultConfig,
8997
VectorMetadata,
@@ -127,6 +135,7 @@
127135
Usage,
128136
)
129137
from .reasoning_client import ReasoningClient
138+
from .toolclad import ToolCladClient
130139
from .schedules import (
131140
CreateScheduleRequest,
132141
CreateScheduleResponse,
@@ -157,7 +166,7 @@
157166
# Load environment variables from .env file
158167
load_dotenv()
159168

160-
__version__ = "1.6.1"
169+
__version__ = "1.8.1"
161170

162171
__all__ = [
163172
# Client
@@ -225,6 +234,13 @@
225234
'MetricsClient', 'MetricsCollector', 'MetricsSnapshot',
226235
'FileMetricsExporter', 'CompositeExporter',
227236

237+
# ToolClad
238+
'ToolCladClient',
239+
'ToolManifestInfo', 'ToolValidationResult', 'ToolTestResult', 'ToolExecutionResult',
240+
241+
# Communication Policy
242+
'CommunicationRule', 'CommunicationEvaluation',
243+
228244
# Reasoning Loop
229245
'ReasoningClient',
230246
'Usage', 'ToolDefinition', 'ToolCallRequest',

symbiont/client.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def __init__(self,
155155
self._agentpin: Optional[Any] = None
156156
self._metrics_client: Optional[Any] = None
157157
self._reasoning: Optional[Any] = None
158+
self._toolclad: Optional[Any] = None
158159

159160
@property
160161
def schedules(self) -> ScheduleClient:
@@ -194,6 +195,14 @@ def metrics_client(self):
194195
self._metrics_client = MetricsClient(self)
195196
return self._metrics_client
196197

198+
@property
199+
def toolclad(self):
200+
"""Lazy-loaded ToolClad manifest management client."""
201+
if self._toolclad is None:
202+
from .toolclad import ToolCladClient
203+
self._toolclad = ToolCladClient(self)
204+
return self._toolclad
205+
197206
def _request(self, method: str, endpoint: str, **kwargs):
198207
"""Make an HTTP request to the API.
199208
@@ -1437,3 +1446,93 @@ def disable_http_endpoint(self, endpoint_id: str) -> Dict[str, Any]:
14371446
"""
14381447
response = self._request("POST", f"endpoints/{endpoint_id}/disable")
14391448
return response.json()
1449+
1450+
# =============================================================================
1451+
# Inter-Agent Communication Policy Methods
1452+
# =============================================================================
1453+
1454+
def list_communication_rules(self) -> List[Dict]:
1455+
"""List all communication policy rules.
1456+
1457+
Returns:
1458+
List[Dict]: List of communication policy rules
1459+
"""
1460+
response = self._request("GET", "api/v1/communication/rules")
1461+
return response.json()
1462+
1463+
def add_communication_rule(self, rule: Dict) -> Dict:
1464+
"""Add a communication policy rule.
1465+
1466+
Args:
1467+
rule: Dict with keys: from_agent, to_agent, action (allow/deny),
1468+
effect, reason, priority, max_depth
1469+
1470+
Returns:
1471+
Dict: Created rule confirmation
1472+
"""
1473+
response = self._request("POST", "api/v1/communication/rules", json=rule)
1474+
return response.json()
1475+
1476+
def remove_communication_rule(self, rule_id: str) -> Dict:
1477+
"""Remove a communication policy rule by ID.
1478+
1479+
Args:
1480+
rule_id: The rule identifier
1481+
1482+
Returns:
1483+
Dict: Removal confirmation
1484+
"""
1485+
response = self._request("DELETE", f"api/v1/communication/rules/{rule_id}")
1486+
return response.json()
1487+
1488+
def evaluate_communication(self, sender: str, recipient: str, action: str) -> Dict:
1489+
"""Evaluate whether a communication is allowed by policy.
1490+
1491+
Args:
1492+
sender: Sending agent identifier
1493+
recipient: Receiving agent identifier
1494+
action: The action to evaluate
1495+
1496+
Returns:
1497+
Dict with 'allowed' (bool), 'rule' (matching rule), 'reason'.
1498+
"""
1499+
response = self._request("POST", "api/v1/communication/evaluate", json={
1500+
"sender": sender,
1501+
"recipient": recipient,
1502+
"action": action,
1503+
})
1504+
return response.json()
1505+
1506+
# =============================================================================
1507+
# Agent Lifecycle Methods
1508+
# =============================================================================
1509+
1510+
def delete_agent(self, agent_id: str) -> Dict:
1511+
"""Delete an agent and its metadata.
1512+
1513+
Args:
1514+
agent_id: The agent identifier
1515+
1516+
Returns:
1517+
Dict: Deletion confirmation
1518+
"""
1519+
response = self._request("DELETE", f"api/v1/agents/{agent_id}")
1520+
return response.json()
1521+
1522+
def re_execute_agent(self, agent_id: str, input_data: Any = None) -> Dict:
1523+
"""Re-execute an agent with optional new input.
1524+
1525+
Resets the agent's ORGA loop state and starts a new execution.
1526+
1527+
Args:
1528+
agent_id: The agent identifier
1529+
input_data: Optional new input data for the agent
1530+
1531+
Returns:
1532+
Dict: Re-execution result
1533+
"""
1534+
payload = {}
1535+
if input_data is not None:
1536+
payload["input"] = input_data
1537+
response = self._request("POST", f"api/v1/agents/{agent_id}/re-execute", json=payload)
1538+
return response.json()

symbiont/models.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,3 +1133,71 @@ class MetricsSnapshot(BaseModel):
11331133
load_balancer: Optional[LoadBalancerMetricsSnapshot] = Field(None, description="Load balancer metrics")
11341134
system: Optional[SystemResourceMetricsSnapshot] = Field(None, description="System resource metrics")
11351135

1136+
1137+
# =============================================================================
1138+
# ToolClad Models
1139+
# =============================================================================
1140+
1141+
class ToolManifestInfo(BaseModel):
1142+
"""Summary of a ToolClad manifest."""
1143+
name: str
1144+
version: str
1145+
description: str
1146+
risk_tier: str
1147+
human_approval: bool = False
1148+
arg_count: int
1149+
backend: str # "shell", "http", "mcp", "session", "browser"
1150+
source_path: str
1151+
1152+
1153+
class ToolValidationResult(BaseModel):
1154+
"""Result of manifest validation."""
1155+
valid: bool
1156+
errors: List[str] = []
1157+
warnings: List[str] = []
1158+
1159+
1160+
class ToolTestResult(BaseModel):
1161+
"""Result of a tool dry-run."""
1162+
command: str
1163+
validations: List[str]
1164+
cedar: Optional[str] = None
1165+
timeout: int
1166+
1167+
1168+
class ToolExecutionResult(BaseModel):
1169+
"""Evidence envelope from tool execution."""
1170+
status: str
1171+
scan_id: str
1172+
tool: str
1173+
command: str
1174+
duration_ms: int
1175+
timestamp: str
1176+
output_hash: Optional[str] = None
1177+
exit_code: int = 0
1178+
stderr: str = ""
1179+
results: Dict[str, Any] = {}
1180+
1181+
1182+
# =============================================================================
1183+
# Communication Policy Models
1184+
# =============================================================================
1185+
1186+
class CommunicationRule(BaseModel):
1187+
"""Inter-agent communication policy rule."""
1188+
id: Optional[str] = None
1189+
from_agent: str
1190+
to_agent: str
1191+
action: str # "allow" or "deny"
1192+
effect: str = "allow"
1193+
reason: str = ""
1194+
priority: int = 0
1195+
max_depth: Optional[int] = None
1196+
1197+
1198+
class CommunicationEvaluation(BaseModel):
1199+
"""Result of a communication policy evaluation."""
1200+
allowed: bool
1201+
rule: Optional[CommunicationRule] = None
1202+
reason: str = ""
1203+

symbiont/reasoning_client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,31 @@ def store_knowledge(
184184
"confidence": confidence,
185185
},
186186
)
187+
188+
# -------------------------------------------------------------------------
189+
# ORGA-Adaptive Features
190+
# -------------------------------------------------------------------------
191+
192+
def get_tool_profiles(self, agent_id: str) -> List[Dict]:
193+
"""Get tool execution profiles for an agent.
194+
195+
Returns timing statistics and success rates per tool.
196+
197+
``GET /api/v1/agents/{id}/reasoning/tool-profiles``
198+
"""
199+
return self._request(
200+
"GET", f"/api/v1/agents/{agent_id}/reasoning/tool-profiles"
201+
)
202+
203+
def get_loop_diagnostics(self, agent_id: str, loop_id: str) -> Dict:
204+
"""Get diagnostics for a reasoning loop.
205+
206+
Includes stuck-loop detection status, iteration history,
207+
and adaptive parameters.
208+
209+
``GET /api/v1/agents/{id}/reasoning/{loop_id}/diagnostics``
210+
"""
211+
return self._request(
212+
"GET",
213+
f"/api/v1/agents/{agent_id}/reasoning/{loop_id}/diagnostics",
214+
)

symbiont/toolclad.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""ToolClad manifest management client for Symbiont SDK."""
2+
3+
from typing import Any, Dict, List
4+
5+
6+
class ToolCladClient:
7+
"""Client for ToolClad manifest operations.
8+
9+
This class is typically accessed through the main ``Client`` instance::
10+
11+
from symbiont import Client
12+
client = Client()
13+
tools = client.toolclad.list_tools()
14+
"""
15+
16+
def __init__(self, client):
17+
self._client = client
18+
19+
def list_tools(self) -> List[Dict[str, Any]]:
20+
"""List all discovered ToolClad manifests."""
21+
response = self._client._request("GET", "api/v1/tools")
22+
return response.json()
23+
24+
def validate_manifest(self, path: str) -> Dict[str, Any]:
25+
"""Validate a .clad.toml manifest file.
26+
27+
Args:
28+
path: Path to the manifest file (relative to tools_dir)
29+
"""
30+
response = self._client._request(
31+
"POST", "api/v1/tools/validate", json={"path": path}
32+
)
33+
return response.json()
34+
35+
def test_tool(self, tool_name: str, args: Dict[str, str]) -> Dict[str, Any]:
36+
"""Dry-run a tool with given arguments (no execution).
37+
38+
Returns the command that would be executed and validation results.
39+
"""
40+
response = self._client._request(
41+
"POST", f"api/v1/tools/{tool_name}/test", json={"args": args}
42+
)
43+
return response.json()
44+
45+
def get_schema(self, tool_name: str) -> Dict[str, Any]:
46+
"""Get the MCP-compatible JSON schema for a tool."""
47+
response = self._client._request("GET", f"api/v1/tools/{tool_name}/schema")
48+
return response.json()
49+
50+
def execute_tool(self, tool_name: str, args: Dict[str, str]) -> Dict[str, Any]:
51+
"""Execute a tool with validated arguments.
52+
53+
Returns an evidence envelope with results.
54+
"""
55+
response = self._client._request(
56+
"POST", f"api/v1/tools/{tool_name}/execute", json={"args": args}
57+
)
58+
return response.json()
59+
60+
def get_tool_info(self, tool_name: str) -> Dict[str, Any]:
61+
"""Get detailed information about a tool manifest."""
62+
response = self._client._request("GET", f"api/v1/tools/{tool_name}")
63+
return response.json()
64+
65+
def reload_tools(self) -> Dict[str, Any]:
66+
"""Trigger a hot-reload of tool manifests from the tools directory."""
67+
response = self._client._request("POST", "api/v1/tools/reload")
68+
return response.json()

0 commit comments

Comments
 (0)