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.
- agentkit/__init__.py +35 -0
- agentkit/agent/__init__.py +7 -0
- agentkit/agent/agent.py +368 -0
- agentkit/agent/budgets.py +48 -0
- agentkit/agent/report.py +166 -0
- agentkit/agent/tool_runtime.py +77 -0
- agentkit/cli/__init__.py +5 -0
- agentkit/cli/main.py +108 -0
- agentkit/config/__init__.py +23 -0
- agentkit/config/loader.py +108 -0
- agentkit/config/provider_defaults.py +96 -0
- agentkit/config/schema.py +148 -0
- agentkit/constants.py +21 -0
- agentkit/errors.py +58 -0
- agentkit/llm/__init__.py +53 -0
- agentkit/llm/base.py +36 -0
- agentkit/llm/factory.py +27 -0
- agentkit/llm/providers/__init__.py +15 -0
- agentkit/llm/providers/anthropic_provider.py +371 -0
- agentkit/llm/providers/gemini_provider.py +396 -0
- agentkit/llm/providers/openai_provider.py +881 -0
- agentkit/llm/providers/qwen_provider.py +34 -0
- agentkit/llm/providers/vllm_provider.py +47 -0
- agentkit/llm/types.py +215 -0
- agentkit/llm/usage.py +72 -0
- agentkit/py.typed +0 -0
- agentkit/runlog/__init__.py +15 -0
- agentkit/runlog/events.py +67 -0
- agentkit/runlog/jsonl.py +90 -0
- agentkit/runlog/recorder.py +94 -0
- agentkit/runlog/sinks.py +15 -0
- agentkit/tools/__init__.py +16 -0
- agentkit/tools/base.py +139 -0
- agentkit/tools/library/__init__.py +8 -0
- agentkit/tools/library/_fs_common.py +330 -0
- agentkit/tools/library/create_file.py +168 -0
- agentkit/tools/library/fs_tools.py +21 -0
- agentkit/tools/library/str_replace.py +241 -0
- agentkit/tools/library/view.py +372 -0
- agentkit/tools/library/word_count.py +138 -0
- agentkit/tools/loader.py +81 -0
- agentkit/tools/registry.py +284 -0
- agentkit/tools/types.py +98 -0
- agentkit/workspace/__init__.py +6 -0
- agentkit/workspace/fs.py +288 -0
- agentkit/workspace/layout.py +33 -0
- base_agentkit-0.1.0.dist-info/METADATA +142 -0
- base_agentkit-0.1.0.dist-info/RECORD +51 -0
- base_agentkit-0.1.0.dist-info/WHEEL +4 -0
- base_agentkit-0.1.0.dist-info/entry_points.txt +3 -0
- 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
|
+
}
|
agentkit/runlog/jsonl.py
ADDED
|
@@ -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]}"
|
agentkit/runlog/sinks.py
ADDED
|
@@ -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
|
+
]
|