agentforge-harness 0.1.2__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.
- agentforge_harness/__init__.py +3 -0
- agentforge_harness/agent/__init__.py +0 -0
- agentforge_harness/agent/agent.py +332 -0
- agentforge_harness/agent/events.py +106 -0
- agentforge_harness/agent/modes.py +6 -0
- agentforge_harness/agent/persistence.py +258 -0
- agentforge_harness/agent/session.py +297 -0
- agentforge_harness/cli/__init__.py +0 -0
- agentforge_harness/cli/commands.py +860 -0
- agentforge_harness/cli/doctor.py +819 -0
- agentforge_harness/cli/models.py +146 -0
- agentforge_harness/cli/report.py +249 -0
- agentforge_harness/cli/run.py +163 -0
- agentforge_harness/cli/setup.py +275 -0
- agentforge_harness/client/__init__.py +0 -0
- agentforge_harness/client/llm_client.py +410 -0
- agentforge_harness/client/response.py +87 -0
- agentforge_harness/config/__init__.py +0 -0
- agentforge_harness/config/config.py +246 -0
- agentforge_harness/config/loader.py +170 -0
- agentforge_harness/context/__init__.py +0 -0
- agentforge_harness/context/compaction.py +93 -0
- agentforge_harness/context/loop_detector.py +51 -0
- agentforge_harness/context/manager.py +290 -0
- agentforge_harness/hooks/__init__.py +0 -0
- agentforge_harness/hooks/hook_system.py +152 -0
- agentforge_harness/prompts/__init__.py +0 -0
- agentforge_harness/prompts/system.py +381 -0
- agentforge_harness/safety/__init__.py +0 -0
- agentforge_harness/safety/approval.py +187 -0
- agentforge_harness/safety/circuit_breaker.py +78 -0
- agentforge_harness/safety/output_hygiene.py +169 -0
- agentforge_harness/safety/prompt_injection.py +58 -0
- agentforge_harness/skills/__init__.py +0 -0
- agentforge_harness/skills/manager.py +473 -0
- agentforge_harness/tools/__init__.py +0 -0
- agentforge_harness/tools/base.py +231 -0
- agentforge_harness/tools/builtin/__init__.py +50 -0
- agentforge_harness/tools/builtin/append_file.py +121 -0
- agentforge_harness/tools/builtin/edit_file.py +241 -0
- agentforge_harness/tools/builtin/git_diff.py +182 -0
- agentforge_harness/tools/builtin/glob.py +68 -0
- agentforge_harness/tools/builtin/grep.py +132 -0
- agentforge_harness/tools/builtin/list_dir.py +76 -0
- agentforge_harness/tools/builtin/memory.py +155 -0
- agentforge_harness/tools/builtin/patch.py +566 -0
- agentforge_harness/tools/builtin/read_file.py +169 -0
- agentforge_harness/tools/builtin/shell.py +184 -0
- agentforge_harness/tools/builtin/todo.py +92 -0
- agentforge_harness/tools/builtin/web_fetch.py +68 -0
- agentforge_harness/tools/builtin/web_search.py +72 -0
- agentforge_harness/tools/builtin/write_file.py +120 -0
- agentforge_harness/tools/discovery.py +74 -0
- agentforge_harness/tools/mcp/__init__.py +0 -0
- agentforge_harness/tools/mcp/client.py +103 -0
- agentforge_harness/tools/mcp/mcp_manager.py +91 -0
- agentforge_harness/tools/mcp/mcp_tool.py +54 -0
- agentforge_harness/tools/registry.py +191 -0
- agentforge_harness/tools/subagents.py +226 -0
- agentforge_harness/ui/__init__.py +0 -0
- agentforge_harness/ui/tui.py +1142 -0
- agentforge_harness/utils/__init__.py +0 -0
- agentforge_harness/utils/errors.py +49 -0
- agentforge_harness/utils/paths.py +55 -0
- agentforge_harness/utils/redaction.py +213 -0
- agentforge_harness/utils/text.py +78 -0
- agentforge_harness-0.1.2.dist-info/METADATA +1028 -0
- agentforge_harness-0.1.2.dist-info/RECORD +72 -0
- agentforge_harness-0.1.2.dist-info/WHEEL +5 -0
- agentforge_harness-0.1.2.dist-info/entry_points.txt +2 -0
- agentforge_harness-0.1.2.dist-info/licenses/LICENSE +21 -0
- agentforge_harness-0.1.2.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
from typing import AsyncGenerator, Callable
|
|
7
|
+
from agentforge_harness.agent.events import AgentEvent, AgentEventType
|
|
8
|
+
from agentforge_harness.agent.modes import AgentMode
|
|
9
|
+
from agentforge_harness.agent.session import Session
|
|
10
|
+
from agentforge_harness.client.response import StreamEventType, TokenUsage, ToolCall, ToolResultMessage
|
|
11
|
+
from agentforge_harness.config.config import Config
|
|
12
|
+
from agentforge_harness.prompts.system import create_loop_breaker_prompt
|
|
13
|
+
from agentforge_harness.tools.base import ToolConfirmation, ToolResult
|
|
14
|
+
from agentforge_harness.utils.redaction import redact_tool_params
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Agent:
|
|
20
|
+
def __init__(self, config: Config, confirmation_callback: Callable[[ToolConfirmation], bool] | None = None):
|
|
21
|
+
self.config = config
|
|
22
|
+
self.session: Session | None = Session(self.config)
|
|
23
|
+
self.session.approval_manager.confirmation_callback = confirmation_callback
|
|
24
|
+
|
|
25
|
+
async def run(self, message: str):
|
|
26
|
+
await self.session.hook_system.trigger_before_agent(message)
|
|
27
|
+
yield AgentEvent.agents_start(message)
|
|
28
|
+
self.session.context_manager.add_user_message(message)
|
|
29
|
+
self.session.loop_detector.clear()
|
|
30
|
+
|
|
31
|
+
final_response: str | None = None
|
|
32
|
+
|
|
33
|
+
async for event in self._agentic_loop():
|
|
34
|
+
yield event
|
|
35
|
+
|
|
36
|
+
if event.type == AgentEventType.TEXT_COMPLETE:
|
|
37
|
+
final_response = event.data.get("content")
|
|
38
|
+
await self.session.hook_system.trigger_after_agent(message, final_response or "")
|
|
39
|
+
yield AgentEvent.agents_end(final_response)
|
|
40
|
+
|
|
41
|
+
async def _agentic_loop(self) -> AsyncGenerator[AgentEvent, None]:
|
|
42
|
+
max_turns = self.config.max_turns
|
|
43
|
+
if self.session.mode == AgentMode.PLAN:
|
|
44
|
+
max_turns = min(max_turns, 8)
|
|
45
|
+
max_llm_retries = 3
|
|
46
|
+
plan_tool_budget = 8
|
|
47
|
+
plan_tool_calls = 0
|
|
48
|
+
force_plan_response = False
|
|
49
|
+
|
|
50
|
+
model_chain = [
|
|
51
|
+
self.config.model_name,
|
|
52
|
+
*(self.config.model.fallbacks or []),
|
|
53
|
+
]
|
|
54
|
+
circuit_breaker = self.session.circuit_breaker
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
for turn_num in range(max_turns):
|
|
58
|
+
self.session.increment_turn()
|
|
59
|
+
|
|
60
|
+
# check context budget and auto-compress if needed
|
|
61
|
+
budget = self.session.context_manager.get_context_budget()
|
|
62
|
+
if budget["warning"]:
|
|
63
|
+
if budget["total_tokens"] > 0:
|
|
64
|
+
yield AgentEvent.text_delta(
|
|
65
|
+
f"\n[Context: {budget['usage_pct']}% ({budget['total_tokens']}/{budget['context_window']} tokens)]"
|
|
66
|
+
)
|
|
67
|
+
if budget["critical"] or budget["usage_pct"] >= 80:
|
|
68
|
+
summary, usage = await self.session.context_manager.compress_old_messages(
|
|
69
|
+
self.session.chat_compactor
|
|
70
|
+
)
|
|
71
|
+
if summary and usage:
|
|
72
|
+
self.session.context_manager.set_latest_usage(usage)
|
|
73
|
+
self.session.context_manager.add_usage(usage)
|
|
74
|
+
|
|
75
|
+
tool_schemas = (
|
|
76
|
+
[]
|
|
77
|
+
if force_plan_response
|
|
78
|
+
else self.session.tool_registry.get_schemas(mode=self.session.mode)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# LLM call with circuit breaker + fallback chain
|
|
82
|
+
response_text = ""
|
|
83
|
+
tool_calls: list[ToolCall] = []
|
|
84
|
+
usage: TokenUsage | None = None
|
|
85
|
+
llm_success = False
|
|
86
|
+
selected_model = model_chain[0]
|
|
87
|
+
|
|
88
|
+
for model_idx, model_name in enumerate(model_chain):
|
|
89
|
+
if circuit_breaker.is_open(model_name):
|
|
90
|
+
yield AgentEvent.text_delta(
|
|
91
|
+
f"\n[Skipping {model_name} (circuit open)]"
|
|
92
|
+
)
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
for attempt in range(max_llm_retries + 1):
|
|
96
|
+
response_text = ""
|
|
97
|
+
tool_calls = []
|
|
98
|
+
usage = None
|
|
99
|
+
saw_error = False
|
|
100
|
+
|
|
101
|
+
async for event in self.session.client.chat_completion(
|
|
102
|
+
self.session.context_manager.get_messages(),
|
|
103
|
+
tools=tool_schemas if tool_schemas else None,
|
|
104
|
+
model=model_name,
|
|
105
|
+
max_retries=0,
|
|
106
|
+
):
|
|
107
|
+
if event.type == StreamEventType.TEXT_DELTA:
|
|
108
|
+
if event.text_delta:
|
|
109
|
+
content = event.text_delta.content
|
|
110
|
+
response_text += content
|
|
111
|
+
yield AgentEvent.text_delta(content)
|
|
112
|
+
elif event.type == StreamEventType.TOOL_CALL_COMPLETE:
|
|
113
|
+
if event.tool_call:
|
|
114
|
+
tool_calls.append(event.tool_call)
|
|
115
|
+
elif event.type == StreamEventType.ERROR:
|
|
116
|
+
circuit_breaker.record_failure(model_name)
|
|
117
|
+
if attempt < max_llm_retries and circuit_breaker.can_try(model_name):
|
|
118
|
+
wait = 2 ** attempt + random.uniform(0, 1)
|
|
119
|
+
err_msg = event.error or "unknown error"
|
|
120
|
+
yield AgentEvent.text_delta(
|
|
121
|
+
f"\n[{model_name} error: {err_msg}, retrying in {wait:.1f}s...]"
|
|
122
|
+
)
|
|
123
|
+
await asyncio.sleep(wait)
|
|
124
|
+
saw_error = True
|
|
125
|
+
break
|
|
126
|
+
elif attempt < max_llm_retries:
|
|
127
|
+
yield AgentEvent.text_delta(
|
|
128
|
+
f"\n[{model_name} circuit open after {circuit_breaker.failure_threshold} failures, trying fallback...]"
|
|
129
|
+
)
|
|
130
|
+
saw_error = True
|
|
131
|
+
break
|
|
132
|
+
else:
|
|
133
|
+
yield AgentEvent.text_delta(
|
|
134
|
+
f"\n[{model_name} failed after {max_llm_retries + 1} attempts, trying fallback...]"
|
|
135
|
+
)
|
|
136
|
+
saw_error = True
|
|
137
|
+
break
|
|
138
|
+
elif event.type == StreamEventType.MESSAGE_COMPLETE:
|
|
139
|
+
usage = event.token_usage
|
|
140
|
+
|
|
141
|
+
if saw_error:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
circuit_breaker.record_success(model_name)
|
|
145
|
+
llm_success = True
|
|
146
|
+
selected_model = model_name
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if llm_success:
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
if not llm_success:
|
|
153
|
+
yield AgentEvent.agents_error(
|
|
154
|
+
f"All models exhausted. Tried: {', '.join(model_chain)}. "
|
|
155
|
+
"Check API keys and network connectivity."
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if selected_model != model_chain[0]:
|
|
160
|
+
yield AgentEvent.text_delta(
|
|
161
|
+
f"\n[Failed over to {selected_model}]\n"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self.session.context_manager.add_assistant_message(
|
|
165
|
+
response_text or None,
|
|
166
|
+
(
|
|
167
|
+
[
|
|
168
|
+
{
|
|
169
|
+
"id": tc.call_id,
|
|
170
|
+
"type": "function",
|
|
171
|
+
"function": {
|
|
172
|
+
"name": tc.name,
|
|
173
|
+
"arguments": json.dumps(tc.arguments),
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
for tc in tool_calls
|
|
177
|
+
]
|
|
178
|
+
if tool_calls
|
|
179
|
+
else None
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
yield AgentEvent.text_complete(response_text)
|
|
184
|
+
if response_text:
|
|
185
|
+
self.session.loop_detector.record_action("response", text=response_text)
|
|
186
|
+
|
|
187
|
+
if not tool_calls:
|
|
188
|
+
if usage:
|
|
189
|
+
self.session.context_manager.set_latest_usage(usage)
|
|
190
|
+
self.session.context_manager.add_usage(usage)
|
|
191
|
+
|
|
192
|
+
self.session.context_manager.prune_tool_outputs()
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
tool_call_results: list[ToolResultMessage] = []
|
|
196
|
+
|
|
197
|
+
for tool_call in tool_calls:
|
|
198
|
+
display_arguments = self._display_tool_arguments(tool_call.arguments)
|
|
199
|
+
yield AgentEvent.tool_call_start(
|
|
200
|
+
tool_call.call_id,
|
|
201
|
+
tool_call.name,
|
|
202
|
+
display_arguments,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
skip_tool_reason: str | None = None
|
|
206
|
+
self.session.loop_detector.record_action(
|
|
207
|
+
"tool_call",
|
|
208
|
+
tool_name=tool_call.name,
|
|
209
|
+
args=tool_call.arguments,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if self.session.mode == AgentMode.PLAN:
|
|
213
|
+
plan_tool_calls += 1
|
|
214
|
+
if plan_tool_calls > plan_tool_budget:
|
|
215
|
+
skip_tool_reason = (
|
|
216
|
+
f"Plan mode read-only exploration limit reached "
|
|
217
|
+
f"({plan_tool_budget} tool call(s))."
|
|
218
|
+
)
|
|
219
|
+
elif loop_detection_error := self.session.loop_detector.check_for_loop():
|
|
220
|
+
skip_tool_reason = (
|
|
221
|
+
f"Plan mode stopped repeated tool exploration: "
|
|
222
|
+
f"{loop_detection_error}."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if skip_tool_reason:
|
|
226
|
+
result = ToolResult.error_result(
|
|
227
|
+
f"{skip_tool_reason} Stop calling tools and provide the plan now."
|
|
228
|
+
)
|
|
229
|
+
force_plan_response = True
|
|
230
|
+
else:
|
|
231
|
+
try:
|
|
232
|
+
result = await self.session.tool_registry.invoke(
|
|
233
|
+
tool_call.name,
|
|
234
|
+
tool_call.arguments,
|
|
235
|
+
self.config.cwd,
|
|
236
|
+
self.session.hook_system,
|
|
237
|
+
self.session.approval_manager,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.warning(
|
|
241
|
+
"Tool '%s' crashed: %s",
|
|
242
|
+
tool_call.name,
|
|
243
|
+
e,
|
|
244
|
+
)
|
|
245
|
+
yield AgentEvent.text_delta(
|
|
246
|
+
f"\n[Tool '{tool_call.name}' crashed: {e}]"
|
|
247
|
+
)
|
|
248
|
+
result = ToolResult.error_result(f"Tool crashed: {e}")
|
|
249
|
+
|
|
250
|
+
if skip_tool_reason:
|
|
251
|
+
yield AgentEvent.text_delta(
|
|
252
|
+
f"\n[{skip_tool_reason} Preparing a plan now.]"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
yield AgentEvent.tool_call_complete(
|
|
256
|
+
tool_call.call_id,
|
|
257
|
+
tool_call.name,
|
|
258
|
+
result,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
tool_call_results.append(
|
|
262
|
+
ToolResultMessage(
|
|
263
|
+
tool_call_id=tool_call.call_id,
|
|
264
|
+
content=result.to_model_output(),
|
|
265
|
+
is_error=not result.success,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
for tool_result in tool_call_results:
|
|
270
|
+
self.session.context_manager.add_tool_result(
|
|
271
|
+
tool_result.tool_call_id,
|
|
272
|
+
tool_result.content,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if force_plan_response and self.session.mode == AgentMode.PLAN:
|
|
276
|
+
self.session.context_manager.add_user_message(
|
|
277
|
+
"SYSTEM NOTICE: Plan mode has enough context or is repeating tool exploration. "
|
|
278
|
+
"Do not call more tools. Produce the final plan now, with goal, approach, steps, "
|
|
279
|
+
"files to change, open questions, and the reminder to switch to /build for implementation."
|
|
280
|
+
)
|
|
281
|
+
self.session.loop_detector.clear()
|
|
282
|
+
self.session.context_manager.prune_tool_outputs()
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if usage:
|
|
286
|
+
self.session.context_manager.set_latest_usage(usage)
|
|
287
|
+
self.session.context_manager.add_usage(usage)
|
|
288
|
+
|
|
289
|
+
loop_detection_error = self.session.loop_detector.check_for_loop()
|
|
290
|
+
if loop_detection_error:
|
|
291
|
+
loop_prompt = create_loop_breaker_prompt(loop_detection_error)
|
|
292
|
+
self.session.context_manager.add_user_message(loop_prompt)
|
|
293
|
+
self.session.loop_detector.clear()
|
|
294
|
+
self.session.context_manager.prune_tool_outputs()
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
self.session.context_manager.prune_tool_outputs()
|
|
298
|
+
|
|
299
|
+
yield AgentEvent.agents_error(f"Maximum turns ({max_turns}) reached")
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.exception("Unhandled exception in agent loop")
|
|
303
|
+
try:
|
|
304
|
+
self.session.save_checkpoint(mode="crash")
|
|
305
|
+
except Exception:
|
|
306
|
+
logger.warning("Failed to save crash checkpoint")
|
|
307
|
+
yield AgentEvent.agents_error(
|
|
308
|
+
f"Internal agent error: {str(e)}",
|
|
309
|
+
details={"turn": self.session._turn_count},
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
def _display_tool_arguments(self, arguments: dict) -> dict:
|
|
314
|
+
if not self.config.redaction_enabled:
|
|
315
|
+
return arguments
|
|
316
|
+
redacted, _ = redact_tool_params(arguments)
|
|
317
|
+
return redacted
|
|
318
|
+
|
|
319
|
+
async def __aenter__(self) -> Agent:
|
|
320
|
+
await self.session.initialize()
|
|
321
|
+
return self
|
|
322
|
+
|
|
323
|
+
async def __aexit__(
|
|
324
|
+
self,
|
|
325
|
+
exc_type,
|
|
326
|
+
exc_val,
|
|
327
|
+
exc_tb,
|
|
328
|
+
) -> None:
|
|
329
|
+
if self.session and self.session.client and self.session.mcp_manager:
|
|
330
|
+
await self.session.client.close()
|
|
331
|
+
await self.session.mcp_manager.shutdown()
|
|
332
|
+
self.session = None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agentforge_harness.client.response import TokenUsage
|
|
8
|
+
from agentforge_harness.tools.base import ToolResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentEventType(str, Enum):
|
|
12
|
+
# Agent lifecycle events
|
|
13
|
+
AGENT_START = "agent_start"
|
|
14
|
+
AGENT_END = "agent_end"
|
|
15
|
+
AGENT_ERROR = "agent_error"
|
|
16
|
+
|
|
17
|
+
# Tool Calls
|
|
18
|
+
TOOL_CALL_START = "tool_call_start"
|
|
19
|
+
TOOL_CALL_COMPLETE = "tool_call_complete"
|
|
20
|
+
|
|
21
|
+
# Text Streaming Events
|
|
22
|
+
TEXT_DELTA = "text_delta"
|
|
23
|
+
TEXT_COMPLETE = "text_complete"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AgentEvent:
|
|
28
|
+
type: AgentEventType
|
|
29
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def agents_start(cls, message: str) -> AgentEvent:
|
|
33
|
+
return cls(
|
|
34
|
+
type=AgentEventType.AGENT_START,
|
|
35
|
+
data={"message": message},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def agents_end(
|
|
40
|
+
cls, response: str | None = None, usage: TokenUsage | None = None
|
|
41
|
+
) -> AgentEvent:
|
|
42
|
+
return cls(
|
|
43
|
+
type=AgentEventType.AGENT_END,
|
|
44
|
+
data={
|
|
45
|
+
"response": response,
|
|
46
|
+
"usage": usage.__dict__ if usage else None,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def agents_error(
|
|
52
|
+
cls, error: str, details: dict[str, Any] | None = None
|
|
53
|
+
) -> AgentEvent:
|
|
54
|
+
return cls(
|
|
55
|
+
type=AgentEventType.AGENT_ERROR,
|
|
56
|
+
data={
|
|
57
|
+
"error": error,
|
|
58
|
+
"details": details or {},
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def text_delta(cls, content: str) -> AgentEvent:
|
|
64
|
+
return cls(
|
|
65
|
+
type=AgentEventType.TEXT_DELTA,
|
|
66
|
+
data={"content": content},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def text_complete(cls, content: str) -> AgentEvent:
|
|
71
|
+
return cls(
|
|
72
|
+
type=AgentEventType.TEXT_COMPLETE,
|
|
73
|
+
data={"content": content},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def tool_call_start(
|
|
78
|
+
cls, call_id: str, name: str, arguments: dict[str, Any]
|
|
79
|
+
) -> AgentEvent:
|
|
80
|
+
return cls(
|
|
81
|
+
type=AgentEventType.TOOL_CALL_START,
|
|
82
|
+
data={
|
|
83
|
+
"call_id": call_id,
|
|
84
|
+
"name": name,
|
|
85
|
+
"arguments": arguments,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def tool_call_complete(
|
|
91
|
+
cls, call_id: str, name: str, result: ToolResult
|
|
92
|
+
) -> AgentEvent:
|
|
93
|
+
return cls(
|
|
94
|
+
type=AgentEventType.TOOL_CALL_COMPLETE,
|
|
95
|
+
data={
|
|
96
|
+
"call_id": call_id,
|
|
97
|
+
"name": name,
|
|
98
|
+
"success": result.success,
|
|
99
|
+
"output": result.output,
|
|
100
|
+
"error": result.error,
|
|
101
|
+
"metadata": result.metadata,
|
|
102
|
+
"diff": result.diff_text,
|
|
103
|
+
"truncated": result.truncated,
|
|
104
|
+
"exit_code" : result.exit_code,
|
|
105
|
+
},
|
|
106
|
+
)
|