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.
- agent_runtime_kit/__init__.py +72 -0
- agent_runtime_kit/_errors.py +34 -0
- agent_runtime_kit/_runtime.py +139 -0
- agent_runtime_kit/_types.py +251 -0
- agent_runtime_kit/adapters/__init__.py +26 -0
- agent_runtime_kit/adapters/_common.py +123 -0
- agent_runtime_kit/adapters/antigravity.py +379 -0
- agent_runtime_kit/adapters/claude.py +302 -0
- agent_runtime_kit/adapters/codex.py +298 -0
- agent_runtime_kit/adapters/diagnostics.py +18 -0
- agent_runtime_kit/events.py +224 -0
- agent_runtime_kit/py.typed +1 -0
- agent_runtime_kit/registry.py +83 -0
- agent_runtime_kit/testing/__init__.py +15 -0
- agent_runtime_kit/testing/fakes.py +164 -0
- agent_runtime_kit-0.1.0.dist-info/METADATA +118 -0
- agent_runtime_kit-0.1.0.dist-info/RECORD +19 -0
- agent_runtime_kit-0.1.0.dist-info/WHEEL +4 -0
- agent_runtime_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""OpenAI Codex SDK runtime adapter."""
|
|
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
|
+
FilesystemAccess,
|
|
15
|
+
PermissionMode,
|
|
16
|
+
RuntimeAvailability,
|
|
17
|
+
Usage,
|
|
18
|
+
)
|
|
19
|
+
from agent_runtime_kit.adapters._common import (
|
|
20
|
+
ensure_supported_model,
|
|
21
|
+
metadata_str,
|
|
22
|
+
output_schema_from,
|
|
23
|
+
package_availability,
|
|
24
|
+
parse_json_output,
|
|
25
|
+
)
|
|
26
|
+
from agent_runtime_kit.events import (
|
|
27
|
+
output_delta_event,
|
|
28
|
+
safe_emit,
|
|
29
|
+
task_completed_event,
|
|
30
|
+
task_failed_event,
|
|
31
|
+
task_started_event,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CodexAgentRuntime:
|
|
36
|
+
"""Run tasks through the official ``openai_codex`` Python SDK."""
|
|
37
|
+
|
|
38
|
+
kind = AgentRuntimeKind.CODEX_AGENT_SDK
|
|
39
|
+
capabilities = AgentCapabilities(
|
|
40
|
+
mcp_support=False,
|
|
41
|
+
working_directory=True,
|
|
42
|
+
session_resume=True,
|
|
43
|
+
structured_output=True,
|
|
44
|
+
streaming=False,
|
|
45
|
+
tool_audit=True,
|
|
46
|
+
cancellation=False,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
default_model: str = "gpt-5.5",
|
|
53
|
+
supported_models: tuple[str, ...] | None = None,
|
|
54
|
+
codex_cls: Any | None = None,
|
|
55
|
+
config_cls: Any | None = None,
|
|
56
|
+
sandbox_cls: Any | None = None,
|
|
57
|
+
approval_mode_cls: Any | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._default_model = default_model
|
|
60
|
+
self._supported_models = supported_models
|
|
61
|
+
self._codex_cls = codex_cls
|
|
62
|
+
self._config_cls = config_cls
|
|
63
|
+
self._sandbox_cls = sandbox_cls
|
|
64
|
+
self._approval_mode_cls = approval_mode_cls
|
|
65
|
+
|
|
66
|
+
def availability(self) -> RuntimeAvailability:
|
|
67
|
+
"""Report OpenAI Codex SDK package availability."""
|
|
68
|
+
|
|
69
|
+
if self._codex_cls is not None:
|
|
70
|
+
return RuntimeAvailability.ok(self.kind, package="openai-codex")
|
|
71
|
+
return package_availability(
|
|
72
|
+
self.kind,
|
|
73
|
+
module_name="openai_codex",
|
|
74
|
+
package_name="openai-codex",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def run(self, task: AgentTask) -> AgentResult:
|
|
78
|
+
"""Execute one task with the Codex SDK."""
|
|
79
|
+
|
|
80
|
+
await safe_emit(task, task_started_event(task, self.kind))
|
|
81
|
+
try:
|
|
82
|
+
if task.mcp_servers:
|
|
83
|
+
raise UnsupportedTaskInputError(
|
|
84
|
+
self.kind,
|
|
85
|
+
"mcp_servers",
|
|
86
|
+
"openai_codex does not expose per-task MCP server configuration",
|
|
87
|
+
)
|
|
88
|
+
model = self._model(task)
|
|
89
|
+
ensure_supported_model(
|
|
90
|
+
kind=self.kind,
|
|
91
|
+
model=model,
|
|
92
|
+
supported_models=self._supported_models,
|
|
93
|
+
)
|
|
94
|
+
codex_cls, config_cls, sandbox_cls, approval_mode_cls = self._load_sdk()
|
|
95
|
+
result = await self._run_codex(
|
|
96
|
+
task,
|
|
97
|
+
model=model,
|
|
98
|
+
codex_cls=codex_cls,
|
|
99
|
+
config_cls=config_cls,
|
|
100
|
+
sandbox_cls=sandbox_cls,
|
|
101
|
+
approval_mode_cls=approval_mode_cls,
|
|
102
|
+
)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
await safe_emit(task, task_failed_event(task, self.kind, error=str(exc)))
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
if result.output:
|
|
108
|
+
await safe_emit(task, output_delta_event(task, self.kind, text=result.output))
|
|
109
|
+
if result.error:
|
|
110
|
+
await safe_emit(task, task_failed_event(task, self.kind, error=result.error))
|
|
111
|
+
else:
|
|
112
|
+
await safe_emit(task, task_completed_event(task, self.kind, result))
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
async def cancel(self, task_id: str) -> None:
|
|
116
|
+
"""Codex SDK cancellation is not exposed through this portable adapter yet."""
|
|
117
|
+
|
|
118
|
+
del task_id
|
|
119
|
+
|
|
120
|
+
def _load_sdk(self) -> tuple[Any, Any, Any, Any]:
|
|
121
|
+
if self._codex_cls is not None and self._config_cls is not None:
|
|
122
|
+
return (
|
|
123
|
+
self._codex_cls,
|
|
124
|
+
self._config_cls,
|
|
125
|
+
self._sandbox_cls,
|
|
126
|
+
self._approval_mode_cls,
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
from openai_codex import ( # type: ignore[import-not-found]
|
|
130
|
+
ApprovalMode,
|
|
131
|
+
AsyncCodex,
|
|
132
|
+
CodexConfig,
|
|
133
|
+
Sandbox,
|
|
134
|
+
)
|
|
135
|
+
except ImportError as exc:
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"openai-codex is not installed. Install agent-runtime-kit[codex]."
|
|
138
|
+
) from exc
|
|
139
|
+
return (
|
|
140
|
+
self._codex_cls or AsyncCodex,
|
|
141
|
+
self._config_cls or CodexConfig,
|
|
142
|
+
self._sandbox_cls or Sandbox,
|
|
143
|
+
self._approval_mode_cls or ApprovalMode,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async def _run_codex(
|
|
147
|
+
self,
|
|
148
|
+
task: AgentTask,
|
|
149
|
+
*,
|
|
150
|
+
model: str,
|
|
151
|
+
codex_cls: Any,
|
|
152
|
+
config_cls: Any,
|
|
153
|
+
sandbox_cls: Any,
|
|
154
|
+
approval_mode_cls: Any,
|
|
155
|
+
) -> AgentResult:
|
|
156
|
+
cwd = str(task.working_directory) if task.working_directory else None
|
|
157
|
+
config = config_cls(cwd=cwd, config_overrides=("features.plugins=false",))
|
|
158
|
+
async with codex_cls(config=config) as codex:
|
|
159
|
+
thread = await self._start_or_resume_thread(
|
|
160
|
+
codex,
|
|
161
|
+
task,
|
|
162
|
+
model=model,
|
|
163
|
+
cwd=cwd,
|
|
164
|
+
sandbox_cls=sandbox_cls,
|
|
165
|
+
approval_mode_cls=approval_mode_cls,
|
|
166
|
+
)
|
|
167
|
+
run_kwargs = {
|
|
168
|
+
"cwd": cwd,
|
|
169
|
+
"model": model,
|
|
170
|
+
"approval_mode": _approval_mode(task.permissions.mode, approval_mode_cls),
|
|
171
|
+
"sandbox": _sandbox_mode(task.permissions.filesystem, sandbox_cls),
|
|
172
|
+
}
|
|
173
|
+
schema = output_schema_from(task.output_schema, task.metadata)
|
|
174
|
+
if schema is not None:
|
|
175
|
+
run_kwargs["output_schema"] = dict(schema)
|
|
176
|
+
effort = metadata_str(task.metadata, "reasoning_effort")
|
|
177
|
+
if effort:
|
|
178
|
+
run_kwargs["effort"] = effort
|
|
179
|
+
raw_result = await thread.run(task.goal, **run_kwargs)
|
|
180
|
+
return _translate_run_result(task, raw_result, model=model, session_id=_thread_id(thread))
|
|
181
|
+
|
|
182
|
+
async def _start_or_resume_thread(
|
|
183
|
+
self,
|
|
184
|
+
codex: Any,
|
|
185
|
+
task: AgentTask,
|
|
186
|
+
*,
|
|
187
|
+
model: str,
|
|
188
|
+
cwd: str | None,
|
|
189
|
+
sandbox_cls: Any,
|
|
190
|
+
approval_mode_cls: Any,
|
|
191
|
+
) -> Any:
|
|
192
|
+
kwargs = {
|
|
193
|
+
"cwd": cwd,
|
|
194
|
+
"developer_instructions": task.system,
|
|
195
|
+
"model": model,
|
|
196
|
+
"approval_mode": _approval_mode(task.permissions.mode, approval_mode_cls),
|
|
197
|
+
"sandbox": _sandbox_mode(task.permissions.filesystem, sandbox_cls),
|
|
198
|
+
}
|
|
199
|
+
thread_id = task.resume_from.session_id if task.resume_from is not None else task.session_id
|
|
200
|
+
if thread_id:
|
|
201
|
+
return await codex.thread_resume(thread_id, **kwargs)
|
|
202
|
+
return await codex.thread_start(**kwargs)
|
|
203
|
+
|
|
204
|
+
def _model(self, task: AgentTask) -> str:
|
|
205
|
+
return metadata_str(task.metadata, "model") or self._default_model
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _translate_run_result(
|
|
209
|
+
task: AgentTask,
|
|
210
|
+
raw_result: Any,
|
|
211
|
+
*,
|
|
212
|
+
model: str,
|
|
213
|
+
session_id: str | None,
|
|
214
|
+
) -> AgentResult:
|
|
215
|
+
output = str(_field(raw_result, "final_response", "") or "")
|
|
216
|
+
usage = _codex_usage(_field(raw_result, "usage"))
|
|
217
|
+
schema = output_schema_from(task.output_schema, task.metadata)
|
|
218
|
+
parsed = parse_json_output(output) if schema is not None else None
|
|
219
|
+
if schema is not None and parsed is None:
|
|
220
|
+
return AgentResult(
|
|
221
|
+
output=output,
|
|
222
|
+
finish_reason="failed",
|
|
223
|
+
error="Codex SDK returned output that did not satisfy output_schema",
|
|
224
|
+
usage=usage,
|
|
225
|
+
session_id=session_id,
|
|
226
|
+
rounds=1,
|
|
227
|
+
metadata={"model": model, "sdk": "openai_codex"},
|
|
228
|
+
)
|
|
229
|
+
if not output:
|
|
230
|
+
return AgentResult(
|
|
231
|
+
output="",
|
|
232
|
+
finish_reason="failed",
|
|
233
|
+
error="Codex SDK completed without final_response",
|
|
234
|
+
usage=usage,
|
|
235
|
+
session_id=session_id,
|
|
236
|
+
metadata={"model": model, "sdk": "openai_codex"},
|
|
237
|
+
)
|
|
238
|
+
return AgentResult(
|
|
239
|
+
output=output,
|
|
240
|
+
parsed_output=parsed,
|
|
241
|
+
usage=usage,
|
|
242
|
+
session_id=session_id,
|
|
243
|
+
rounds=1,
|
|
244
|
+
metadata={"model": model, "sdk": "openai_codex"},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _codex_usage(value: Any) -> Usage:
|
|
249
|
+
total = _field(value, "total")
|
|
250
|
+
if total is None and isinstance(value, Mapping):
|
|
251
|
+
total = value.get("total", value)
|
|
252
|
+
input_tokens = _optional_int(_field(total, "input_tokens"))
|
|
253
|
+
output_tokens = _optional_int(_field(total, "output_tokens"))
|
|
254
|
+
cached = _optional_int(_field(total, "cached_input_tokens"))
|
|
255
|
+
total_tokens = _optional_int(_field(total, "total_tokens"))
|
|
256
|
+
return Usage(
|
|
257
|
+
input_tokens=input_tokens,
|
|
258
|
+
output_tokens=output_tokens,
|
|
259
|
+
cache_read_tokens=cached,
|
|
260
|
+
total_tokens=total_tokens or input_tokens + output_tokens + cached,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _approval_mode(mode: PermissionMode, approval_mode_cls: Any) -> Any:
|
|
265
|
+
if approval_mode_cls is None:
|
|
266
|
+
return "auto_review" if mode is not PermissionMode.PERMISSIVE else "deny_all"
|
|
267
|
+
if mode is PermissionMode.PERMISSIVE:
|
|
268
|
+
return getattr(approval_mode_cls, "deny_all", "deny_all")
|
|
269
|
+
return getattr(approval_mode_cls, "auto_review", "auto_review")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _sandbox_mode(filesystem: FilesystemAccess, sandbox_cls: Any) -> Any:
|
|
273
|
+
name = {
|
|
274
|
+
FilesystemAccess.READ_ONLY: "read_only",
|
|
275
|
+
FilesystemAccess.WORKSPACE_WRITE: "workspace_write",
|
|
276
|
+
FilesystemAccess.FULL_ACCESS: "full_access",
|
|
277
|
+
}[filesystem]
|
|
278
|
+
if sandbox_cls is None:
|
|
279
|
+
return name.replace("_", "-")
|
|
280
|
+
return getattr(sandbox_cls, name, name.replace("_", "-"))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _thread_id(thread: Any) -> str | None:
|
|
284
|
+
value = getattr(thread, "id", None)
|
|
285
|
+
return str(value) if value else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _field(value: Any, name: str, default: Any = None) -> Any:
|
|
289
|
+
if isinstance(value, Mapping):
|
|
290
|
+
return value.get(name, default)
|
|
291
|
+
return getattr(value, name, default)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _optional_int(value: Any) -> int:
|
|
295
|
+
try:
|
|
296
|
+
return int(value or 0)
|
|
297
|
+
except (TypeError, ValueError):
|
|
298
|
+
return 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Provider adapter diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agent_runtime_kit._types import RuntimeAvailability
|
|
6
|
+
from agent_runtime_kit.adapters.antigravity import AntigravityAgentRuntime
|
|
7
|
+
from agent_runtime_kit.adapters.claude import ClaudeAgentRuntime
|
|
8
|
+
from agent_runtime_kit.adapters.codex import CodexAgentRuntime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def collect_provider_diagnostics() -> tuple[RuntimeAvailability, ...]:
|
|
12
|
+
"""Return availability diagnostics for installed provider adapters."""
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
ClaudeAgentRuntime().availability(),
|
|
16
|
+
CodexAgentRuntime().availability(),
|
|
17
|
+
AntigravityAgentRuntime().availability(),
|
|
18
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Normalized runtime event helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agent_runtime_kit._types import AgentResult, AgentRuntimeKind, AgentTask, ToolCallAudit
|
|
10
|
+
|
|
11
|
+
DEFAULT_PREVIEW_CHARS = 1000
|
|
12
|
+
SENSITIVE_KEY_PARTS = ("api_key", "apikey", "authorization", "password", "secret", "token")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def task_started_event(
|
|
16
|
+
task: AgentTask,
|
|
17
|
+
kind: AgentRuntimeKind | str,
|
|
18
|
+
*,
|
|
19
|
+
summary: str | None = None,
|
|
20
|
+
extra: Mapping[str, Any] | None = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""Build an ``agent.task.started`` event."""
|
|
23
|
+
|
|
24
|
+
attrs = {
|
|
25
|
+
"task_id": task.task_id,
|
|
26
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
27
|
+
"session_id": task.session_id,
|
|
28
|
+
"task_goal": task.goal,
|
|
29
|
+
"system_prompt": task.system,
|
|
30
|
+
"working_directory": str(task.working_directory) if task.working_directory else None,
|
|
31
|
+
"sdk_executions": task.sdk_executions,
|
|
32
|
+
"budget_usd": task.budget_usd,
|
|
33
|
+
"metadata": dict(task.metadata),
|
|
34
|
+
}
|
|
35
|
+
if extra:
|
|
36
|
+
attrs.update(extra)
|
|
37
|
+
return _event("agent.task.started", summary or f"started task {task.task_id}", attrs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def task_completed_event(
|
|
41
|
+
task: AgentTask,
|
|
42
|
+
kind: AgentRuntimeKind | str,
|
|
43
|
+
result: AgentResult,
|
|
44
|
+
*,
|
|
45
|
+
summary: str | None = None,
|
|
46
|
+
extra: Mapping[str, Any] | None = None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Build an ``agent.task.completed`` event."""
|
|
49
|
+
|
|
50
|
+
attrs = {
|
|
51
|
+
"task_id": task.task_id,
|
|
52
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
53
|
+
"finish_reason": result.finish_reason,
|
|
54
|
+
"output": result.output,
|
|
55
|
+
"rounds": result.rounds,
|
|
56
|
+
"cost_usd": result.cost_usd,
|
|
57
|
+
"session_id": result.session_id,
|
|
58
|
+
"tool_call_count": len(result.tool_calls),
|
|
59
|
+
"result_metadata": dict(result.metadata),
|
|
60
|
+
}
|
|
61
|
+
if extra:
|
|
62
|
+
attrs.update(extra)
|
|
63
|
+
return _event(
|
|
64
|
+
"agent.task.completed",
|
|
65
|
+
summary or f"completed task {task.task_id} ({result.finish_reason})",
|
|
66
|
+
attrs,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def task_failed_event(
|
|
71
|
+
task: AgentTask,
|
|
72
|
+
kind: AgentRuntimeKind | str,
|
|
73
|
+
*,
|
|
74
|
+
error: str,
|
|
75
|
+
finish_reason: str = "failed",
|
|
76
|
+
summary: str | None = None,
|
|
77
|
+
extra: Mapping[str, Any] | None = None,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Build an ``agent.task.failed`` event."""
|
|
80
|
+
|
|
81
|
+
attrs = {
|
|
82
|
+
"task_id": task.task_id,
|
|
83
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
84
|
+
"finish_reason": finish_reason,
|
|
85
|
+
"error": error,
|
|
86
|
+
}
|
|
87
|
+
if extra:
|
|
88
|
+
attrs.update(extra)
|
|
89
|
+
return _event("agent.task.failed", summary or f"failed task {task.task_id}", attrs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def output_delta_event(
|
|
93
|
+
task: AgentTask,
|
|
94
|
+
kind: AgentRuntimeKind | str,
|
|
95
|
+
*,
|
|
96
|
+
text: str,
|
|
97
|
+
summary: str | None = None,
|
|
98
|
+
extra: Mapping[str, Any] | None = None,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""Build an ``agent.output.delta`` event."""
|
|
101
|
+
|
|
102
|
+
preview, truncated = _preview(text)
|
|
103
|
+
attrs = {
|
|
104
|
+
"task_id": task.task_id,
|
|
105
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
106
|
+
"text_delta": preview,
|
|
107
|
+
"text_delta_length": len(text),
|
|
108
|
+
"text_delta_truncated": truncated,
|
|
109
|
+
}
|
|
110
|
+
if extra:
|
|
111
|
+
attrs.update(extra)
|
|
112
|
+
return _event("agent.output.delta", summary or f"streamed {len(text)} chars", attrs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def tool_requested_event(
|
|
116
|
+
task: AgentTask,
|
|
117
|
+
kind: AgentRuntimeKind | str,
|
|
118
|
+
*,
|
|
119
|
+
tool_name: str,
|
|
120
|
+
arguments: Mapping[str, Any] | None = None,
|
|
121
|
+
summary: str | None = None,
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""Build an ``agent.tool.requested`` event."""
|
|
124
|
+
|
|
125
|
+
attrs = {
|
|
126
|
+
"task_id": task.task_id,
|
|
127
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
128
|
+
"tool_name": tool_name,
|
|
129
|
+
"argument_count": len(arguments or {}),
|
|
130
|
+
"argument_keys": sorted(str(key) for key in (arguments or {})),
|
|
131
|
+
}
|
|
132
|
+
return _event("agent.tool.requested", summary or f"tool requested: {tool_name}", attrs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def tool_completed_event(
|
|
136
|
+
task: AgentTask,
|
|
137
|
+
kind: AgentRuntimeKind | str,
|
|
138
|
+
audit: ToolCallAudit,
|
|
139
|
+
*,
|
|
140
|
+
summary: str | None = None,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
"""Build an ``agent.tool.completed`` event."""
|
|
143
|
+
|
|
144
|
+
attrs = {
|
|
145
|
+
"task_id": task.task_id,
|
|
146
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
147
|
+
"tool_name": audit.tool_name,
|
|
148
|
+
"status": audit.status,
|
|
149
|
+
"duration_ms": audit.duration_ms,
|
|
150
|
+
"result_preview_length": len(audit.result_preview),
|
|
151
|
+
}
|
|
152
|
+
return _event(
|
|
153
|
+
"agent.tool.completed",
|
|
154
|
+
summary or f"tool completed: {audit.tool_name} ({audit.status})",
|
|
155
|
+
attrs,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def vendor_turn_event(
|
|
160
|
+
task: AgentTask,
|
|
161
|
+
kind: AgentRuntimeKind | str,
|
|
162
|
+
*,
|
|
163
|
+
payload: Mapping[str, Any] | None = None,
|
|
164
|
+
turn_index: int | None = None,
|
|
165
|
+
summary: str | None = None,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Build an ``agent.vendor.turn`` event."""
|
|
168
|
+
|
|
169
|
+
attrs = {
|
|
170
|
+
"task_id": task.task_id,
|
|
171
|
+
"runtime_kind": AgentRuntimeKind.coerce(kind).value,
|
|
172
|
+
"turn_index": turn_index,
|
|
173
|
+
"payload": dict(payload or {}),
|
|
174
|
+
}
|
|
175
|
+
return _event("agent.vendor.turn", summary or "vendor turn update", attrs)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def safe_emit(task: AgentTask, event: Mapping[str, Any]) -> None:
|
|
179
|
+
"""Best-effort event emission that never aborts a runtime."""
|
|
180
|
+
|
|
181
|
+
if task.event_sink is None:
|
|
182
|
+
return
|
|
183
|
+
try:
|
|
184
|
+
await task.event_sink.emit(event)
|
|
185
|
+
except Exception:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _event(name: str, summary: str, attributes: Mapping[str, Any]) -> dict[str, Any]:
|
|
190
|
+
return {
|
|
191
|
+
"name": name,
|
|
192
|
+
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
|
|
193
|
+
"summary": summary,
|
|
194
|
+
"attributes": _sanitize(attributes),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _sanitize(value: Any, *, key: str | None = None) -> Any:
|
|
199
|
+
if key is not None and _is_sensitive_key(key):
|
|
200
|
+
return "[redacted]"
|
|
201
|
+
if isinstance(value, Mapping):
|
|
202
|
+
return {
|
|
203
|
+
str(item_key): _sanitize(item_value, key=str(item_key))
|
|
204
|
+
for item_key, item_value in value.items()
|
|
205
|
+
}
|
|
206
|
+
if isinstance(value, tuple):
|
|
207
|
+
return tuple(_sanitize(item) for item in value)
|
|
208
|
+
if isinstance(value, list):
|
|
209
|
+
return [_sanitize(item) for item in value]
|
|
210
|
+
if isinstance(value, str):
|
|
211
|
+
preview, truncated = _preview(value)
|
|
212
|
+
return f"{preview}...[truncated]" if truncated else preview
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _preview(text: str) -> tuple[str, bool]:
|
|
217
|
+
if len(text) <= DEFAULT_PREVIEW_CHARS:
|
|
218
|
+
return text, False
|
|
219
|
+
return text[:DEFAULT_PREVIEW_CHARS], True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _is_sensitive_key(key: str) -> bool:
|
|
223
|
+
lowered = key.lower().replace("-", "_")
|
|
224
|
+
return any(part in lowered for part in SENSITIVE_KEY_PARTS)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Runtime registry helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_runtime_kit._errors import RuntimeNotRegisteredError
|
|
9
|
+
from agent_runtime_kit._runtime import FakeAgentRuntime
|
|
10
|
+
from agent_runtime_kit._types import (
|
|
11
|
+
AgentCapabilities,
|
|
12
|
+
AgentRuntime,
|
|
13
|
+
AgentRuntimeKind,
|
|
14
|
+
RuntimeAvailability,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
RuntimeFactory = Callable[..., AgentRuntime]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RuntimeRegistry:
|
|
21
|
+
"""Register and resolve runtime factories by kind."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._factories: dict[AgentRuntimeKind, RuntimeFactory] = {}
|
|
25
|
+
|
|
26
|
+
def register(
|
|
27
|
+
self,
|
|
28
|
+
kind: AgentRuntimeKind | str,
|
|
29
|
+
factory: RuntimeFactory,
|
|
30
|
+
*,
|
|
31
|
+
replace: bool = False,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Register a runtime factory."""
|
|
34
|
+
|
|
35
|
+
normalized = AgentRuntimeKind.coerce(kind)
|
|
36
|
+
if normalized in self._factories and not replace:
|
|
37
|
+
raise ValueError(f"Runtime already registered for {normalized.value!r}")
|
|
38
|
+
self._factories[normalized] = factory
|
|
39
|
+
|
|
40
|
+
def unregister(self, kind: AgentRuntimeKind | str) -> None:
|
|
41
|
+
"""Remove a registered runtime factory."""
|
|
42
|
+
|
|
43
|
+
normalized = AgentRuntimeKind.coerce(kind)
|
|
44
|
+
self._factories.pop(normalized, None)
|
|
45
|
+
|
|
46
|
+
def kinds(self) -> tuple[AgentRuntimeKind, ...]:
|
|
47
|
+
"""Return registered runtime kinds."""
|
|
48
|
+
|
|
49
|
+
return tuple(self._factories)
|
|
50
|
+
|
|
51
|
+
def resolve(self, kind: AgentRuntimeKind | str, **kwargs: Any) -> AgentRuntime:
|
|
52
|
+
"""Construct a runtime for ``kind``."""
|
|
53
|
+
|
|
54
|
+
normalized = AgentRuntimeKind.coerce(kind)
|
|
55
|
+
factory = self._factories.get(normalized)
|
|
56
|
+
if factory is None:
|
|
57
|
+
raise RuntimeNotRegisteredError(normalized)
|
|
58
|
+
return factory(**kwargs)
|
|
59
|
+
|
|
60
|
+
def capabilities_for(self, kind: AgentRuntimeKind | str) -> AgentCapabilities:
|
|
61
|
+
"""Construct a runtime and return its advertised capabilities."""
|
|
62
|
+
|
|
63
|
+
return self.resolve(kind).capabilities
|
|
64
|
+
|
|
65
|
+
def availability_for(self, kind: AgentRuntimeKind | str) -> RuntimeAvailability:
|
|
66
|
+
"""Construct a runtime and return its availability diagnostic."""
|
|
67
|
+
|
|
68
|
+
return self.resolve(kind).availability()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_default_registry(
|
|
72
|
+
*,
|
|
73
|
+
include_fake: bool = True,
|
|
74
|
+
extra_factories: Iterable[tuple[AgentRuntimeKind | str, RuntimeFactory]] = (),
|
|
75
|
+
) -> RuntimeRegistry:
|
|
76
|
+
"""Create a registry with built-in dependency-free runtimes."""
|
|
77
|
+
|
|
78
|
+
registry = RuntimeRegistry()
|
|
79
|
+
if include_fake:
|
|
80
|
+
registry.register(AgentRuntimeKind.FAKE, FakeAgentRuntime)
|
|
81
|
+
for kind, factory in extra_factories:
|
|
82
|
+
registry.register(kind, factory)
|
|
83
|
+
return registry
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Testing helpers for adapter contract tests."""
|
|
2
|
+
|
|
3
|
+
from agent_runtime_kit.testing.fakes import (
|
|
4
|
+
FakeSDKHarness,
|
|
5
|
+
FakeSDKRuntime,
|
|
6
|
+
FakeSDKScenario,
|
|
7
|
+
RecordingEventSink,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"FakeSDKHarness",
|
|
12
|
+
"FakeSDKRuntime",
|
|
13
|
+
"FakeSDKScenario",
|
|
14
|
+
"RecordingEventSink",
|
|
15
|
+
]
|