agent-runtime-kit 0.1.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.
@@ -0,0 +1,72 @@
1
+ """Public API for agent-runtime-kit."""
2
+
3
+ from agent_runtime_kit._errors import (
4
+ AgentRuntimeError,
5
+ AgentRuntimeUnavailableError,
6
+ RuntimeNotRegisteredError,
7
+ UnsupportedTaskInputError,
8
+ )
9
+ from agent_runtime_kit._runtime import FakeAgentRuntime
10
+ from agent_runtime_kit._types import (
11
+ AgentCapabilities,
12
+ AgentResult,
13
+ AgentRuntime,
14
+ AgentRuntimeKind,
15
+ AgentTask,
16
+ ArtifactRef,
17
+ AvailabilityReason,
18
+ EventSink,
19
+ FilesystemAccess,
20
+ McpServerConfig,
21
+ PermissionMode,
22
+ PermissionProfile,
23
+ RuntimeAvailability,
24
+ SessionResumeState,
25
+ ToolCallAudit,
26
+ Usage,
27
+ )
28
+ from agent_runtime_kit.events import (
29
+ output_delta_event,
30
+ safe_emit,
31
+ task_completed_event,
32
+ task_failed_event,
33
+ task_started_event,
34
+ tool_completed_event,
35
+ tool_requested_event,
36
+ vendor_turn_event,
37
+ )
38
+ from agent_runtime_kit.registry import RuntimeRegistry, create_default_registry
39
+
40
+ __all__ = [
41
+ "AgentCapabilities",
42
+ "AgentResult",
43
+ "AgentRuntime",
44
+ "AgentRuntimeError",
45
+ "AgentRuntimeKind",
46
+ "AgentRuntimeUnavailableError",
47
+ "AgentTask",
48
+ "ArtifactRef",
49
+ "AvailabilityReason",
50
+ "EventSink",
51
+ "FakeAgentRuntime",
52
+ "FilesystemAccess",
53
+ "McpServerConfig",
54
+ "PermissionMode",
55
+ "PermissionProfile",
56
+ "RuntimeAvailability",
57
+ "RuntimeNotRegisteredError",
58
+ "RuntimeRegistry",
59
+ "SessionResumeState",
60
+ "ToolCallAudit",
61
+ "UnsupportedTaskInputError",
62
+ "Usage",
63
+ "create_default_registry",
64
+ "output_delta_event",
65
+ "safe_emit",
66
+ "task_completed_event",
67
+ "task_failed_event",
68
+ "task_started_event",
69
+ "tool_completed_event",
70
+ "tool_requested_event",
71
+ "vendor_turn_event",
72
+ ]
@@ -0,0 +1,34 @@
1
+ """Typed errors raised by agent runtimes and registries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agent_runtime_kit._types import AgentRuntimeKind
6
+
7
+
8
+ class AgentRuntimeError(RuntimeError):
9
+ """Base class for runtime-layer failures."""
10
+
11
+
12
+ class AgentRuntimeUnavailableError(AgentRuntimeError):
13
+ """A runtime cannot be constructed or used in the current environment."""
14
+
15
+ def __init__(self, kind: AgentRuntimeKind | str, message: str) -> None:
16
+ self.kind = AgentRuntimeKind.coerce(kind)
17
+ super().__init__(message)
18
+
19
+
20
+ class UnsupportedTaskInputError(AgentRuntimeError, ValueError):
21
+ """A runtime was asked to honor an input it does not support."""
22
+
23
+ def __init__(self, kind: AgentRuntimeKind | str, field: str, message: str) -> None:
24
+ self.kind = AgentRuntimeKind.coerce(kind)
25
+ self.field = field
26
+ super().__init__(f"{self.kind.value} cannot honor {field}: {message}")
27
+
28
+
29
+ class RuntimeNotRegisteredError(AgentRuntimeError, LookupError):
30
+ """No runtime factory is registered for the requested runtime kind."""
31
+
32
+ def __init__(self, kind: AgentRuntimeKind | str) -> None:
33
+ self.kind = AgentRuntimeKind.coerce(kind)
34
+ super().__init__(f"No runtime registered for {self.kind.value!r}")
@@ -0,0 +1,139 @@
1
+ """Dependency-free runtime implementations used by tests and examples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from agent_runtime_kit._errors import UnsupportedTaskInputError
9
+ from agent_runtime_kit._types import (
10
+ AgentCapabilities,
11
+ AgentResult,
12
+ AgentRuntimeKind,
13
+ AgentTask,
14
+ RuntimeAvailability,
15
+ ToolCallAudit,
16
+ )
17
+ from agent_runtime_kit.events import (
18
+ output_delta_event,
19
+ safe_emit,
20
+ task_completed_event,
21
+ task_failed_event,
22
+ task_started_event,
23
+ tool_completed_event,
24
+ tool_requested_event,
25
+ vendor_turn_event,
26
+ )
27
+
28
+
29
+ class FakeAgentRuntime:
30
+ """Small deterministic runtime for tests and local examples."""
31
+
32
+ kind = AgentRuntimeKind.FAKE
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ output: str | None = None,
38
+ capabilities: AgentCapabilities | None = None,
39
+ metadata: Mapping[str, Any] | None = None,
40
+ ) -> None:
41
+ self.capabilities = capabilities or AgentCapabilities(
42
+ mcp_support=True,
43
+ working_directory=True,
44
+ session_resume=True,
45
+ structured_output=True,
46
+ streaming=False,
47
+ tool_audit=True,
48
+ cancellation=True,
49
+ )
50
+ self._output = output
51
+ self._metadata = dict(metadata or {})
52
+ self.cancelled_task_ids: set[str] = set()
53
+
54
+ def availability(self) -> RuntimeAvailability:
55
+ """Fake runtime is always available."""
56
+
57
+ return RuntimeAvailability.ok(self.kind, package="agent-runtime-kit")
58
+
59
+ async def run(self, task: AgentTask) -> AgentResult:
60
+ """Return a deterministic result after validating capabilities."""
61
+
62
+ await safe_emit(task, task_started_event(task, self.kind))
63
+ try:
64
+ _ensure_supported(self.kind, self.capabilities, task)
65
+ output = self._output if self._output is not None else f"Fake result for: {task.goal}"
66
+ parsed = {"output": output} if task.output_schema is not None else None
67
+ tool_call = ToolCallAudit(
68
+ tool_name="fake",
69
+ arguments={"goal": task.goal},
70
+ result_preview=output,
71
+ )
72
+ await safe_emit(
73
+ task,
74
+ output_delta_event(task, self.kind, text=output),
75
+ )
76
+ await safe_emit(
77
+ task,
78
+ tool_requested_event(
79
+ task,
80
+ self.kind,
81
+ tool_name="fake",
82
+ arguments=tool_call.arguments,
83
+ ),
84
+ )
85
+ await safe_emit(task, tool_completed_event(task, self.kind, tool_call))
86
+ await safe_emit(
87
+ task,
88
+ vendor_turn_event(
89
+ task,
90
+ self.kind,
91
+ payload={"runtime": "fake", "round": 1},
92
+ summary="fake runtime completed one turn",
93
+ ),
94
+ )
95
+ result = AgentResult(
96
+ output=output,
97
+ parsed_output=parsed,
98
+ tool_calls=(tool_call,),
99
+ session_id=task.session_id or task.task_id,
100
+ rounds=1,
101
+ metadata={"task_id": task.task_id, **self._metadata},
102
+ )
103
+ except Exception as exc:
104
+ await safe_emit(task, task_failed_event(task, self.kind, error=str(exc)))
105
+ raise
106
+ await safe_emit(task, task_completed_event(task, self.kind, result))
107
+ return result
108
+
109
+ async def cancel(self, task_id: str) -> None:
110
+ """Record cancellation requests for assertions."""
111
+
112
+ self.cancelled_task_ids.add(task_id)
113
+
114
+
115
+ def _ensure_supported(
116
+ kind: AgentRuntimeKind,
117
+ capabilities: AgentCapabilities,
118
+ task: AgentTask,
119
+ ) -> None:
120
+ if task.mcp_servers and not capabilities.mcp_support:
121
+ raise UnsupportedTaskInputError(kind, "mcp_servers", "runtime does not support MCP")
122
+ if task.working_directory is not None and not capabilities.working_directory:
123
+ raise UnsupportedTaskInputError(
124
+ kind,
125
+ "working_directory",
126
+ "runtime does not support per-task working directories",
127
+ )
128
+ if (task.session_id or task.resume_from) and not capabilities.session_resume:
129
+ raise UnsupportedTaskInputError(
130
+ kind,
131
+ "session_id",
132
+ "runtime does not support session resume",
133
+ )
134
+ if task.output_schema is not None and not capabilities.structured_output:
135
+ raise UnsupportedTaskInputError(
136
+ kind,
137
+ "output_schema",
138
+ "runtime does not support structured output",
139
+ )
@@ -0,0 +1,251 @@
1
+ """Core public models and protocols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any, Protocol, runtime_checkable
10
+ from uuid import uuid4
11
+
12
+
13
+ class AgentRuntimeKind(str, Enum):
14
+ """Supported runtime families."""
15
+
16
+ FAKE = "fake"
17
+ CLAUDE_AGENT_SDK = "claude-agent-sdk"
18
+ CODEX_AGENT_SDK = "codex-agent-sdk"
19
+ ANTIGRAVITY_AGENT_SDK = "antigravity-agent-sdk"
20
+
21
+ @classmethod
22
+ def coerce(cls, value: AgentRuntimeKind | str) -> AgentRuntimeKind:
23
+ """Normalize a string or enum value into an ``AgentRuntimeKind``."""
24
+
25
+ if isinstance(value, cls):
26
+ return value
27
+ return cls(value)
28
+
29
+
30
+ class AvailabilityReason(str, Enum):
31
+ """Why a runtime is, or is not, available."""
32
+
33
+ AVAILABLE = "available"
34
+ MISSING_PACKAGE = "missing-package"
35
+ MISSING_CREDENTIALS = "missing-credentials"
36
+ UNSUPPORTED_MODEL = "unsupported-model"
37
+ SETUP_FAILED = "setup-failed"
38
+ UNKNOWN = "unknown"
39
+
40
+
41
+ class PermissionMode(str, Enum):
42
+ """High-level permission intent for vendor runtimes."""
43
+
44
+ DEFAULT = "default"
45
+ STRICT = "strict"
46
+ CAUTIOUS = "cautious"
47
+ PERMISSIVE = "permissive"
48
+
49
+
50
+ class FilesystemAccess(str, Enum):
51
+ """Filesystem mutation level requested by a task."""
52
+
53
+ READ_ONLY = "read-only"
54
+ WORKSPACE_WRITE = "workspace-write"
55
+ FULL_ACCESS = "full-access"
56
+
57
+
58
+ @runtime_checkable
59
+ class EventSink(Protocol):
60
+ """Async destination for normalized runtime events."""
61
+
62
+ async def emit(self, event: Mapping[str, Any]) -> None:
63
+ """Receive one normalized event."""
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class AgentCapabilities:
68
+ """Runtime capability advertisement."""
69
+
70
+ mcp_support: bool = False
71
+ working_directory: bool = False
72
+ session_resume: bool = False
73
+ structured_output: bool = False
74
+ streaming: bool = False
75
+ tool_audit: bool = False
76
+ sdk_turn_limit: bool = False
77
+ cancellation: bool = False
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class RuntimeAvailability:
82
+ """Availability diagnostic for a runtime in the current environment."""
83
+
84
+ kind: AgentRuntimeKind
85
+ available: bool
86
+ reason: AvailabilityReason = AvailabilityReason.UNKNOWN
87
+ message: str = ""
88
+ package: str | None = None
89
+ version: str | None = None
90
+ metadata: Mapping[str, Any] = field(default_factory=dict)
91
+
92
+ @classmethod
93
+ def ok(
94
+ cls,
95
+ kind: AgentRuntimeKind | str,
96
+ *,
97
+ package: str | None = None,
98
+ version: str | None = None,
99
+ metadata: Mapping[str, Any] | None = None,
100
+ ) -> RuntimeAvailability:
101
+ """Build a positive availability result."""
102
+
103
+ return cls(
104
+ kind=AgentRuntimeKind.coerce(kind),
105
+ available=True,
106
+ reason=AvailabilityReason.AVAILABLE,
107
+ message="available",
108
+ package=package,
109
+ version=version,
110
+ metadata=dict(metadata or {}),
111
+ )
112
+
113
+ @classmethod
114
+ def unavailable(
115
+ cls,
116
+ kind: AgentRuntimeKind | str,
117
+ *,
118
+ reason: AvailabilityReason,
119
+ message: str,
120
+ package: str | None = None,
121
+ metadata: Mapping[str, Any] | None = None,
122
+ ) -> RuntimeAvailability:
123
+ """Build a negative availability result."""
124
+
125
+ return cls(
126
+ kind=AgentRuntimeKind.coerce(kind),
127
+ available=False,
128
+ reason=reason,
129
+ message=message,
130
+ package=package,
131
+ metadata=dict(metadata or {}),
132
+ )
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class McpServerConfig:
137
+ """Configuration for a stdio MCP server owned by a vendor runtime."""
138
+
139
+ name: str
140
+ command: str
141
+ args: tuple[str, ...] = ()
142
+ env: Mapping[str, str] = field(default_factory=dict)
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class PermissionProfile:
147
+ """Portable permission request mapped by each adapter."""
148
+
149
+ mode: PermissionMode = PermissionMode.DEFAULT
150
+ filesystem: FilesystemAccess = FilesystemAccess.WORKSPACE_WRITE
151
+ allowed_tools: tuple[str, ...] = ()
152
+ disallowed_tools: tuple[str, ...] = ()
153
+ network: bool | None = None
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class ToolCallAudit:
158
+ """Best-effort audit entry for one vendor-observed tool invocation."""
159
+
160
+ tool_name: str
161
+ arguments: Mapping[str, Any] = field(default_factory=dict)
162
+ result_preview: str = ""
163
+ status: str = "ok"
164
+ duration_ms: int = 0
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class ArtifactRef:
169
+ """Reference to an artifact produced by a runtime."""
170
+
171
+ uri: str
172
+ kind: str = "file"
173
+ metadata: Mapping[str, Any] = field(default_factory=dict)
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class SessionResumeState:
178
+ """Opaque session handle carried between invocations."""
179
+
180
+ session_id: str
181
+ transcript: tuple[Any, ...] = ()
182
+
183
+
184
+ @dataclass(frozen=True)
185
+ class Usage:
186
+ """Token and cost metadata reported by a runtime."""
187
+
188
+ input_tokens: int = 0
189
+ output_tokens: int = 0
190
+ cache_read_tokens: int = 0
191
+ cache_creation_tokens: int = 0
192
+ total_tokens: int | None = None
193
+ cost_usd: float = 0.0
194
+
195
+
196
+ @dataclass(frozen=True)
197
+ class AgentTask:
198
+ """One task dispatched to an agent runtime."""
199
+
200
+ goal: str
201
+ task_id: str = field(default_factory=lambda: f"task-{uuid4().hex}")
202
+ system: str | None = None
203
+ working_directory: Path | None = None
204
+ mcp_servers: tuple[McpServerConfig, ...] = ()
205
+ permissions: PermissionProfile = field(default_factory=PermissionProfile)
206
+ event_sink: EventSink | None = None
207
+ sdk_executions: int = 1
208
+ budget_usd: float | None = None
209
+ session_id: str | None = None
210
+ resume_from: SessionResumeState | None = None
211
+ output_schema: Mapping[str, Any] | None = None
212
+ metadata: Mapping[str, Any] = field(default_factory=dict)
213
+
214
+
215
+ @dataclass(frozen=True)
216
+ class AgentResult:
217
+ """Typed result returned by all runtimes."""
218
+
219
+ output: str
220
+ finish_reason: str = "done"
221
+ error: str | None = None
222
+ parsed_output: Any | None = None
223
+ usage: Usage = field(default_factory=Usage)
224
+ tool_calls: tuple[ToolCallAudit, ...] = ()
225
+ artifacts: tuple[ArtifactRef, ...] = ()
226
+ session_id: str | None = None
227
+ rounds: int = 0
228
+ metadata: Mapping[str, Any] = field(default_factory=dict)
229
+
230
+ @property
231
+ def cost_usd(self) -> float:
232
+ """Return the reported task cost in USD."""
233
+
234
+ return self.usage.cost_usd
235
+
236
+
237
+ @runtime_checkable
238
+ class AgentRuntime(Protocol):
239
+ """Async runtime that drives an ``AgentTask`` to completion."""
240
+
241
+ kind: AgentRuntimeKind
242
+ capabilities: AgentCapabilities
243
+
244
+ def availability(self) -> RuntimeAvailability:
245
+ """Report whether this runtime can execute in the current environment."""
246
+
247
+ async def run(self, task: AgentTask) -> AgentResult:
248
+ """Execute one task."""
249
+
250
+ async def cancel(self, task_id: str) -> None:
251
+ """Request cancellation for a task if supported."""
@@ -0,0 +1,26 @@
1
+ """Vendor adapter modules."""
2
+
3
+ from agent_runtime_kit._types import AgentRuntimeKind
4
+ from agent_runtime_kit.adapters.antigravity import AntigravityAgentRuntime
5
+ from agent_runtime_kit.adapters.claude import ClaudeAgentRuntime
6
+ from agent_runtime_kit.adapters.codex import CodexAgentRuntime
7
+ from agent_runtime_kit.registry import RuntimeRegistry
8
+
9
+ __all__ = [
10
+ "AntigravityAgentRuntime",
11
+ "ClaudeAgentRuntime",
12
+ "CodexAgentRuntime",
13
+ "register_adapters",
14
+ ]
15
+
16
+
17
+ def register_adapters(registry: RuntimeRegistry, *, replace: bool = False) -> None:
18
+ """Register the built-in vendor adapters in a runtime registry."""
19
+
20
+ registry.register(AgentRuntimeKind.CLAUDE_AGENT_SDK, ClaudeAgentRuntime, replace=replace)
21
+ registry.register(AgentRuntimeKind.CODEX_AGENT_SDK, CodexAgentRuntime, replace=replace)
22
+ registry.register(
23
+ AgentRuntimeKind.ANTIGRAVITY_AGENT_SDK,
24
+ AntigravityAgentRuntime,
25
+ replace=replace,
26
+ )
@@ -0,0 +1,123 @@
1
+ """Shared helpers for optional vendor adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from collections.abc import Mapping
8
+ from importlib import metadata, util
9
+ from typing import Any
10
+
11
+ from agent_runtime_kit._errors import UnsupportedTaskInputError
12
+ from agent_runtime_kit._types import (
13
+ AgentRuntimeKind,
14
+ AvailabilityReason,
15
+ RuntimeAvailability,
16
+ )
17
+
18
+
19
+ def package_availability(
20
+ kind: AgentRuntimeKind,
21
+ *,
22
+ module_name: str,
23
+ package_name: str,
24
+ ) -> RuntimeAvailability:
25
+ """Return import/package availability without importing the package."""
26
+
27
+ try:
28
+ module_spec = util.find_spec(module_name)
29
+ except ModuleNotFoundError:
30
+ module_spec = None
31
+ if module_spec is None:
32
+ return RuntimeAvailability.unavailable(
33
+ kind,
34
+ reason=AvailabilityReason.MISSING_PACKAGE,
35
+ message=f"Install the optional dependency: agent-runtime-kit[{_extra_name(kind)}]",
36
+ package=package_name,
37
+ )
38
+ return RuntimeAvailability.ok(
39
+ kind,
40
+ package=package_name,
41
+ version=package_version(package_name),
42
+ )
43
+
44
+
45
+ def package_version(package_name: str) -> str | None:
46
+ """Return installed distribution version, if available."""
47
+
48
+ try:
49
+ return metadata.version(package_name)
50
+ except metadata.PackageNotFoundError:
51
+ return None
52
+
53
+
54
+ def ensure_supported_model(
55
+ *,
56
+ kind: AgentRuntimeKind,
57
+ model: str,
58
+ supported_models: tuple[str, ...] | None,
59
+ ) -> None:
60
+ """Raise a typed error when a runtime was configured with an allow-list."""
61
+
62
+ if supported_models is None or model in supported_models:
63
+ return
64
+ supported = ", ".join(supported_models)
65
+ raise UnsupportedTaskInputError(
66
+ kind,
67
+ "metadata.model",
68
+ f"model {model!r} is not supported by this runtime; supported: {supported}",
69
+ )
70
+
71
+
72
+ def metadata_str(metadata_values: Mapping[str, Any], key: str) -> str | None:
73
+ """Return a stripped string metadata value."""
74
+
75
+ value = metadata_values.get(key)
76
+ if not isinstance(value, str):
77
+ return None
78
+ stripped = value.strip()
79
+ return stripped or None
80
+
81
+
82
+ def output_schema_from(
83
+ task_output_schema: Mapping[str, Any] | None,
84
+ metadata_values: Mapping[str, Any],
85
+ ) -> Mapping[str, Any] | None:
86
+ """Resolve output schema from first-class task field or metadata aliases."""
87
+
88
+ if task_output_schema is not None:
89
+ return task_output_schema
90
+ for key in ("output_schema", "json_schema"):
91
+ raw = metadata_values.get(key)
92
+ if isinstance(raw, Mapping):
93
+ return raw
94
+ return None
95
+
96
+
97
+ def parse_json_output(output: str) -> Any | None:
98
+ """Best-effort JSON parsing for structured-output fallbacks."""
99
+
100
+ try:
101
+ return json.loads(output)
102
+ except json.JSONDecodeError:
103
+ return None
104
+
105
+
106
+ def filter_supported_kwargs(factory: Any, kwargs: Mapping[str, Any]) -> dict[str, Any]:
107
+ """Drop kwargs unsupported by an injected or vendor options constructor."""
108
+
109
+ try:
110
+ signature = inspect.signature(factory)
111
+ except (TypeError, ValueError):
112
+ return dict(kwargs)
113
+ if any(param.kind is inspect.Parameter.VAR_KEYWORD for param in signature.parameters.values()):
114
+ return dict(kwargs)
115
+ return {key: value for key, value in kwargs.items() if key in signature.parameters}
116
+
117
+
118
+ def _extra_name(kind: AgentRuntimeKind) -> str:
119
+ return {
120
+ AgentRuntimeKind.CLAUDE_AGENT_SDK: "claude",
121
+ AgentRuntimeKind.CODEX_AGENT_SDK: "codex",
122
+ AgentRuntimeKind.ANTIGRAVITY_AGENT_SDK: "antigravity",
123
+ }.get(kind, "all")