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.
Files changed (131) hide show
  1. agent/__init__.py +33 -0
  2. agent/acp/__init__.py +2 -0
  3. agent/acp/approval_adapter.py +134 -0
  4. agent/acp/content_adapter.py +45 -0
  5. agent/acp/jsonrpc.py +92 -0
  6. agent/acp/server.py +197 -0
  7. agent/acp/session_manager.py +193 -0
  8. agent/acp/update_adapter.py +192 -0
  9. agent/app_paths.py +25 -0
  10. agent/approval.py +169 -0
  11. agent/cancellation.py +52 -0
  12. agent/change_snapshot.py +186 -0
  13. agent/context_compressor.py +116 -0
  14. agent/graph.py +137 -0
  15. agent/llm_retry.py +434 -0
  16. agent/logger.py +97 -0
  17. agent/lsp/__init__.py +13 -0
  18. agent/lsp/client.py +151 -0
  19. agent/lsp/manager.py +234 -0
  20. agent/lsp/types.py +119 -0
  21. agent/message_context_manager.py +322 -0
  22. agent/message_format.py +105 -0
  23. agent/nodes/llm_node.py +58 -0
  24. agent/nodes/state.py +12 -0
  25. agent/nodes/task_guard_node.py +50 -0
  26. agent/nodes/tools_node.py +70 -0
  27. agent/plan_snapshot.py +70 -0
  28. agent/providers/__init__.py +13 -0
  29. agent/providers/anthropic_provider.py +268 -0
  30. agent/providers/base.py +52 -0
  31. agent/providers/openai_provider.py +279 -0
  32. agent/providers/text_tool_calls.py +118 -0
  33. agent/runtime/approval_service.py +184 -0
  34. agent/runtime/context.py +43 -0
  35. agent/runtime/tool_events.py +368 -0
  36. agent/runtime/tool_executor.py +208 -0
  37. agent/runtime/tool_output.py +261 -0
  38. agent/runtime/tool_registry.py +91 -0
  39. agent/runtime/tool_scheduler.py +35 -0
  40. agent/runtime/workflow_guard.py +217 -0
  41. agent/runtime/workspace.py +5 -0
  42. agent/runtime/workspace_tools.py +22 -0
  43. agent/session.py +787 -0
  44. agent/session_replay.py +95 -0
  45. agent/session_store.py +186 -0
  46. agent/skills.py +254 -0
  47. agent/streaming.py +248 -0
  48. agent/subagent.py +634 -0
  49. agent/task_memory.py +340 -0
  50. agent/todo_manager.py +304 -0
  51. agent/tool_retry.py +106 -0
  52. agent/tui/__init__.py +14 -0
  53. agent/tui/app.py +1325 -0
  54. agent/tui/approval.py +53 -0
  55. agent/tui/commands/__init__.py +6 -0
  56. agent/tui/commands/base.py +48 -0
  57. agent/tui/commands/clear.py +37 -0
  58. agent/tui/commands/help.py +27 -0
  59. agent/tui/commands/registry.py +94 -0
  60. agent/tui/help_content.py +108 -0
  61. agent/tui/renderers.py +1961 -0
  62. agent/tui/runner.py +439 -0
  63. agent/tui/state.py +653 -0
  64. main.py +465 -0
  65. tools/__init__.py +50 -0
  66. tools/apply_patch.py +305 -0
  67. tools/bash.py +76 -0
  68. tools/diff_utils.py +139 -0
  69. tools/edit_file.py +40 -0
  70. tools/git_diff.py +72 -0
  71. tools/git_show.py +65 -0
  72. tools/grep.py +149 -0
  73. tools/list_files.py +90 -0
  74. tools/list_skills.py +24 -0
  75. tools/load_skill.py +30 -0
  76. tools/lsp_definition.py +27 -0
  77. tools/lsp_diagnostics.py +32 -0
  78. tools/lsp_document_symbols.py +23 -0
  79. tools/lsp_hover.py +29 -0
  80. tools/lsp_references.py +37 -0
  81. tools/lsp_utils.py +38 -0
  82. tools/lsp_workspace_symbols.py +23 -0
  83. tools/read_file.py +61 -0
  84. tools/read_many_files.py +50 -0
  85. tools/safety.py +50 -0
  86. tools/subagent.py +57 -0
  87. tools/todo.py +89 -0
  88. tools/verify.py +107 -0
  89. tools/web_search.py +250 -0
  90. tools/workspace.py +36 -0
  91. tools/workspace_state.py +60 -0
  92. tools/write_file.py +88 -0
  93. utils/__init__.py +5 -0
  94. utils/retry.py +13 -0
  95. yycode-0.3.2.data/data/skills/code_review.md +61 -0
  96. yycode-0.3.2.data/data/skills/code_workflow.md +404 -0
  97. yycode-0.3.2.data/data/skills/drawio/SKILL.md +636 -0
  98. yycode-0.3.2.data/data/skills/drawio/agents/openai.yaml +19 -0
  99. yycode-0.3.2.data/data/skills/drawio/assets/demo-erd.drawio +84 -0
  100. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.drawio +91 -0
  101. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered-cn.png +0 -0
  102. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.drawio +112 -0
  103. yycode-0.3.2.data/data/skills/drawio/assets/demo-layered.png +0 -0
  104. yycode-0.3.2.data/data/skills/drawio/assets/demo-ml.drawio +90 -0
  105. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.drawio +68 -0
  106. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring-cn.png +0 -0
  107. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.drawio +86 -0
  108. yycode-0.3.2.data/data/skills/drawio/assets/demo-ring.png +0 -0
  109. yycode-0.3.2.data/data/skills/drawio/assets/demo-sequence.drawio +116 -0
  110. yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.drawio +66 -0
  111. yycode-0.3.2.data/data/skills/drawio/assets/demo-star-cn.png +0 -0
  112. yycode-0.3.2.data/data/skills/drawio/assets/demo-star.drawio +79 -0
  113. yycode-0.3.2.data/data/skills/drawio/assets/demo-star.png +0 -0
  114. yycode-0.3.2.data/data/skills/drawio/assets/demo-uml-class.drawio +64 -0
  115. yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.drawio +173 -0
  116. yycode-0.3.2.data/data/skills/drawio/assets/microservices-example.png +0 -0
  117. yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.drawio +120 -0
  118. yycode-0.3.2.data/data/skills/drawio/assets/workflow-cn.png +0 -0
  119. yycode-0.3.2.data/data/skills/drawio/assets/workflow.drawio +120 -0
  120. yycode-0.3.2.data/data/skills/drawio/assets/workflow.png +0 -0
  121. yycode-0.3.2.data/data/skills/drawio/docs/index.html +469 -0
  122. yycode-0.3.2.data/data/skills/drawio/docs/zh.html +456 -0
  123. yycode-0.3.2.data/data/skills/drawio/references/style-extraction.md +254 -0
  124. yycode-0.3.2.data/data/skills/drawio/styles/schema.json +112 -0
  125. yycode-0.3.2.data/data/skills/plan.md +115 -0
  126. yycode-0.3.2.data/data/skills/ppt/SKILL.md +254 -0
  127. yycode-0.3.2.dist-info/METADATA +12 -0
  128. yycode-0.3.2.dist-info/RECORD +131 -0
  129. yycode-0.3.2.dist-info/WHEEL +5 -0
  130. yycode-0.3.2.dist-info/entry_points.txt +2 -0
  131. yycode-0.3.2.dist-info/top_level.txt +4 -0
@@ -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()