Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 177 additions & 100 deletions sdk/python/agentfield/agent.py

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions sdk/python/agentfield/agent_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import datetime, timezone
from agentfield.logger import log_debug


class _AgentDiscoveryMixin:
def _handle_discovery(self) -> dict:
"""
Expand Down Expand Up @@ -48,11 +49,14 @@ def _build_callback_discovery_payload(self) -> Optional[Dict[str, Any]]:
"preferred": self.base_url,
"callback_candidates": self.callback_candidates,
"container": self._is_running_in_container(),
"submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
"submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[
:-3
]
+ "Z",
}

return payload

def _apply_discovery_response(self, payload: Optional[Dict[str, Any]]) -> None:
"""Update agent networking state from AgentField discovery response."""

Expand Down Expand Up @@ -92,4 +96,4 @@ def _apply_discovery_response(self, payload: Optional[Dict[str, Any]]) -> None:
normalized.insert(0, resolved)

if normalized:
self.callback_candidates = normalized
self.callback_candidates = normalized
16 changes: 5 additions & 11 deletions sdk/python/agentfield/agent_field_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ async def register_with_agentfield_server(self, port: int):
f"(pending tags: {pending_tags})"
)
await self._wait_for_approval()
log_success(
f"Node '{self.agent.node_id}' tag approval granted"
)
log_success(f"Node '{self.agent.node_id}' tag approval granted")
else:
log_success(
f"Registered node '{self.agent.node_id}' with AgentField server"
Expand Down Expand Up @@ -210,15 +208,11 @@ async def _wait_for_approval(self, timeout: int = 300):
status = data.get("lifecycle_status", "")
if status and status != "pending_approval":
return
log_debug(
f"Node '{self.agent.node_id}' still pending approval..."
)
log_debug(f"Node '{self.agent.node_id}' still pending approval...")
except Exception as e:
log_debug(f"Polling for approval status failed: {e}")

log_error(
f"Node '{self.agent.node_id}' approval timed out after {timeout}s"
)
log_error(f"Node '{self.agent.node_id}' approval timed out after {timeout}s")
raise TimeoutError(
f"Agent '{self.agent.node_id}' tag approval timed out after {timeout} seconds. "
"Please approve the agent's tags in the control plane admin UI."
Expand Down Expand Up @@ -299,8 +293,8 @@ async def send_enhanced_heartbeat(self) -> bool:
heartbeat_data = HeartbeatData(
status=self.agent._current_status,
timestamp=datetime.now().isoformat(),
version=getattr(self.agent, 'version', '') or '',
instance_id=getattr(self.agent, 'agent_instance_id', '') or '',
version=getattr(self.agent, "version", "") or "",
instance_id=getattr(self.agent, "agent_instance_id", "") or "",
)

# Send enhanced heartbeat
Expand Down
5 changes: 3 additions & 2 deletions sdk/python/agentfield/agent_schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any, Dict, Union


class _AgentSchemaMixin:
def _types_to_json_schema(self, input_types: Dict[str, tuple]) -> Dict:
"""Convert Python types dict to JSON schema (on-demand generation)."""
Expand All @@ -18,7 +19,7 @@ def _types_to_json_schema(self, input_types: Dict[str, tuple]) -> Dict:
if required:
schema["required"] = required
return schema

def _type_to_json_schema(self, typ: type) -> Dict:
"""Convert a Python type to JSON schema."""
# Handle None/NoneType
Expand Down Expand Up @@ -158,4 +159,4 @@ def _validate_handler_input(
except (ValueError, TypeError) as e:
raise ValueError(f"Invalid value for field '{name}': {e}")

return result
return result
3 changes: 2 additions & 1 deletion sdk/python/agentfield/agent_serverless.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from agentfield.execution_context import ExecutionContext
from agentfield.logger import log_warn


class _AgentServerlessMixin:
def handle_serverless(
self, event: dict, adapter: Optional[Callable] = None
Expand Down Expand Up @@ -163,4 +164,4 @@ def lambda_handler(event, context):
return {"statusCode": 500, "body": {"error": str(e)}}
finally:
# Clean up execution context
self._current_execution_context = None
self._current_execution_context = None
15 changes: 7 additions & 8 deletions sdk/python/agentfield/agent_vc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from typing import Any, Dict, Optional

from agentfield.logger import log_debug, log_info, log_warn, log_error
Expand Down Expand Up @@ -26,7 +25,7 @@ def _initialize_did_system(self):
log_error(f"Failed to initialize DID system: {e}")
self.did_manager = None
self.vc_generator = None

def _populate_execution_context_with_did(
self, execution_context, did_execution_context
):
Expand All @@ -46,7 +45,7 @@ def _populate_execution_context_with_did(
def _agent_vc_default(self) -> bool:
"""Resolve the agent-level VC default, falling back to enabled."""
return True if self._agent_vc_enabled is None else self._agent_vc_enabled

def _set_reasoner_vc_override(
self, reasoner_id: str, value: Optional[bool]
) -> None:
Expand All @@ -67,7 +66,7 @@ def _effective_component_vc_setting(
if component_id in overrides:
return overrides[component_id]
return self._agent_vc_default()

def _should_generate_vc(
self, component_id: str, overrides: Dict[str, bool]
) -> bool:
Expand All @@ -78,7 +77,7 @@ def _should_generate_vc(
):
return False
return self._effective_component_vc_setting(component_id, overrides)

def _build_agent_metadata(self) -> Optional[Dict[str, Any]]:
"""Build agent metadata (description, tags, author) for registration payload."""
metadata: Dict[str, Any] = {}
Expand All @@ -89,7 +88,7 @@ def _build_agent_metadata(self) -> Optional[Dict[str, Any]]:
if self.author:
metadata["author"] = self.author
return metadata if metadata else None

def _build_vc_metadata(self) -> Dict[str, Any]:
"""Produce a serializable VC policy snapshot for control-plane visibility."""
effective_reasoners = {
Expand All @@ -114,7 +113,7 @@ def _build_vc_metadata(self) -> Dict[str, Any]:
"effective_reasoners": effective_reasoners,
"effective_skills": effective_skills,
}

async def _generate_vc_async(
self,
vc_generator,
Expand Down Expand Up @@ -152,4 +151,4 @@ async def _generate_vc_async(
if vc:
log_info(f"Generated VC {vc.vc_id} for {function_name}")
except Exception as e:
log_warn(f"Failed to generate VC for {function_name}: {e}")
log_warn(f"Failed to generate VC for {function_name}: {e}")
14 changes: 8 additions & 6 deletions sdk/python/agentfield/agent_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async def notify_call_start(
*,
parent_execution_id: Optional[str] = None,
) -> None:
await self._emit_execution_transition_log(
self._emit_execution_transition_log(
context,
reasoner_name,
event_type="reasoner.started",
Expand All @@ -131,6 +131,7 @@ async def notify_call_start(
input_data=input_data,
parent_execution_id=parent_execution_id,
)

payload = self._build_event_payload(
context,
reasoner_name,
Expand All @@ -151,7 +152,7 @@ async def notify_call_complete(
input_data: Optional[Dict[str, Any]] = None,
parent_execution_id: Optional[str] = None,
) -> None:
await self._emit_execution_transition_log(
self._emit_execution_transition_log(
context,
context.reasoner_name,
event_type="reasoner.completed",
Expand All @@ -163,6 +164,7 @@ async def notify_call_complete(
input_data=input_data,
parent_execution_id=parent_execution_id,
)

payload = self._build_event_payload(
context,
context.reasoner_name,
Expand All @@ -185,7 +187,7 @@ async def notify_call_error(
input_data: Optional[Dict[str, Any]] = None,
parent_execution_id: Optional[str] = None,
) -> None:
await self._emit_execution_transition_log(
self._emit_execution_transition_log(
context,
context.reasoner_name,
event_type="reasoner.failed",
Expand Down Expand Up @@ -291,7 +293,7 @@ async def _ensure_execution_registered(
context.execution_id = body.get("execution_id", context.execution_id)
context.workflow_id = body.get("workflow_id", context.workflow_id)
context.run_id = body.get("run_id", context.run_id)
await self._emit_execution_transition_log(
self._emit_execution_transition_log(
context,
reasoner_name,
event_type="execution.registered",
Expand All @@ -303,7 +305,7 @@ async def _ensure_execution_registered(
except Exception as exc: # pragma: no cover - network failure path
if getattr(self.agent, "dev_mode", False):
log_warn(f"Workflow registration failed: {exc}")
await self._emit_execution_transition_log(
self._emit_execution_transition_log(
context,
reasoner_name,
event_type="execution.registration.failed",
Expand Down Expand Up @@ -348,7 +350,7 @@ def _build_event_payload(
payload["input_data"] = input_data
return payload

async def _emit_execution_transition_log(
def _emit_execution_transition_log(
self,
context: ExecutionContext,
reasoner_name: str,
Expand Down
46 changes: 28 additions & 18 deletions sdk/python/agentfield/async_execution_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
import aiohttp

from .async_config import AsyncConfig
from .execution_state import ExecuteError, ExecutionPriority, ExecutionState, ExecutionStatus
from .execution_state import (
ExecuteError,
ExecutionPriority,
ExecutionState,
ExecutionStatus,
)
from .exceptions import (
AgentFieldClientError,
ExecutionCancelledError,
Expand Down Expand Up @@ -241,9 +246,7 @@ def __init__(

# Polling coordination
self._polling_task: Optional[asyncio.Task] = None
self._polling_semaphore = LazySemaphore(
lambda: self.config.max_active_polls
)
self._polling_semaphore = LazySemaphore(lambda: self.config.max_active_polls)
self._shutdown_event: Optional[asyncio.Event] = None

# Metrics and monitoring
Expand Down Expand Up @@ -389,7 +392,9 @@ async def submit_execution(

# Check circuit breaker
if self._is_circuit_breaker_open():
raise AgentFieldClientError("Circuit breaker is open - too many recent failures")
raise AgentFieldClientError(
"Circuit breaker is open - too many recent failures"
)

# Reserve capacity slot; released once terminal
await self._capacity_semaphore.acquire()
Expand All @@ -413,7 +418,10 @@ async def submit_execution(
body_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")

# Add DID authentication headers if configured
if self._did_authenticator is not None and self._did_authenticator.is_configured:
if (
self._did_authenticator is not None
and self._did_authenticator.is_configured
):
did_headers = self._did_authenticator.sign_headers(body_bytes)
request_headers.update(did_headers)

Expand All @@ -437,8 +445,14 @@ async def submit_execution(
error_body = None
body_msg = ""
if isinstance(error_body, dict):
body_msg = error_body.get("message") or error_body.get("error") or ""
msg = f"{response.status}, {body_msg}" if body_msg else str(response.status)
body_msg = (
error_body.get("message") or error_body.get("error") or ""
)
msg = (
f"{response.status}, {body_msg}"
if body_msg
else str(response.status)
)
raise ExecuteError(response.status, msg, error_body)
result = await response.json()

Expand Down Expand Up @@ -616,9 +630,7 @@ def _active_elapsed() -> float:
async with self._execution_lock:
execution = self._executions.get(execution_id)
if execution is not None:
previous_pause_clock = getattr(
execution, "_pause_clock", None
)
previous_pause_clock = getattr(execution, "_pause_clock", None)
execution._pause_clock = pause_clock
attached_pause_clock = True
try:
Expand Down Expand Up @@ -1216,9 +1228,7 @@ async def _poll_single_execution(self, execution: ExecutionState) -> None:
kwargs: Dict[str, Any] = {"timeout": self.config.polling_timeout}
if self._auth_headers:
kwargs["headers"] = dict(self._auth_headers)
response = await self.connection_manager.request(
"GET", url, **kwargs
)
response = await self.connection_manager.request("GET", url, **kwargs)
duration = time.time() - start_time

await self._process_poll_response(execution, response, duration)
Expand Down Expand Up @@ -1302,10 +1312,10 @@ async def _update_execution_from_status(

old_status = execution.status

should_apply_terminal_payload = new_status != old_status or (
new_status == ExecutionStatus.SUCCEEDED and execution.result is None
) or (
new_status == ExecutionStatus.FAILED and not execution.error_message
should_apply_terminal_payload = (
new_status != old_status
or (new_status == ExecutionStatus.SUCCEEDED and execution.result is None)
or (new_status == ExecutionStatus.FAILED and not execution.error_message)
)

# Update status and terminal payloads. Replay-hit executions can be
Expand Down
Loading
Loading