yycode 0.3.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.
- agent/__init__.py +33 -0
- agent/acp/__init__.py +2 -0
- agent/acp/approval_adapter.py +134 -0
- agent/acp/content_adapter.py +45 -0
- agent/acp/jsonrpc.py +92 -0
- agent/acp/server.py +197 -0
- agent/acp/session_manager.py +193 -0
- agent/acp/update_adapter.py +192 -0
- agent/app_paths.py +25 -0
- agent/approval.py +169 -0
- agent/cancellation.py +52 -0
- agent/change_snapshot.py +186 -0
- agent/context_compressor.py +116 -0
- agent/graph.py +137 -0
- agent/llm_retry.py +434 -0
- agent/logger.py +97 -0
- agent/lsp/__init__.py +13 -0
- agent/lsp/client.py +151 -0
- agent/lsp/manager.py +234 -0
- agent/lsp/types.py +119 -0
- agent/message_context_manager.py +322 -0
- agent/message_format.py +105 -0
- agent/nodes/llm_node.py +58 -0
- agent/nodes/state.py +12 -0
- agent/nodes/task_guard_node.py +50 -0
- agent/nodes/tools_node.py +70 -0
- agent/plan_snapshot.py +70 -0
- agent/providers/__init__.py +13 -0
- agent/providers/anthropic_provider.py +268 -0
- agent/providers/base.py +52 -0
- agent/providers/openai_provider.py +279 -0
- agent/providers/text_tool_calls.py +118 -0
- agent/runtime/approval_service.py +184 -0
- agent/runtime/context.py +43 -0
- agent/runtime/tool_events.py +368 -0
- agent/runtime/tool_executor.py +208 -0
- agent/runtime/tool_output.py +261 -0
- agent/runtime/tool_registry.py +91 -0
- agent/runtime/tool_scheduler.py +35 -0
- agent/runtime/workflow_guard.py +217 -0
- agent/runtime/workspace.py +5 -0
- agent/runtime/workspace_tools.py +22 -0
- agent/session.py +787 -0
- agent/session_replay.py +95 -0
- agent/session_store.py +186 -0
- agent/skills.py +254 -0
- agent/streaming.py +248 -0
- agent/subagent.py +634 -0
- agent/task_memory.py +340 -0
- agent/todo_manager.py +304 -0
- agent/tool_retry.py +106 -0
- agent/tui/__init__.py +14 -0
- agent/tui/app.py +1325 -0
- agent/tui/approval.py +53 -0
- agent/tui/commands/__init__.py +6 -0
- agent/tui/commands/base.py +48 -0
- agent/tui/commands/clear.py +37 -0
- agent/tui/commands/help.py +27 -0
- agent/tui/commands/registry.py +94 -0
- agent/tui/help_content.py +108 -0
- agent/tui/renderers.py +1961 -0
- agent/tui/runner.py +439 -0
- agent/tui/state.py +653 -0
- main.py +465 -0
- tools/__init__.py +50 -0
- tools/apply_patch.py +305 -0
- tools/bash.py +76 -0
- tools/diff_utils.py +139 -0
- tools/edit_file.py +40 -0
- tools/git_diff.py +72 -0
- tools/git_show.py +65 -0
- tools/grep.py +149 -0
- tools/list_files.py +90 -0
- tools/list_skills.py +24 -0
- tools/load_skill.py +30 -0
- tools/lsp_definition.py +27 -0
- tools/lsp_diagnostics.py +32 -0
- tools/lsp_document_symbols.py +23 -0
- tools/lsp_hover.py +29 -0
- tools/lsp_references.py +37 -0
- tools/lsp_utils.py +38 -0
- tools/lsp_workspace_symbols.py +23 -0
- tools/read_file.py +61 -0
- tools/read_many_files.py +50 -0
- tools/safety.py +50 -0
- tools/subagent.py +57 -0
- tools/todo.py +89 -0
- tools/verify.py +107 -0
- tools/web_search.py +250 -0
- tools/workspace.py +36 -0
- tools/workspace_state.py +60 -0
- tools/write_file.py +88 -0
- utils/__init__.py +5 -0
- utils/retry.py +13 -0
- yycode-0.3.2.data/data/skills/code_review.md +61 -0
- yycode-0.3.2.data/data/skills/code_workflow.md +404 -0
- yycode-0.3.2.data/data/skills/drawio/SKILL.md +636 -0
- yycode-0.3.2.data/data/skills/drawio/agents/openai.yaml +19 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-erd.drawio +84 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.drawio +91 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.drawio +112 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ml.drawio +90 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.drawio +68 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.drawio +86 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-sequence.drawio +116 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.drawio +66 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star.drawio +79 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-star.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/demo-uml-class.drawio +64 -0
- yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.drawio +173 -0
- yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.drawio +120 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow.drawio +120 -0
- yycode-0.3.2.data/data/skills/drawio/assets/workflow.png +0 -0
- yycode-0.3.2.data/data/skills/drawio/docs/index.html +469 -0
- yycode-0.3.2.data/data/skills/drawio/docs/zh.html +456 -0
- yycode-0.3.2.data/data/skills/drawio/references/style-extraction.md +254 -0
- yycode-0.3.2.data/data/skills/drawio/styles/schema.json +112 -0
- yycode-0.3.2.data/data/skills/plan.md +115 -0
- yycode-0.3.2.data/data/skills/ppt/SKILL.md +254 -0
- yycode-0.3.2.dist-info/METADATA +12 -0
- yycode-0.3.2.dist-info/RECORD +131 -0
- yycode-0.3.2.dist-info/WHEEL +5 -0
- yycode-0.3.2.dist-info/entry_points.txt +2 -0
- yycode-0.3.2.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Formatting helpers for tool stream events."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MAX_TOOL_RESULT_PREVIEW_CHARS = 12_000
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_tool_description(tc) -> str:
|
|
10
|
+
"""Format a tool call for display."""
|
|
11
|
+
tool_name = tc.name
|
|
12
|
+
args = tc.args or {}
|
|
13
|
+
if tool_name == "bash":
|
|
14
|
+
cmd = args.get("command", "")
|
|
15
|
+
cmd_preview = cmd[:40] + "..." if len(cmd) > 40 else cmd
|
|
16
|
+
return f"{tool_name}: {cmd_preview}"
|
|
17
|
+
if tool_name in {"read_file", "write_file", "edit_file"}:
|
|
18
|
+
path = args.get("path", "")
|
|
19
|
+
return f"{tool_name}: {path}"
|
|
20
|
+
if tool_name == "todo":
|
|
21
|
+
items = args.get("items", [])
|
|
22
|
+
return f"{tool_name}: {len(items)} item(s)"
|
|
23
|
+
if tool_name == "subagent":
|
|
24
|
+
role = args.get("role", "")
|
|
25
|
+
task = args.get("task", "")
|
|
26
|
+
if role and task:
|
|
27
|
+
task_preview = task[:30] + "..." if len(task) > 30 else task
|
|
28
|
+
return f"{tool_name} @{role}: {task_preview}"
|
|
29
|
+
return tool_name
|
|
30
|
+
return tool_name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_tool_event_metadata(tc) -> dict:
|
|
34
|
+
"""Return timeline-friendly metadata for a tool call."""
|
|
35
|
+
tool_name = tc.name
|
|
36
|
+
args = tc.args or {}
|
|
37
|
+
metadata = {"args": args}
|
|
38
|
+
|
|
39
|
+
if tool_name == "read_file":
|
|
40
|
+
path = args.get("path", "")
|
|
41
|
+
start_line = args.get("start_line")
|
|
42
|
+
end_line = args.get("end_line")
|
|
43
|
+
if start_line or end_line:
|
|
44
|
+
detail = f"{path} lines {start_line or 1}-{end_line or 'end'}"
|
|
45
|
+
title = "Read file range"
|
|
46
|
+
else:
|
|
47
|
+
detail = path
|
|
48
|
+
title = "Read file"
|
|
49
|
+
return {
|
|
50
|
+
"title": title,
|
|
51
|
+
"detail": detail,
|
|
52
|
+
"phase": "exploring",
|
|
53
|
+
"tool_name": tool_name,
|
|
54
|
+
"file_paths": [path] if path else None,
|
|
55
|
+
"metadata": metadata,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if tool_name == "read_many_files":
|
|
59
|
+
paths = args.get("paths") or []
|
|
60
|
+
return {
|
|
61
|
+
"title": "Read files",
|
|
62
|
+
"detail": ", ".join(paths[:3]) + ("..." if len(paths) > 3 else ""),
|
|
63
|
+
"phase": "exploring",
|
|
64
|
+
"tool_name": tool_name,
|
|
65
|
+
"file_paths": paths,
|
|
66
|
+
"metadata": metadata,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if tool_name == "grep":
|
|
70
|
+
pattern = args.get("pattern", "")
|
|
71
|
+
path = args.get("path", ".")
|
|
72
|
+
search_metadata = _grep_search_metadata(pattern, path)
|
|
73
|
+
metadata.update(search_metadata)
|
|
74
|
+
return {
|
|
75
|
+
"title": "Search code",
|
|
76
|
+
"detail": search_metadata["search_display"],
|
|
77
|
+
"phase": "exploring",
|
|
78
|
+
"tool_name": tool_name,
|
|
79
|
+
"file_paths": [path],
|
|
80
|
+
"metadata": metadata,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if tool_name in {"list_files", "git_show", "workspace_state", "git_diff"}:
|
|
84
|
+
return {
|
|
85
|
+
"title": _title_for_readonly_tool(tool_name),
|
|
86
|
+
"detail": _detail_for_path_args(args),
|
|
87
|
+
"phase": "exploring",
|
|
88
|
+
"tool_name": tool_name,
|
|
89
|
+
"file_paths": _file_paths_from_args(args),
|
|
90
|
+
"metadata": metadata,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if tool_name.startswith("lsp_"):
|
|
94
|
+
return {
|
|
95
|
+
"title": _title_for_lsp_tool(tool_name),
|
|
96
|
+
"detail": _detail_for_lsp_args(args),
|
|
97
|
+
"phase": "semantic_navigation",
|
|
98
|
+
"tool_name": tool_name,
|
|
99
|
+
"file_paths": _file_paths_from_args(args),
|
|
100
|
+
"metadata": metadata,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if tool_name in {"apply_patch", "write_file", "edit_file"}:
|
|
104
|
+
return {
|
|
105
|
+
"title": _title_for_write_tool(tool_name),
|
|
106
|
+
"detail": _detail_for_path_args(args),
|
|
107
|
+
"phase": "implementing",
|
|
108
|
+
"tool_name": tool_name,
|
|
109
|
+
"file_paths": _file_paths_from_args(args),
|
|
110
|
+
"metadata": metadata,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if tool_name == "verify":
|
|
114
|
+
target = args.get("target") or args.get("command") or args.get("kind", "")
|
|
115
|
+
return {
|
|
116
|
+
"title": "Run verification",
|
|
117
|
+
"detail": str(target),
|
|
118
|
+
"phase": "verifying",
|
|
119
|
+
"tool_name": tool_name,
|
|
120
|
+
"metadata": metadata,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if tool_name == "todo":
|
|
124
|
+
items = args.get("items") or []
|
|
125
|
+
active = next((item for item in items if item.get("status") == "in_progress"), None)
|
|
126
|
+
return {
|
|
127
|
+
"title": "Update task plan",
|
|
128
|
+
"detail": active.get("text", "") if active else f"{len(items)} item(s)",
|
|
129
|
+
"phase": "planning",
|
|
130
|
+
"tool_name": tool_name,
|
|
131
|
+
"metadata": metadata,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if tool_name == "subagent":
|
|
135
|
+
role = args.get("role", "subagent")
|
|
136
|
+
task = args.get("task", "")
|
|
137
|
+
return {
|
|
138
|
+
"title": f"Start {role} subagent",
|
|
139
|
+
"detail": task,
|
|
140
|
+
"phase": "implementing" if role == "worker" else "exploring",
|
|
141
|
+
"tool_name": tool_name,
|
|
142
|
+
"metadata": metadata,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if tool_name == "bash":
|
|
146
|
+
command = args.get("command", "")
|
|
147
|
+
return {
|
|
148
|
+
"title": _title_for_bash(command),
|
|
149
|
+
"detail": command,
|
|
150
|
+
"phase": _phase_for_bash(command),
|
|
151
|
+
"tool_name": tool_name,
|
|
152
|
+
"metadata": {"command": command, "args": args},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"title": f"Run {tool_name}",
|
|
157
|
+
"detail": format_tool_description(tc),
|
|
158
|
+
"tool_name": tool_name,
|
|
159
|
+
"metadata": metadata,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def file_paths_for_tool_call(tc) -> list[str]:
|
|
164
|
+
"""Return workspace paths referenced by a tool call when obvious."""
|
|
165
|
+
return _file_paths_from_args(tc.args or {}) or []
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def diff_preview_from_output(output: str) -> str:
|
|
169
|
+
"""Extract a bounded diff preview from a tool output string."""
|
|
170
|
+
marker = "\ndiff:\n"
|
|
171
|
+
if marker in output:
|
|
172
|
+
preview = output.split(marker, 1)[1]
|
|
173
|
+
elif output.startswith("diff:\n"):
|
|
174
|
+
preview = output[len("diff:\n") :]
|
|
175
|
+
else:
|
|
176
|
+
preview = output
|
|
177
|
+
if len(preview) > MAX_TOOL_RESULT_PREVIEW_CHARS:
|
|
178
|
+
return preview[:MAX_TOOL_RESULT_PREVIEW_CHARS] + (
|
|
179
|
+
f"\n... diff preview truncated to {MAX_TOOL_RESULT_PREVIEW_CHARS} chars"
|
|
180
|
+
)
|
|
181
|
+
return preview
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def tool_output_indicates_successful_write(output: str) -> bool:
|
|
185
|
+
"""Return whether a workspace write output looks successful."""
|
|
186
|
+
return not output.startswith(
|
|
187
|
+
(
|
|
188
|
+
"Error:",
|
|
189
|
+
"approval_required:",
|
|
190
|
+
"Code workflow guard blocked",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _title_for_readonly_tool(tool_name: str) -> str:
|
|
196
|
+
return {
|
|
197
|
+
"list_files": "List files",
|
|
198
|
+
"git_show": "Read file from git",
|
|
199
|
+
"workspace_state": "Check workspace state",
|
|
200
|
+
"git_diff": "Review git diff",
|
|
201
|
+
}.get(tool_name, f"Run {tool_name}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _title_for_lsp_tool(tool_name: str) -> str:
|
|
205
|
+
return {
|
|
206
|
+
"lsp_document_symbols": "LSP document symbols",
|
|
207
|
+
"lsp_workspace_symbols": "LSP workspace symbols",
|
|
208
|
+
"lsp_definition": "LSP definition",
|
|
209
|
+
"lsp_references": "LSP references",
|
|
210
|
+
"lsp_hover": "LSP hover",
|
|
211
|
+
"lsp_diagnostics": "LSP diagnostics",
|
|
212
|
+
}.get(tool_name, f"LSP {tool_name.removeprefix('lsp_').replace('_', ' ')}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _detail_for_lsp_args(args: dict) -> str:
|
|
216
|
+
path = args.get("path")
|
|
217
|
+
query = args.get("query")
|
|
218
|
+
line = args.get("line")
|
|
219
|
+
character = args.get("character")
|
|
220
|
+
parts: list[str] = []
|
|
221
|
+
if path:
|
|
222
|
+
parts.append(str(path))
|
|
223
|
+
if query:
|
|
224
|
+
parts.append(f"query={query}")
|
|
225
|
+
if line is not None and character is not None:
|
|
226
|
+
parts.append(f"position={line}:{character}")
|
|
227
|
+
return " · ".join(parts)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _title_for_write_tool(tool_name: str) -> str:
|
|
231
|
+
return {
|
|
232
|
+
"apply_patch": "Apply patch",
|
|
233
|
+
"write_file": "Create file",
|
|
234
|
+
"edit_file": "Edit file",
|
|
235
|
+
}.get(tool_name, f"Run {tool_name}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _detail_for_path_args(args: dict) -> str:
|
|
239
|
+
paths = _file_paths_from_args(args)
|
|
240
|
+
if paths:
|
|
241
|
+
return ", ".join(paths)
|
|
242
|
+
command = args.get("command")
|
|
243
|
+
return str(command or "")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _grep_search_metadata(pattern: object, path: object) -> dict:
|
|
247
|
+
pattern_text = str(pattern or "")
|
|
248
|
+
path_text = str(path or ".")
|
|
249
|
+
display_path = "workspace" if path_text == "." else path_text
|
|
250
|
+
terms = _grep_search_terms(pattern_text)
|
|
251
|
+
term_count = len(terms)
|
|
252
|
+
keyword_label = _keyword_label(term_count)
|
|
253
|
+
return {
|
|
254
|
+
"search_display": f"Searching {display_path} · {keyword_label}",
|
|
255
|
+
"pattern_preview": _truncate(pattern_text, 80),
|
|
256
|
+
"search_terms": terms[:5],
|
|
257
|
+
"term_count": term_count,
|
|
258
|
+
"path": display_path,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _grep_search_terms(pattern: str) -> list[str]:
|
|
263
|
+
if not pattern:
|
|
264
|
+
return []
|
|
265
|
+
raw_parts = pattern.split("|") if "|" in pattern else [pattern]
|
|
266
|
+
terms: list[str] = []
|
|
267
|
+
seen: set[str] = set()
|
|
268
|
+
for raw in raw_parts:
|
|
269
|
+
term = _clean_regex_term(raw)
|
|
270
|
+
if not term or term in seen:
|
|
271
|
+
continue
|
|
272
|
+
seen.add(term)
|
|
273
|
+
terms.append(term)
|
|
274
|
+
if not terms and pattern.strip():
|
|
275
|
+
return ["regex query"]
|
|
276
|
+
return terms
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _clean_regex_term(raw: str) -> str:
|
|
280
|
+
term = raw.strip()
|
|
281
|
+
term = term.strip("()")
|
|
282
|
+
term = term.replace("\\_", "_").replace("\\-", "-").replace("\\.", ".")
|
|
283
|
+
term = term.replace("\\/", "/").replace("\\:", ":")
|
|
284
|
+
term = re.sub(r"\\b|\\B|\\A|\\Z", "", term)
|
|
285
|
+
if re.search(r"[\[\]\{\}\+\*\?\^\$]", term):
|
|
286
|
+
return "regex query"
|
|
287
|
+
term = re.sub(r"\\(.)", r"\1", term)
|
|
288
|
+
term = term.strip()
|
|
289
|
+
return _truncate(term, 48) if term else ""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _keyword_label(count: int) -> str:
|
|
293
|
+
if count <= 0:
|
|
294
|
+
return "regex query"
|
|
295
|
+
if count == 1:
|
|
296
|
+
return "1 keyword"
|
|
297
|
+
return f"{count} keywords"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _truncate(text: str, limit: int) -> str:
|
|
301
|
+
if len(text) <= limit:
|
|
302
|
+
return text
|
|
303
|
+
return text[: max(0, limit - 3)] + "..."
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _file_paths_from_args(args: dict) -> list[str]:
|
|
307
|
+
paths = args.get("paths")
|
|
308
|
+
if isinstance(paths, list):
|
|
309
|
+
return [str(path) for path in paths]
|
|
310
|
+
path = args.get("path")
|
|
311
|
+
if path:
|
|
312
|
+
return [str(path)]
|
|
313
|
+
patch = args.get("patch")
|
|
314
|
+
if patch:
|
|
315
|
+
return _paths_from_unified_diff(str(patch))
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _title_for_bash(command: str) -> str:
|
|
320
|
+
if _is_drawio_command(command):
|
|
321
|
+
if "--version" in command:
|
|
322
|
+
return "Check draw.io CLI"
|
|
323
|
+
if " -x " in f" {command} " or " --export " in f" {command} ":
|
|
324
|
+
return "Export draw.io diagram"
|
|
325
|
+
return "Run draw.io command"
|
|
326
|
+
if command.startswith(("pytest", "ruff", "mypy")):
|
|
327
|
+
return "Run verification"
|
|
328
|
+
if command.startswith("git status"):
|
|
329
|
+
return "Check workspace state"
|
|
330
|
+
if command.startswith("git diff"):
|
|
331
|
+
return "Review git diff"
|
|
332
|
+
if command.startswith(("sed ", "grep ", "rg ")):
|
|
333
|
+
return "Inspect workspace"
|
|
334
|
+
return "Run command"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _phase_for_bash(command: str) -> str:
|
|
338
|
+
if _is_drawio_command(command):
|
|
339
|
+
return "verifying" if "--version" in command else "implementing"
|
|
340
|
+
if command.startswith(("pytest", "ruff", "mypy")):
|
|
341
|
+
return "verifying"
|
|
342
|
+
return "exploring"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _is_drawio_command(command: str) -> bool:
|
|
346
|
+
return any(token in command for token in ("draw.io", "drawio"))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _paths_from_unified_diff(patch: str) -> list[str]:
|
|
350
|
+
paths: list[str] = []
|
|
351
|
+
for line in patch.splitlines():
|
|
352
|
+
path = None
|
|
353
|
+
begin_patch_match = re.match(r"\*\*\* (?:Add|Update|Delete) File: (.+)$", line)
|
|
354
|
+
if begin_patch_match:
|
|
355
|
+
path = begin_patch_match.group(1).strip()
|
|
356
|
+
elif line.startswith("*** Move to: "):
|
|
357
|
+
path = line[len("*** Move to: "):].strip()
|
|
358
|
+
elif line.startswith("diff --git "):
|
|
359
|
+
match = re.match(r"diff --git a/(.+?) b/(.+)$", line)
|
|
360
|
+
if match:
|
|
361
|
+
path = match.group(2)
|
|
362
|
+
elif line.startswith("+++ "):
|
|
363
|
+
raw = line[4:].split("\t", 1)[0].strip()
|
|
364
|
+
if raw != "/dev/null":
|
|
365
|
+
path = raw[2:] if raw.startswith("b/") else raw
|
|
366
|
+
if path and path not in paths:
|
|
367
|
+
paths.append(path)
|
|
368
|
+
return paths
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Single tool call execution pipeline."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from langchain_core.messages import ToolMessage
|
|
6
|
+
|
|
7
|
+
from agent.approval import ApprovalDenied, ApprovalTargetMissing
|
|
8
|
+
from agent.logger import get_logger
|
|
9
|
+
from agent.runtime.approval_service import ApprovalService
|
|
10
|
+
from agent.runtime.context import AgentRuntimeContext
|
|
11
|
+
from agent.runtime.tool_events import (
|
|
12
|
+
diff_preview_from_output,
|
|
13
|
+
file_paths_for_tool_call,
|
|
14
|
+
format_tool_description,
|
|
15
|
+
format_tool_event_metadata,
|
|
16
|
+
)
|
|
17
|
+
from agent.runtime.tool_output import build_tool_output_view, compact_preflight_output
|
|
18
|
+
from agent.runtime.tool_registry import RuntimeToolRegistry
|
|
19
|
+
from agent.runtime.workflow_guard import WorkflowGuard
|
|
20
|
+
from agent.streaming import StreamEvent
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolExecutor:
|
|
26
|
+
"""Execute one tool call with guardrails, approval, and stream events."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
runtime: AgentRuntimeContext,
|
|
31
|
+
registry: RuntimeToolRegistry,
|
|
32
|
+
workflow_guard: WorkflowGuard,
|
|
33
|
+
approval_service: ApprovalService,
|
|
34
|
+
):
|
|
35
|
+
self.runtime = runtime
|
|
36
|
+
self.registry = registry
|
|
37
|
+
self.workflow_guard = workflow_guard
|
|
38
|
+
self.approval_service = approval_service
|
|
39
|
+
|
|
40
|
+
async def execute(self, tc) -> ToolMessage:
|
|
41
|
+
"""Execute a tool call and return a ToolMessage."""
|
|
42
|
+
start_time = time.perf_counter()
|
|
43
|
+
status = "completed"
|
|
44
|
+
await self._emit_tool_start(tc)
|
|
45
|
+
try:
|
|
46
|
+
logger.debug(f"Calling tool: {getattr(tc, 'name', 'unknown')}")
|
|
47
|
+
logger.debug(f"Full tc object: {tc!r}")
|
|
48
|
+
logger.debug(f"tc type: {type(tc)}")
|
|
49
|
+
logger.debug(f"tc.name: {getattr(tc, 'name', 'N/A')!r}")
|
|
50
|
+
logger.debug(f"tc.args: {getattr(tc, 'args', 'N/A')!r}")
|
|
51
|
+
logger.debug(f"tc.id: {getattr(tc, 'id', 'N/A')!r}")
|
|
52
|
+
|
|
53
|
+
if self.registry.is_workspace_write(tc.name) and not self.workflow_guard.has_preflight():
|
|
54
|
+
output = await self.workflow_guard.run_preflight()
|
|
55
|
+
return self._tool_message(tc, compact_preflight_output(output))
|
|
56
|
+
|
|
57
|
+
if self.workflow_guard.should_require_apply_patch(tc):
|
|
58
|
+
return self._tool_message(
|
|
59
|
+
tc,
|
|
60
|
+
self.workflow_guard.apply_patch_required_message(tc),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
approved_args = await self.approval_service.approve(tc.name, tc.args or {})
|
|
65
|
+
except ApprovalTargetMissing as exc:
|
|
66
|
+
status = "failed"
|
|
67
|
+
await self._emit_tool_result(
|
|
68
|
+
str(exc),
|
|
69
|
+
title="File edit blocked",
|
|
70
|
+
detail="No target file detected",
|
|
71
|
+
phase="blocked",
|
|
72
|
+
)
|
|
73
|
+
return self._tool_message(tc, str(exc))
|
|
74
|
+
runner = self.registry.create_subagent_runner() if tc.name == "subagent" else None
|
|
75
|
+
handler = runner.run if runner else self.registry.resolve(tc.name)
|
|
76
|
+
output = await self.runtime.run_tool(
|
|
77
|
+
handler,
|
|
78
|
+
tc.name,
|
|
79
|
+
max_retries=3,
|
|
80
|
+
timeout_seconds=self.registry.timeout_for(tc.name),
|
|
81
|
+
**approved_args,
|
|
82
|
+
)
|
|
83
|
+
output_view = build_tool_output_view(tc.name, output, tc)
|
|
84
|
+
|
|
85
|
+
logger.debug(f"Tool output: {output[:200]}...")
|
|
86
|
+
logger.debug(f"End tool: {getattr(tc, 'name', 'unknown')}")
|
|
87
|
+
tool_message = self._tool_message(tc, output_view.model)
|
|
88
|
+
if output_view.context_policy != "full":
|
|
89
|
+
tool_message.additional_kwargs["context_policy"] = output_view.context_policy
|
|
90
|
+
if runner and runner.last_usage:
|
|
91
|
+
tool_message.additional_kwargs["usage"] = dict(runner.last_usage)
|
|
92
|
+
|
|
93
|
+
if tc.name == "todo":
|
|
94
|
+
await self._emit_tool_result(
|
|
95
|
+
output_view.display,
|
|
96
|
+
title="Task Plan",
|
|
97
|
+
detail="Updated todo items and task memory",
|
|
98
|
+
phase="planning",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
should_emit_diff = self.workflow_guard.update_after_tool(tc, output)
|
|
102
|
+
if should_emit_diff:
|
|
103
|
+
await self._emit_tool_result(diff_preview_from_output(output_view.display))
|
|
104
|
+
await self._emit_file_changed(tc)
|
|
105
|
+
return tool_message
|
|
106
|
+
except ApprovalDenied:
|
|
107
|
+
status = "failed"
|
|
108
|
+
raise
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
status = "failed"
|
|
111
|
+
output = f"Error executing tool {tc.name}: {exc}"
|
|
112
|
+
logger.exception("Tool execution failed: %s", getattr(tc, "name", "unknown"))
|
|
113
|
+
await self._emit_tool_result(
|
|
114
|
+
output,
|
|
115
|
+
title="Tool failed",
|
|
116
|
+
detail=tc.name,
|
|
117
|
+
phase="blocked",
|
|
118
|
+
)
|
|
119
|
+
return self._tool_message(tc, output)
|
|
120
|
+
finally:
|
|
121
|
+
elapsed_ms = int((time.perf_counter() - start_time) * 1000)
|
|
122
|
+
await self._emit_tool_end(tc, status, elapsed_ms)
|
|
123
|
+
|
|
124
|
+
def _tool_message(self, tc, output: str) -> ToolMessage:
|
|
125
|
+
return ToolMessage(
|
|
126
|
+
content=output,
|
|
127
|
+
tool_call_id=tc.id,
|
|
128
|
+
name=tc.name,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def _emit_tool_start(self, tc) -> None:
|
|
132
|
+
if not self.runtime.stream_callback:
|
|
133
|
+
return
|
|
134
|
+
await self.runtime.stream_callback(
|
|
135
|
+
StreamEvent(
|
|
136
|
+
source=self.runtime.source,
|
|
137
|
+
session_id=self.runtime.session_id,
|
|
138
|
+
role=self.runtime.role,
|
|
139
|
+
parent_session_id=self.runtime.parent_session_id,
|
|
140
|
+
event_type="tool_start",
|
|
141
|
+
content=format_tool_description(tc),
|
|
142
|
+
status="running",
|
|
143
|
+
**format_tool_event_metadata(tc),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def _emit_tool_end(self, tc, status: str, elapsed_ms: int) -> None:
|
|
148
|
+
if not self.runtime.stream_callback:
|
|
149
|
+
return
|
|
150
|
+
await self.runtime.stream_callback(
|
|
151
|
+
StreamEvent(
|
|
152
|
+
source=self.runtime.source,
|
|
153
|
+
session_id=self.runtime.session_id,
|
|
154
|
+
role=self.runtime.role,
|
|
155
|
+
parent_session_id=self.runtime.parent_session_id,
|
|
156
|
+
event_type="tool_end",
|
|
157
|
+
content=tc.name,
|
|
158
|
+
status=status,
|
|
159
|
+
elapsed_ms=elapsed_ms,
|
|
160
|
+
**format_tool_event_metadata(tc),
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def _emit_tool_result(
|
|
165
|
+
self,
|
|
166
|
+
content: str,
|
|
167
|
+
*,
|
|
168
|
+
title: str = "Review diff",
|
|
169
|
+
detail: str = "Workspace changes produced a diff preview",
|
|
170
|
+
phase: str = "reviewing",
|
|
171
|
+
) -> None:
|
|
172
|
+
if not self.runtime.stream_callback:
|
|
173
|
+
return
|
|
174
|
+
await self.runtime.stream_callback(
|
|
175
|
+
StreamEvent(
|
|
176
|
+
source=self.runtime.source,
|
|
177
|
+
session_id=self.runtime.session_id,
|
|
178
|
+
role=self.runtime.role,
|
|
179
|
+
parent_session_id=self.runtime.parent_session_id,
|
|
180
|
+
event_type="tool_result",
|
|
181
|
+
content=content,
|
|
182
|
+
title=title,
|
|
183
|
+
detail=detail,
|
|
184
|
+
phase=phase,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
async def _emit_file_changed(self, tc) -> None:
|
|
189
|
+
if not self.runtime.stream_callback:
|
|
190
|
+
return
|
|
191
|
+
file_paths = file_paths_for_tool_call(tc)
|
|
192
|
+
await self.runtime.stream_callback(
|
|
193
|
+
StreamEvent(
|
|
194
|
+
source=self.runtime.source,
|
|
195
|
+
session_id=self.runtime.session_id,
|
|
196
|
+
role=self.runtime.role,
|
|
197
|
+
parent_session_id=self.runtime.parent_session_id,
|
|
198
|
+
event_type="file_changed",
|
|
199
|
+
content=", ".join(file_paths),
|
|
200
|
+
title="File changed",
|
|
201
|
+
detail=", ".join(file_paths),
|
|
202
|
+
phase="implementing",
|
|
203
|
+
status="completed",
|
|
204
|
+
tool_name=tc.name,
|
|
205
|
+
file_paths=file_paths,
|
|
206
|
+
metadata={"tool_call_id": tc.id},
|
|
207
|
+
)
|
|
208
|
+
)
|