base-agentkit 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.
Files changed (51) hide show
  1. agentkit/__init__.py +35 -0
  2. agentkit/agent/__init__.py +7 -0
  3. agentkit/agent/agent.py +368 -0
  4. agentkit/agent/budgets.py +48 -0
  5. agentkit/agent/report.py +166 -0
  6. agentkit/agent/tool_runtime.py +77 -0
  7. agentkit/cli/__init__.py +5 -0
  8. agentkit/cli/main.py +108 -0
  9. agentkit/config/__init__.py +23 -0
  10. agentkit/config/loader.py +108 -0
  11. agentkit/config/provider_defaults.py +96 -0
  12. agentkit/config/schema.py +148 -0
  13. agentkit/constants.py +21 -0
  14. agentkit/errors.py +58 -0
  15. agentkit/llm/__init__.py +53 -0
  16. agentkit/llm/base.py +36 -0
  17. agentkit/llm/factory.py +27 -0
  18. agentkit/llm/providers/__init__.py +15 -0
  19. agentkit/llm/providers/anthropic_provider.py +371 -0
  20. agentkit/llm/providers/gemini_provider.py +396 -0
  21. agentkit/llm/providers/openai_provider.py +881 -0
  22. agentkit/llm/providers/qwen_provider.py +34 -0
  23. agentkit/llm/providers/vllm_provider.py +47 -0
  24. agentkit/llm/types.py +215 -0
  25. agentkit/llm/usage.py +72 -0
  26. agentkit/py.typed +0 -0
  27. agentkit/runlog/__init__.py +15 -0
  28. agentkit/runlog/events.py +67 -0
  29. agentkit/runlog/jsonl.py +90 -0
  30. agentkit/runlog/recorder.py +94 -0
  31. agentkit/runlog/sinks.py +15 -0
  32. agentkit/tools/__init__.py +16 -0
  33. agentkit/tools/base.py +139 -0
  34. agentkit/tools/library/__init__.py +8 -0
  35. agentkit/tools/library/_fs_common.py +330 -0
  36. agentkit/tools/library/create_file.py +168 -0
  37. agentkit/tools/library/fs_tools.py +21 -0
  38. agentkit/tools/library/str_replace.py +241 -0
  39. agentkit/tools/library/view.py +372 -0
  40. agentkit/tools/library/word_count.py +138 -0
  41. agentkit/tools/loader.py +81 -0
  42. agentkit/tools/registry.py +284 -0
  43. agentkit/tools/types.py +98 -0
  44. agentkit/workspace/__init__.py +6 -0
  45. agentkit/workspace/fs.py +288 -0
  46. agentkit/workspace/layout.py +33 -0
  47. base_agentkit-0.1.0.dist-info/METADATA +142 -0
  48. base_agentkit-0.1.0.dist-info/RECORD +51 -0
  49. base_agentkit-0.1.0.dist-info/WHEEL +4 -0
  50. base_agentkit-0.1.0.dist-info/entry_points.txt +3 -0
  51. base_agentkit-0.1.0.dist-info/licenses/LICENSE +183 -0
