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
agent/change_snapshot.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Shared changed-files snapshot helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ChangedFile:
|
|
12
|
+
"""One changed file and its diff stats."""
|
|
13
|
+
|
|
14
|
+
path: str
|
|
15
|
+
added: int
|
|
16
|
+
removed: int
|
|
17
|
+
diff: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ChangedFilesSnapshot:
|
|
22
|
+
"""A stable changed-files snapshot for TUI/protocol adapters."""
|
|
23
|
+
|
|
24
|
+
files: list[ChangedFile] = field(default_factory=list)
|
|
25
|
+
total_added: int = 0
|
|
26
|
+
total_removed: int = 0
|
|
27
|
+
source: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_diff_text(content: str) -> str:
|
|
31
|
+
"""Return the unified diff portion from a tool result, if present."""
|
|
32
|
+
if not content:
|
|
33
|
+
return ""
|
|
34
|
+
lines = content.splitlines()
|
|
35
|
+
for index, line in enumerate(lines):
|
|
36
|
+
if line.startswith(("diff --git ", "--- ")):
|
|
37
|
+
return "\n".join(lines[index:])
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_changed_files_snapshot(diff: str, *, source: str = "diff") -> ChangedFilesSnapshot:
|
|
42
|
+
"""Return per-file stats and diff sections from a unified diff blob."""
|
|
43
|
+
sections: list[dict] = []
|
|
44
|
+
current: dict | None = None
|
|
45
|
+
for line in diff.splitlines():
|
|
46
|
+
if line.startswith("diff --git "):
|
|
47
|
+
if current is not None:
|
|
48
|
+
sections.append(current)
|
|
49
|
+
current = {"path": _path_from_diff_header(line), "added": 0, "removed": 0, "lines": [line]}
|
|
50
|
+
continue
|
|
51
|
+
if line.startswith("--- "):
|
|
52
|
+
if current is not None and _diff_section_has_changes(current):
|
|
53
|
+
sections.append(current)
|
|
54
|
+
current = {
|
|
55
|
+
"path": _strip_diff_prefix(line[4:].split("\t", 1)[0].strip()),
|
|
56
|
+
"added": 0,
|
|
57
|
+
"removed": 0,
|
|
58
|
+
"lines": [line],
|
|
59
|
+
}
|
|
60
|
+
elif current is None:
|
|
61
|
+
current = {
|
|
62
|
+
"path": _strip_diff_prefix(line[4:].split("\t", 1)[0].strip()),
|
|
63
|
+
"added": 0,
|
|
64
|
+
"removed": 0,
|
|
65
|
+
"lines": [line],
|
|
66
|
+
}
|
|
67
|
+
else:
|
|
68
|
+
current["lines"].append(line)
|
|
69
|
+
continue
|
|
70
|
+
if current is None:
|
|
71
|
+
continue
|
|
72
|
+
current["lines"].append(line)
|
|
73
|
+
if line.startswith("+++ "):
|
|
74
|
+
path = _strip_diff_prefix(line[4:].split("\t", 1)[0].strip())
|
|
75
|
+
if path != "/dev/null":
|
|
76
|
+
current["path"] = path
|
|
77
|
+
continue
|
|
78
|
+
if line.startswith("--- ") or line.startswith("@@") or line.startswith("index "):
|
|
79
|
+
continue
|
|
80
|
+
if line.startswith("+"):
|
|
81
|
+
current["added"] += 1
|
|
82
|
+
elif line.startswith("-"):
|
|
83
|
+
current["removed"] += 1
|
|
84
|
+
if current is not None:
|
|
85
|
+
sections.append(current)
|
|
86
|
+
files = _merge_changed_file_sections(sections)
|
|
87
|
+
return ChangedFilesSnapshot(
|
|
88
|
+
files=files,
|
|
89
|
+
total_added=sum(item.added for item in files),
|
|
90
|
+
total_removed=sum(item.removed for item in files),
|
|
91
|
+
source=source,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def changed_files_as_dicts(snapshot: ChangedFilesSnapshot) -> list[dict]:
|
|
96
|
+
"""Return snapshot files in the legacy dict shape used by TUI state."""
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
"path": file.path,
|
|
100
|
+
"added": file.added,
|
|
101
|
+
"removed": file.removed,
|
|
102
|
+
"diff": file.diff,
|
|
103
|
+
}
|
|
104
|
+
for file in snapshot.files
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def merge_changed_paths(items: Iterable[object]) -> list[str]:
|
|
109
|
+
"""Return unique changed paths from timeline-like items."""
|
|
110
|
+
paths: list[str] = []
|
|
111
|
+
for item in items:
|
|
112
|
+
event_type = getattr(item, "event_type", "")
|
|
113
|
+
tool_name = getattr(item, "tool_name", "")
|
|
114
|
+
if event_type == "file_changed":
|
|
115
|
+
candidates = list(getattr(item, "file_paths", []) or [])
|
|
116
|
+
elif event_type in {"tool_start", "tool_end"} and tool_name in {
|
|
117
|
+
"apply_patch",
|
|
118
|
+
"write_file",
|
|
119
|
+
"edit_file",
|
|
120
|
+
}:
|
|
121
|
+
candidates = list(getattr(item, "file_paths", []) or [])
|
|
122
|
+
else:
|
|
123
|
+
candidates = []
|
|
124
|
+
for path in candidates:
|
|
125
|
+
if path and path not in paths:
|
|
126
|
+
paths.append(path)
|
|
127
|
+
return paths
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def turn_had_successful_write(items: Iterable[object]) -> bool:
|
|
131
|
+
"""Return whether timeline-like items include a successful write tool."""
|
|
132
|
+
return any(
|
|
133
|
+
getattr(item, "event_type", "") == "tool_end"
|
|
134
|
+
and getattr(item, "status", None) != "failed"
|
|
135
|
+
and getattr(item, "tool_name", "") in {"apply_patch", "write_file", "edit_file"}
|
|
136
|
+
for item in items
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _merge_changed_file_sections(sections: list[dict]) -> list[ChangedFile]:
|
|
141
|
+
merged: dict[str, dict] = {}
|
|
142
|
+
order: list[str] = []
|
|
143
|
+
for item in sections:
|
|
144
|
+
if not item.get("added") and not item.get("removed"):
|
|
145
|
+
continue
|
|
146
|
+
path = str(item.get("path", ""))
|
|
147
|
+
if not path:
|
|
148
|
+
continue
|
|
149
|
+
if path not in merged:
|
|
150
|
+
merged[path] = {"path": path, "added": 0, "removed": 0, "diffs": []}
|
|
151
|
+
order.append(path)
|
|
152
|
+
merged_item = merged[path]
|
|
153
|
+
merged_item["added"] += int(item.get("added", 0) or 0)
|
|
154
|
+
merged_item["removed"] += int(item.get("removed", 0) or 0)
|
|
155
|
+
merged_item["diffs"].append("\n".join(item.get("lines", [])))
|
|
156
|
+
return [
|
|
157
|
+
ChangedFile(
|
|
158
|
+
path=merged[path]["path"],
|
|
159
|
+
added=merged[path]["added"],
|
|
160
|
+
removed=merged[path]["removed"],
|
|
161
|
+
diff="\n\n".join(diff for diff in merged[path]["diffs"] if diff),
|
|
162
|
+
)
|
|
163
|
+
for path in order
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _diff_section_has_changes(section: dict) -> bool:
|
|
168
|
+
return bool(
|
|
169
|
+
section.get("added")
|
|
170
|
+
or section.get("removed")
|
|
171
|
+
or any(str(line).startswith("@@") for line in section.get("lines", []))
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _path_from_diff_header(line: str) -> str:
|
|
176
|
+
match = re.match(r"diff --git a/(.+?) b/(.+)$", line)
|
|
177
|
+
if match:
|
|
178
|
+
return match.group(2)
|
|
179
|
+
parts = line.split()
|
|
180
|
+
return _strip_diff_prefix(parts[-1]) if parts else "file"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _strip_diff_prefix(path: str) -> str:
|
|
184
|
+
if path.startswith("a/") or path.startswith("b/"):
|
|
185
|
+
return path[2:]
|
|
186
|
+
return path
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Lightweight context compression for long-running sessions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from langchain_core.messages import BaseMessage, ToolMessage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_COMPRESSION_RATIO = 0.7
|
|
9
|
+
DEFAULT_KEEP_RECENT_MESSAGES = 20
|
|
10
|
+
DEFAULT_MAX_TOOL_CHARS = 2_000
|
|
11
|
+
AUTO_COMPRESSION_REASON = "context window usage crossed the compression threshold."
|
|
12
|
+
MANUAL_COMPRESSION_REASON = "manually compressed by Message Token Manager."
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CompressionResult:
|
|
17
|
+
"""Result of a context compression pass."""
|
|
18
|
+
|
|
19
|
+
messages: list[BaseMessage]
|
|
20
|
+
did_compress: bool
|
|
21
|
+
original_tokens: int
|
|
22
|
+
compressed_tokens: int
|
|
23
|
+
compressed_messages: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextCompressor:
|
|
27
|
+
"""Conservative compressor that trims old tool outputs."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
context_window_tokens: int,
|
|
33
|
+
compression_ratio: float = DEFAULT_COMPRESSION_RATIO,
|
|
34
|
+
keep_recent_messages: int = DEFAULT_KEEP_RECENT_MESSAGES,
|
|
35
|
+
max_tool_chars: int = DEFAULT_MAX_TOOL_CHARS,
|
|
36
|
+
):
|
|
37
|
+
self.context_window_tokens = context_window_tokens
|
|
38
|
+
self.compression_ratio = compression_ratio
|
|
39
|
+
self.keep_recent_messages = keep_recent_messages
|
|
40
|
+
self.max_tool_chars = max_tool_chars
|
|
41
|
+
|
|
42
|
+
def maybe_compress(
|
|
43
|
+
self,
|
|
44
|
+
messages: list[BaseMessage],
|
|
45
|
+
estimated_tokens: int,
|
|
46
|
+
estimate_tokens,
|
|
47
|
+
) -> CompressionResult:
|
|
48
|
+
"""Compress old tool outputs when context usage crosses the threshold."""
|
|
49
|
+
if estimated_tokens < self.threshold_tokens:
|
|
50
|
+
return CompressionResult(messages, False, estimated_tokens, estimated_tokens, 0)
|
|
51
|
+
|
|
52
|
+
compressed_messages = []
|
|
53
|
+
compressed_count = 0
|
|
54
|
+
cutoff = max(len(messages) - self.keep_recent_messages, 0)
|
|
55
|
+
for index, message in enumerate(messages):
|
|
56
|
+
if index < cutoff and self._should_trim_tool_message(message):
|
|
57
|
+
compressed_messages.append(self._trim_tool_message(message))
|
|
58
|
+
compressed_count += 1
|
|
59
|
+
else:
|
|
60
|
+
compressed_messages.append(message)
|
|
61
|
+
|
|
62
|
+
if compressed_count == 0:
|
|
63
|
+
return CompressionResult(messages, False, estimated_tokens, estimated_tokens, 0)
|
|
64
|
+
|
|
65
|
+
compressed_tokens = estimate_tokens(compressed_messages)
|
|
66
|
+
return CompressionResult(
|
|
67
|
+
compressed_messages,
|
|
68
|
+
compressed_tokens < estimated_tokens,
|
|
69
|
+
estimated_tokens,
|
|
70
|
+
compressed_tokens,
|
|
71
|
+
compressed_count,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def threshold_tokens(self) -> int:
|
|
76
|
+
"""Token threshold that triggers compression."""
|
|
77
|
+
return int(self.context_window_tokens * self.compression_ratio)
|
|
78
|
+
|
|
79
|
+
def _should_trim_tool_message(self, message: BaseMessage) -> bool:
|
|
80
|
+
if not isinstance(message, ToolMessage):
|
|
81
|
+
return False
|
|
82
|
+
content = message.content
|
|
83
|
+
return isinstance(content, str) and len(content) > self.max_tool_chars
|
|
84
|
+
|
|
85
|
+
def _trim_tool_message(self, message: ToolMessage) -> ToolMessage:
|
|
86
|
+
return compress_tool_message(message, reason=AUTO_COMPRESSION_REASON)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compress_tool_message(
|
|
90
|
+
message: ToolMessage,
|
|
91
|
+
*,
|
|
92
|
+
reason: str,
|
|
93
|
+
estimated_original_tokens: int | None = None,
|
|
94
|
+
) -> ToolMessage:
|
|
95
|
+
"""Return a compact marker ToolMessage while preserving tool linkage."""
|
|
96
|
+
original_chars = len(str(message.content))
|
|
97
|
+
name = message.name or "unknown"
|
|
98
|
+
lines = [
|
|
99
|
+
"[Compressed old tool output]",
|
|
100
|
+
f"tool: {name}",
|
|
101
|
+
f"original_chars: {original_chars}",
|
|
102
|
+
]
|
|
103
|
+
if estimated_original_tokens is not None:
|
|
104
|
+
lines.append(f"estimated_original_tokens: {estimated_original_tokens}")
|
|
105
|
+
lines.append(f"reason: {reason}")
|
|
106
|
+
trimmed = ToolMessage(
|
|
107
|
+
content="\n".join(lines),
|
|
108
|
+
tool_call_id=message.tool_call_id,
|
|
109
|
+
name=message.name,
|
|
110
|
+
)
|
|
111
|
+
trimmed.additional_kwargs.update(getattr(message, "additional_kwargs", {}) or {})
|
|
112
|
+
trimmed.additional_kwargs["context_compressed"] = True
|
|
113
|
+
trimmed.additional_kwargs["original_chars"] = original_chars
|
|
114
|
+
if estimated_original_tokens is not None:
|
|
115
|
+
trimmed.additional_kwargs["estimated_original_tokens"] = estimated_original_tokens
|
|
116
|
+
return trimmed
|
agent/graph.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Agent graph definition using LangGraph with provider abstraction."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from langgraph.graph import StateGraph, START
|
|
6
|
+
|
|
7
|
+
from agent.approval import ApprovalCallback
|
|
8
|
+
from agent.nodes.llm_node import create_llm_node as _create_llm_node
|
|
9
|
+
from agent.nodes.state import AgentState
|
|
10
|
+
from agent.nodes.task_guard_node import (
|
|
11
|
+
create_task_guard_node,
|
|
12
|
+
route_after_llm,
|
|
13
|
+
route_after_tools,
|
|
14
|
+
route_after_task_guard,
|
|
15
|
+
)
|
|
16
|
+
from agent.nodes.tools_node import create_tools_node as _create_tools_node
|
|
17
|
+
from agent.providers.base import LLMProvider
|
|
18
|
+
from agent.runtime.context import AgentRuntimeContext, WorkflowState
|
|
19
|
+
from agent.streaming import StreamEventCallback
|
|
20
|
+
from agent.tool_retry import async_run_tool_with_retry
|
|
21
|
+
from tools import TOOL_HANDLERS, TOOLS
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_runtime(
|
|
25
|
+
provider: LLMProvider,
|
|
26
|
+
system_prompt: str,
|
|
27
|
+
todo_manager,
|
|
28
|
+
workdir: Path,
|
|
29
|
+
session_id: str,
|
|
30
|
+
skill_dirs: list[str] | None = None,
|
|
31
|
+
app_root: Path | None = None,
|
|
32
|
+
stream_callback: StreamEventCallback = None,
|
|
33
|
+
approval_callback: ApprovalCallback = None,
|
|
34
|
+
) -> AgentRuntimeContext:
|
|
35
|
+
"""Create runtime context for a graph run."""
|
|
36
|
+
return AgentRuntimeContext(
|
|
37
|
+
provider=provider,
|
|
38
|
+
system_prompt=system_prompt,
|
|
39
|
+
todo_manager=todo_manager,
|
|
40
|
+
workdir=workdir,
|
|
41
|
+
session_id=session_id,
|
|
42
|
+
skill_dirs=skill_dirs,
|
|
43
|
+
app_root=app_root,
|
|
44
|
+
stream_callback=stream_callback,
|
|
45
|
+
approval_callback=approval_callback,
|
|
46
|
+
tools=TOOLS,
|
|
47
|
+
tool_handlers=TOOL_HANDLERS,
|
|
48
|
+
workflow_state=WorkflowState(),
|
|
49
|
+
run_tool=async_run_tool_with_retry,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_llm_node(
|
|
54
|
+
provider: LLMProvider,
|
|
55
|
+
system_prompt: str,
|
|
56
|
+
session_id: str,
|
|
57
|
+
stream_callback: StreamEventCallback = None,
|
|
58
|
+
):
|
|
59
|
+
"""Backward-compatible LLM node factory."""
|
|
60
|
+
runtime = AgentRuntimeContext(
|
|
61
|
+
provider=provider,
|
|
62
|
+
system_prompt=system_prompt,
|
|
63
|
+
todo_manager=None,
|
|
64
|
+
workdir=Path.cwd(),
|
|
65
|
+
session_id=session_id,
|
|
66
|
+
stream_callback=stream_callback,
|
|
67
|
+
tools=TOOLS,
|
|
68
|
+
tool_handlers=TOOL_HANDLERS,
|
|
69
|
+
)
|
|
70
|
+
return _create_llm_node(runtime)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_tools_node(
|
|
74
|
+
provider: LLMProvider,
|
|
75
|
+
system_prompt: str,
|
|
76
|
+
todo_manager,
|
|
77
|
+
workdir: Path,
|
|
78
|
+
session_id: str,
|
|
79
|
+
skill_dirs: list[str] | None = None,
|
|
80
|
+
app_root: Path | None = None,
|
|
81
|
+
stream_callback: StreamEventCallback = None,
|
|
82
|
+
approval_callback: ApprovalCallback = None,
|
|
83
|
+
):
|
|
84
|
+
"""Backward-compatible tools node factory."""
|
|
85
|
+
return _create_tools_node(
|
|
86
|
+
create_runtime(
|
|
87
|
+
provider=provider,
|
|
88
|
+
system_prompt=system_prompt,
|
|
89
|
+
todo_manager=todo_manager,
|
|
90
|
+
workdir=workdir,
|
|
91
|
+
session_id=session_id,
|
|
92
|
+
skill_dirs=skill_dirs,
|
|
93
|
+
app_root=app_root,
|
|
94
|
+
stream_callback=stream_callback,
|
|
95
|
+
approval_callback=approval_callback,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_graph(
|
|
101
|
+
provider: LLMProvider,
|
|
102
|
+
system_prompt: str,
|
|
103
|
+
todo_manager,
|
|
104
|
+
workdir: Path,
|
|
105
|
+
session_id: str,
|
|
106
|
+
skill_dirs: list[str] | None = None,
|
|
107
|
+
app_root: Path | None = None,
|
|
108
|
+
stream_callback: StreamEventCallback = None,
|
|
109
|
+
approval_callback: ApprovalCallback = None,
|
|
110
|
+
):
|
|
111
|
+
"""Build the agent graph."""
|
|
112
|
+
runtime = create_runtime(
|
|
113
|
+
provider=provider,
|
|
114
|
+
system_prompt=system_prompt,
|
|
115
|
+
todo_manager=todo_manager,
|
|
116
|
+
workdir=workdir,
|
|
117
|
+
session_id=session_id,
|
|
118
|
+
skill_dirs=skill_dirs,
|
|
119
|
+
app_root=app_root,
|
|
120
|
+
stream_callback=stream_callback,
|
|
121
|
+
approval_callback=approval_callback,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
builder = StateGraph(AgentState)
|
|
125
|
+
builder.add_node("llm", _create_llm_node(runtime))
|
|
126
|
+
builder.add_node("tools", _create_tools_node(runtime))
|
|
127
|
+
builder.add_node("task_guard", create_task_guard_node(todo_manager))
|
|
128
|
+
|
|
129
|
+
builder.add_edge(START, "llm")
|
|
130
|
+
builder.add_conditional_edges("llm", route_after_llm)
|
|
131
|
+
builder.add_conditional_edges("tools", route_after_tools)
|
|
132
|
+
builder.add_conditional_edges(
|
|
133
|
+
"task_guard",
|
|
134
|
+
lambda state: route_after_task_guard(state, todo_manager),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return builder.compile()
|