@@ -0,0 +1,34 @@
1
+ """Qwen provider via OpenAI-compatible Chat Completions API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agentkit.config.schema import ProviderConfig
8
+ from agentkit.llm.providers.openai_provider import OpenAIProvider
9
+ from agentkit.llm.types import UnifiedLLMRequest, UnifiedLLMResponse
10
+
11
+
12
+ class QwenProvider(OpenAIProvider):
13
+ """Qwen adapter using OpenAI-compatible chat.completions."""
14
+
15
+ def __init__(self, config: ProviderConfig) -> None:
16
+ super().__init__(config)
17
+
18
+ def generate(self, req: UnifiedLLMRequest) -> UnifiedLLMResponse:
19
+ response = super().generate(req)
20
+ response.provider_name = "qwen"
21
+ return response
22
+
23
+ def _extra_chat_kwargs(self, req: UnifiedLLMRequest) -> dict[str, Any]:
24
+ thinking_enabled = req.options.thinking_enabled
25
+ if thinking_enabled is None:
26
+ thinking_enabled = self.config.enable_thinking
27
+
28
+ extra_body: dict[str, Any] = {
29
+ "enable_thinking": bool(thinking_enabled),
30
+ }
31
+ if self.config.thinking_budget is not None:
32
+ extra_body["thinking_budget"] = self.config.thinking_budget
33
+
34
+ return {"extra_body": extra_body}
@@ -0,0 +1,47 @@
1
+ """vLLM OpenAI-compatible provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from typing import Any
7
+
8
+ from agentkit.config.provider_defaults import is_localhost_base_url
9
+ from agentkit.config.schema import ProviderConfig
10
+ from agentkit.llm.providers.openai_provider import OpenAIProvider
11
+ from agentkit.llm.types import UnifiedLLMRequest, UnifiedLLMResponse
12
+
13
+ _LOCAL_VLLM_CLIENT_API_KEY = "empty"
14
+
15
+
16
+ class VLLMProvider(OpenAIProvider):
17
+ """OpenAI chat-compatible adapter with vLLM-specific request flags."""
18
+
19
+ def __init__(self, config: ProviderConfig) -> None:
20
+ client_config = replace(config)
21
+ if client_config.api_key is None and is_localhost_base_url(
22
+ client_config.base_url
23
+ ):
24
+ client_config.api_key = _LOCAL_VLLM_CLIENT_API_KEY
25
+ super().__init__(client_config)
26
+ self.config = config
27
+
28
+ def generate(self, req: UnifiedLLMRequest) -> UnifiedLLMResponse:
29
+ response = super().generate(req)
30
+ response.provider_name = "vllm"
31
+ return response
32
+
33
+ def _allow_reasoning_effort(self) -> bool:
34
+ # vLLM's OpenAI-compatible server does not support reasoning_effort.
35
+ return False
36
+
37
+ def _extra_chat_kwargs(self, req: UnifiedLLMRequest) -> dict[str, Any]:
38
+ thinking_enabled = req.options.thinking_enabled
39
+ if thinking_enabled is None:
40
+ thinking_enabled = self.config.enable_thinking
41
+ return {
42
+ "extra_body": {
43
+ "chat_template_kwargs": {
44
+ "enable_thinking": bool(thinking_enabled),
45
+ }
46
+ }
47
+ }
agentkit/llm/types.py ADDED
@@ -0,0 +1,215 @@
1
+ """Unified provider-agnostic LLM request/response and conversation types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Literal
8
+
9
+ ProviderKind = Literal["openai", "anthropic", "gemini", "vllm", "qwen"]
10
+ ConversationMode = Literal["auto", "client", "server"]
11
+
12
+ TurnStatus = Literal["completed", "requires_tool", "incomplete", "blocked", "failed"]
13
+ CompletionReason = Literal[
14
+ "stop",
15
+ "tool_call",
16
+ "max_tokens",
17
+ "content_filter",
18
+ "refusal",
19
+ "pause",
20
+ "context_window",
21
+ "error",
22
+ "unknown",
23
+ ]
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class MessageItem:
28
+ """Plain-text message stored in unified conversation history.
29
+
30
+ Attributes:
31
+ role: Logical speaker for the message.
32
+ text: Provider-normalized text payload.
33
+ """
34
+
35
+ role: Literal["user", "assistant"]
36
+ text: str
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class ToolCallItem:
41
+ """Structured tool request emitted by a provider.
42
+
43
+ Attributes:
44
+ call_id: Stable identifier used to match the eventual tool result.
45
+ name: Registered tool name to execute.
46
+ arguments: Parsed arguments when the provider returned valid JSON.
47
+ raw_arguments: Original serialized arguments, preserved for replay fidelity.
48
+ """
49
+
50
+ call_id: str
51
+ name: str
52
+ arguments: dict[str, Any]
53
+ raw_arguments: str | None = None
54
+
55
+
56
+ @dataclass(slots=True)
57
+ class ToolResultItem:
58
+ """Tool execution result appended back into conversation history.
59
+
60
+ Attributes:
61
+ call_id: Identifier of the tool call this result satisfies.
62
+ tool_name: Tool name, retained for providers that require it on replay.
63
+ payload: Model-facing success or error payload.
64
+ is_error: Whether the tool execution failed.
65
+ """
66
+
67
+ call_id: str
68
+ tool_name: str | None = None
69
+ payload: Any = field(default_factory=dict)
70
+ is_error: bool = False
71
+
72
+ @property
73
+ def output_text(self) -> str:
74
+ """Return the payload as text for providers that require string content."""
75
+ if isinstance(self.payload, str):
76
+ return self.payload
77
+ return json.dumps(self.payload, ensure_ascii=False)
78
+
79
+
80
+ @dataclass(slots=True)
81
+ class ReasoningItem:
82
+ """Provider reasoning artifact captured separately from assistant text.
83
+
84
+ Attributes:
85
+ text: Full reasoning text when the provider exposes it.
86
+ summary: Shorter reasoning summary when full text is unavailable.
87
+ raw_item: Original provider payload used for transcript replay.
88
+ replay_hint: Whether the raw payload should be sent back on future turns.
89
+ """
90
+
91
+ text: str | None
92
+ summary: str | None = None
93
+ raw_item: dict[str, Any] | None = None
94
+ replay_hint: bool = True
95
+
96
+
97
+ ConversationItem = MessageItem | ToolCallItem | ToolResultItem | ReasoningItem
98
+
99
+
100
+ @dataclass(slots=True)
101
+ class ConversationState:
102
+ """Mutable provider conversation state carried across turns.
103
+
104
+ Attributes:
105
+ mode: Whether conversation state is managed by the client or provider.
106
+ history: Canonical transcript items that have already been committed.
107
+ provider_cursor: Provider-issued cursor or response id for server-side state.
108
+ provider_meta: Provider-specific metadata needed to continue a session.
109
+ """
110
+
111
+ mode: ConversationMode = "auto"
112
+ history: list[ConversationItem] = field(default_factory=list)
113
+ provider_cursor: str | None = None
114
+ provider_meta: dict[str, Any] = field(default_factory=dict)
115
+
116
+
117
+ @dataclass(slots=True)
118
+ class UnifiedToolSpec:
119
+ """Model-facing description of one callable tool."""
120
+
121
+ name: str
122
+ description: str
123
+ parameters: dict[str, Any]
124
+
125
+
126
+ @dataclass(slots=True)
127
+ class GenerationOptions:
128
+ """Per-request generation knobs after config defaults are applied."""
129
+
130
+ temperature: float | None = None
131
+ max_output_tokens: int | None = None
132
+ stop_sequences: list[str] | None = None
133
+ thinking_enabled: bool | None = None
134
+ reasoning_effort: str | None = None
135
+
136
+
137
+ @dataclass(slots=True)
138
+ class UnifiedLLMRequest:
139
+ """Provider-agnostic request assembled for one model turn.
140
+
141
+ Attributes:
142
+ provider: Selected provider backend.
143
+ model: Provider model identifier.
144
+ state: Conversation state from previous turns.
145
+ inputs: New conversation items to append for this turn.
146
+ instructions: System-level instructions for the turn.
147
+ tools: Tool schemas exposed to the provider.
148
+ options: Request-level generation overrides.
149
+ """
150
+
151
+ provider: ProviderKind
152
+ model: str
153
+ state: ConversationState
154
+ inputs: list[ConversationItem]
155
+ instructions: str
156
+ tools: list[UnifiedToolSpec]
157
+ options: GenerationOptions
158
+
159
+
160
+ @dataclass(slots=True)
161
+ class Usage:
162
+ """Normalized token accounting returned by providers."""
163
+
164
+ input_tokens: int | None = None
165
+ output_tokens: int | None = None
166
+ total_tokens: int | None = None
167
+ reasoning_tokens: int | None = None
168
+ cache_read_tokens: int | None = None
169
+ cache_write_tokens: int | None = None
170
+ raw: dict[str, Any] | None = None
171
+
172
+
173
+ @dataclass(slots=True)
174
+ class StatePatch:
175
+ """Incremental provider-state update returned after a model turn."""
176
+
177
+ new_provider_cursor: str | None = None
178
+ provider_meta_patch: dict[str, Any] = field(default_factory=dict)
179
+
180
+
181
+ @dataclass(slots=True)
182
+ class UnifiedLLMResponse:
183
+ """Provider-agnostic response produced for one model turn.
184
+
185
+ Attributes:
186
+ response_id: Provider response identifier when available.
187
+ status: High-level turn status used by the agent loop.
188
+ reason: Normalized completion reason.
189
+ output_items: Canonical output items emitted by the provider.
190
+ output_text: Human-facing assistant text synthesized from ``output_items``.
191
+ usage: Normalized token usage information.
192
+ state_patch: Provider-state updates to apply after committing the turn.
193
+ provider_name: Provider that produced the response.
194
+ raw_response: Original provider payload for debugging and replay support.
195
+ """
196
+
197
+ response_id: str | None
198
+ status: TurnStatus
199
+ reason: CompletionReason
200
+ output_items: list[ConversationItem]
201
+ output_text: str
202
+ usage: Usage
203
+ state_patch: StatePatch
204
+ provider_name: ProviderKind
205
+ raw_response: dict[str, Any] | None = None
206
+
207
+ @property
208
+ def tool_calls(self) -> list[ToolCallItem]:
209
+ """Return only tool calls from ``output_items`` for the agent loop."""
210
+ return [item for item in self.output_items if isinstance(item, ToolCallItem)]
211
+
212
+ @property
213
+ def has_tool_calls(self) -> bool:
214
+ """Report whether the provider requested at least one tool execution."""
215
+ return any(isinstance(item, ToolCallItem) for item in self.output_items)
agentkit/llm/usage.py ADDED
@@ -0,0 +1,72 @@
1
+ """Helpers for working with provider usage accounting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+ from agentkit.llm.types import Usage
8
+
9
+
10
+ def merge_usage(total: Usage, delta: Usage) -> None:
11
+ """Accumulate one usage snapshot into another in place."""
12
+
13
+ total.input_tokens = _sum_optional_int(total.input_tokens, delta.input_tokens)
14
+ total.output_tokens = _sum_optional_int(total.output_tokens, delta.output_tokens)
15
+ total.reasoning_tokens = _sum_optional_int(
16
+ total.reasoning_tokens, delta.reasoning_tokens
17
+ )
18
+ total.cache_read_tokens = _sum_optional_int(
19
+ total.cache_read_tokens, delta.cache_read_tokens
20
+ )
21
+ total.cache_write_tokens = _sum_optional_int(
22
+ total.cache_write_tokens, delta.cache_write_tokens
23
+ )
24
+
25
+ delta_total = delta.total_tokens
26
+ if delta_total is None and delta.input_tokens is not None and delta.output_tokens is not None:
27
+ delta_total = delta.input_tokens + delta.output_tokens
28
+ total.total_tokens = _sum_optional_int(total.total_tokens, delta_total)
29
+ total.raw = None
30
+
31
+
32
+ def usage_to_payload(usage: Usage) -> dict[str, int | None]:
33
+ """Serialize usage into the run-log friendly payload shape."""
34
+ return {
35
+ "input_tokens": usage.input_tokens,
36
+ "output_tokens": usage.output_tokens,
37
+ "total_tokens": usage.total_tokens,
38
+ "reasoning_tokens": usage.reasoning_tokens,
39
+ "cache_read_tokens": usage.cache_read_tokens,
40
+ "cache_write_tokens": usage.cache_write_tokens,
41
+ }
42
+
43
+
44
+ def usage_from_payload(payload: Mapping[str, Any]) -> Usage:
45
+ """Rebuild a :class:`Usage` object from serialized run-log data."""
46
+ return Usage(
47
+ input_tokens=_optional_int(payload.get("input_tokens")),
48
+ output_tokens=_optional_int(payload.get("output_tokens")),
49
+ total_tokens=_optional_int(payload.get("total_tokens")),
50
+ reasoning_tokens=_optional_int(payload.get("reasoning_tokens")),
51
+ cache_read_tokens=_optional_int(payload.get("cache_read_tokens")),
52
+ cache_write_tokens=_optional_int(payload.get("cache_write_tokens")),
53
+ )
54
+
55
+
56
+ def _sum_optional_int(current: int | None, delta: int | None) -> int | None:
57
+ """Add two optional counters while preserving ``None`` as unknown."""
58
+ if delta is None:
59
+ return current
60
+ if current is None:
61
+ return delta
62
+ return current + delta
63
+
64
+
65
+ def _optional_int(value: Any) -> int | None:
66
+ """Best-effort convert provider payload values into integers."""
67
+ if value is None:
68
+ return None
69
+ try:
70
+ return int(value)
71
+ except (TypeError, ValueError):
72
+ return None
agentkit/py.typed ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ """Canonical run event recording primitives."""
2
+
3
+ from .events import RUN_EVENT_SCHEMA, RunEvent, RunEventKind
4
+ from .jsonl import JsonlRunLogSink
5
+ from .recorder import RunRecorder
6
+ from .sinks import RunEventSink
7
+
8
+ __all__ = [
9
+ "JsonlRunLogSink",
10
+ "RUN_EVENT_SCHEMA",
11
+ "RunEvent",
12
+ "RunEventKind",
13
+ "RunEventSink",
14
+ "RunRecorder",
15
+ ]
@@ -0,0 +1,67 @@
1
+ """Run event schema shared by reporting and run log projections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Literal
8
+
9
+ RUN_EVENT_SCHEMA = "agentkit.run_event.v3"
10
+
11
+ RunEventKind = Literal[
12
+ "run_started",
13
+ "model_responded",
14
+ "tool_executed",
15
+ "run_finished",
16
+ ]
17
+
18
+
19
+ def _utc_now() -> str:
20
+ """Return the current UTC timestamp in ISO-8601 form."""
21
+ return datetime.now(timezone.utc).isoformat()
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class RunEvent:
26
+ """Canonical runtime event emitted by :class:`agentkit.runlog.RunRecorder`."""
27
+
28
+ schema: str
29
+ seq: int
30
+ ts: str
31
+ run_id: str
32
+ kind: RunEventKind
33
+ step: int | None = None
34
+ payload: dict[str, Any] = field(default_factory=dict)
35
+
36
+ @classmethod
37
+ def create(
38
+ cls,
39
+ *,
40
+ seq: int,
41
+ run_id: str,
42
+ kind: RunEventKind,
43
+ step: int | None = None,
44
+ payload: dict[str, Any] | None = None,
45
+ ) -> "RunEvent":
46
+ """Create a run event with the canonical schema version and timestamp."""
47
+ return cls(
48
+ schema=RUN_EVENT_SCHEMA,
49
+ seq=seq,
50
+ ts=_utc_now(),
51
+ run_id=run_id,
52
+ kind=kind,
53
+ step=step,
54
+ payload=payload or {},
55
+ )
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ """Serialize the event for sinks that need plain dictionaries."""
59
+ return {
60
+ "schema": self.schema,
61
+ "seq": self.seq,
62
+ "ts": self.ts,
63
+ "run_id": self.run_id,
64
+ "kind": self.kind,
65
+ "step": self.step,
66
+ "payload": self.payload,
67
+ }
@@ -0,0 +1,90 @@
1
+ """JSONL run log sink with basic redaction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agentkit.config.schema import RunLogConfig
10
+ from agentkit.constants import DEFAULT_RUNLOG_PATH, SENSITIVE_KEYS
11
+ from agentkit.runlog.events import RunEvent
12
+ from agentkit.workspace.fs import WorkspaceFS
13
+
14
+
15
+ class JsonlRunLogSink:
16
+ """Write canonical run events to JSONL with optional redaction."""
17
+
18
+ def __init__(self, fs: WorkspaceFS, config: RunLogConfig) -> None:
19
+ """Prepare the sink and ensure the run-log directory exists."""
20
+ self._enabled = config.enabled
21
+ self._redact = config.redact
22
+ self._max_text_chars = config.max_text_chars
23
+ self._default_path: Path = fs.resolve_path(DEFAULT_RUNLOG_PATH)
24
+ self._default_path.parent.mkdir(parents=True, exist_ok=True)
25
+ self._current_path: Path | None = None
26
+ self._current_run_id: str | None = None
27
+
28
+ @property
29
+ def enabled(self) -> bool:
30
+ """Return whether this sink should persist events to disk."""
31
+ return self._enabled
32
+
33
+ @property
34
+ def current_run_id(self) -> str | None:
35
+ """Return the run id currently being written, if any."""
36
+ return self._current_run_id
37
+
38
+ @property
39
+ def current_runlog_path(self) -> Path:
40
+ """Return the active run-log path, or the default path before a run starts."""
41
+ return self._current_path or self._default_path
42
+
43
+ def runlog_path_for_run(self, run_id: str) -> Path:
44
+ """Return the per-run JSONL path for a given run id."""
45
+ return self._default_path.parent / f"run_{run_id}.jsonl"
46
+
47
+ def consume(self, event: RunEvent) -> None:
48
+ """Persist one event, applying redaction and truncation when enabled."""
49
+ if event.kind == "run_started":
50
+ self._current_run_id = event.run_id
51
+ self._current_path = self.runlog_path_for_run(event.run_id)
52
+
53
+ if not self._enabled:
54
+ if event.kind == "run_finished":
55
+ self._current_run_id = None
56
+ self._current_path = None
57
+ return
58
+
59
+ payload: dict[str, Any] = event.to_dict()
60
+ if self._redact:
61
+ # Redact before truncation so secrets are never partially preserved in
62
+ # the run log, even when a long string would be shortened.
63
+ payload = self._sanitize(payload)
64
+
65
+ target_path = self._current_path or self.runlog_path_for_run(event.run_id)
66
+ with target_path.open("a", encoding="utf-8") as f:
67
+ f.write(json.dumps(payload, ensure_ascii=False) + "\n")
68
+
69
+ if event.kind == "run_finished":
70
+ self._current_run_id = None
71
+ self._current_path = None
72
+
73
+ def _sanitize(self, obj: Any, *, key_hint: str = "") -> Any:
74
+ """Recursively redact sensitive keys and cap very large text fields."""
75
+ if isinstance(obj, dict):
76
+ out: dict[str, Any] = {}
77
+ for key, value in obj.items():
78
+ lowered = key.lower()
79
+ if any(token in lowered for token in SENSITIVE_KEYS):
80
+ out[key] = "***REDACTED***"
81
+ else:
82
+ out[key] = self._sanitize(value, key_hint=key)
83
+ return out
84
+ if isinstance(obj, list):
85
+ return [self._sanitize(item, key_hint=key_hint) for item in obj]
86
+ if isinstance(obj, str):
87
+ if len(obj) > self._max_text_chars:
88
+ return obj[: self._max_text_chars] + "...<truncated>"
89
+ return obj
90
+ return obj
@@ -0,0 +1,94 @@
1
+ """Run-scoped canonical event recorder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from collections.abc import Callable, Sequence
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+ from agentkit.runlog.events import RunEvent, RunEventKind
11
+ from agentkit.runlog.sinks import RunEventSink
12
+
13
+
14
+ class RunRecorder:
15
+ """Emit canonical run events to one or more sinks."""
16
+
17
+ def __init__(
18
+ self,
19
+ sinks: Sequence[RunEventSink],
20
+ *,
21
+ run_id_factory: Callable[[], str] | None = None,
22
+ ) -> None:
23
+ """Create a recorder that multicasts canonical events to configured sinks."""
24
+ self._sinks = list(sinks)
25
+ self._run_id_factory = run_id_factory or _build_run_id
26
+ self._current_run_id: str | None = None
27
+ self._seq = 0
28
+
29
+ @property
30
+ def current_run_id(self) -> str | None:
31
+ """Return the active run id, or ``None`` when no run is open."""
32
+ return self._current_run_id
33
+
34
+ def start_run(
35
+ self,
36
+ *,
37
+ task: str = "",
38
+ context: dict[str, Any] | None = None,
39
+ ) -> str:
40
+ """Open a new run and emit the initial ``run_started`` event."""
41
+ if self._current_run_id is not None:
42
+ raise RuntimeError("A run is already active.")
43
+
44
+ run_id = self._run_id_factory()
45
+ self._current_run_id = run_id
46
+ self._seq = 0
47
+
48
+ self.emit(
49
+ "run_started",
50
+ payload={
51
+ "task": task,
52
+ "context": dict(context or {}),
53
+ },
54
+ )
55
+ return run_id
56
+
57
+ def emit(
58
+ self,
59
+ kind: RunEventKind,
60
+ *,
61
+ step: int | None = None,
62
+ payload: dict[str, Any] | None = None,
63
+ ) -> RunEvent:
64
+ """Emit one run event to every sink and return the created event."""
65
+ if self._current_run_id is None:
66
+ raise RuntimeError("No active run. Call start_run first.")
67
+ self._seq += 1
68
+ event = RunEvent.create(
69
+ seq=self._seq,
70
+ run_id=self._current_run_id,
71
+ kind=kind,
72
+ step=step,
73
+ payload=payload,
74
+ )
75
+ for sink in self._sinks:
76
+ sink.consume(event)
77
+ return event
78
+
79
+ def end_run(self, *, status: str, payload: dict[str, Any] | None = None) -> None:
80
+ """Emit the terminal event for the current run and clear recorder state."""
81
+ run_payload = dict(payload or {})
82
+ run_payload["status"] = status
83
+ self.emit(
84
+ "run_finished",
85
+ payload=run_payload,
86
+ )
87
+ self._current_run_id = None
88
+
89
+
90
+ def _build_run_id() -> str:
91
+ """Build a run id from UTC timestamp and random suffix."""
92
+
93
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
94
+ return f"{timestamp}_{uuid.uuid4().hex[:8]}"
@@ -0,0 +1,15 @@
1
+ """Sink protocol for run event consumers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from agentkit.runlog.events import RunEvent
8
+
9
+
10
+ class RunEventSink(Protocol):
11
+ """Consume one canonical runtime event."""
12
+
13
+ def consume(self, event: RunEvent) -> None:
14
+ """Handle one event emitted by :class:`agentkit.runlog.recorder.RunRecorder`."""
15
+ ...
@@ -0,0 +1,16 @@
1
+ """Public tool abstraction and registry exports."""
2
+
3
+ from .base import FunctionTool, Tool
4
+ from .loader import load_tools_from_library
5
+ from .registry import ToolRegistry
6
+ from .types import ToolCallOutcome, ToolInvocation, ToolModelError
7
+
8
+ __all__ = [
9
+ "FunctionTool",
10
+ "Tool",
11
+ "ToolCallOutcome",
12
+ "ToolInvocation",
13
+ "ToolModelError",
14
+ "ToolRegistry",
15
+ "load_tools_from_library",
16
+ ]