mycode-coding-agent 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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,25 @@
1
+ """Compatibility exports for agent domain events."""
2
+
3
+ from mycode.agent.events import (
4
+ AgentEvent,
5
+ AgentEventType,
6
+ AgentModelResponse,
7
+ AgentModelRetry,
8
+ AgentProgressSnapshot,
9
+ AgentStopReason,
10
+ AgentToolCall,
11
+ AgentWarning,
12
+ AgentWarningType,
13
+ )
14
+
15
+ __all__ = [
16
+ "AgentEvent",
17
+ "AgentEventType",
18
+ "AgentModelResponse",
19
+ "AgentModelRetry",
20
+ "AgentProgressSnapshot",
21
+ "AgentStopReason",
22
+ "AgentToolCall",
23
+ "AgentWarning",
24
+ "AgentWarningType",
25
+ ]
mycode/agent/events.py ADDED
@@ -0,0 +1,111 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Literal
3
+
4
+ from mycode.reasoning import ReasoningState, normalize_reasoning
5
+ from mycode.tools import ToolResult
6
+
7
+
8
+ AgentStopReason = Literal[
9
+ "final_answer",
10
+ "tool_calls",
11
+ "max_turns",
12
+ "tool_error",
13
+ "repeated_tool_call",
14
+ "context_overflow",
15
+ "model_error",
16
+ "control_tool",
17
+ ]
18
+
19
+ AgentEventType = Literal[
20
+ "context",
21
+ "turn",
22
+ "progress",
23
+ "artifact_warning",
24
+ "model_start",
25
+ "model_retry",
26
+ "reasoning_delta",
27
+ "reasoning_state",
28
+ "text_delta",
29
+ "tool_call",
30
+ "tool_result",
31
+ "stop",
32
+ "error",
33
+ ]
34
+
35
+ AgentWarningType = Literal["artifact_externalization"]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class AgentToolCall:
40
+ id: str
41
+ name: str
42
+ arguments: dict[str, object]
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class AgentWarning:
47
+ type: AgentWarningType
48
+ content: str
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class AgentProgressSnapshot:
53
+ stagnation_turns: int = 0
54
+ same_tool_repeat: int = 0
55
+ same_result_repeat: int = 0
56
+ resource_repeat: int = 0
57
+ convergence_guided: bool = False
58
+ reason: str = "run_started"
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class AgentModelRetry:
63
+ attempt: int
64
+ max_retries: int
65
+ delay_seconds: float
66
+ error_type: str
67
+ error_code: str
68
+ retryable: bool
69
+ call_kind: str
70
+ stream_started: bool
71
+ partial_output_chars: int
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class AgentModelResponse:
76
+ content: str = ""
77
+ tool_calls: list[AgentToolCall] = field(default_factory=list)
78
+ stop_reason: AgentStopReason = "final_answer"
79
+ warnings: tuple[AgentWarning, ...] = ()
80
+ reasoning_content: str | None = field(default=None, repr=False)
81
+ reasoning_state: ReasoningState = field(default="absent", repr=False)
82
+
83
+ def __post_init__(self) -> None:
84
+ reasoning_state, reasoning_content = normalize_reasoning(
85
+ self.reasoning_state,
86
+ self.reasoning_content,
87
+ )
88
+ object.__setattr__(self, "reasoning_state", reasoning_state)
89
+ object.__setattr__(self, "reasoning_content", reasoning_content)
90
+ if reasoning_state != "absent" and (
91
+ self.stop_reason != "tool_calls" or not self.tool_calls
92
+ ):
93
+ raise ValueError(
94
+ "reasoning_content is retained only for assistant tool-call responses."
95
+ )
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class AgentEvent:
100
+ type: AgentEventType
101
+ content: str = ""
102
+ turn_number: int | None = None
103
+ max_turns: int | None = None
104
+ progress: AgentProgressSnapshot | None = None
105
+ model_retry: AgentModelRetry | None = None
106
+ tool_call: AgentToolCall | None = None
107
+ tool_result: ToolResult | None = None
108
+ stop_reason: AgentStopReason | None = None
109
+ error: str | None = None
110
+ reasoning_content: str = field(default="", repr=False)
111
+ reasoning_state: ReasoningState = field(default="absent", repr=False)
@@ -0,0 +1,103 @@
1
+ """Structured status returned by one top-level Agent run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ from typing import Literal, cast
8
+
9
+ from mycode.agent.events import AgentStopReason
10
+
11
+
12
+ AgentRunStatus = Literal["completed", "task_incomplete", "runtime_failure"]
13
+
14
+ _COMPLETED_REASONS = frozenset({"final_answer", "control_tool"})
15
+ _TASK_INCOMPLETE_REASONS = frozenset(
16
+ {"max_turns", "repeated_tool_call"}
17
+ )
18
+ _KNOWN_STOP_REASONS = frozenset(
19
+ {
20
+ "final_answer",
21
+ "tool_calls",
22
+ "max_turns",
23
+ "tool_error",
24
+ "repeated_tool_call",
25
+ "context_overflow",
26
+ "model_error",
27
+ "control_tool",
28
+ }
29
+ )
30
+ _EXIT_CODES: dict[AgentRunStatus, int] = {
31
+ "completed": 0,
32
+ "runtime_failure": 1,
33
+ "task_incomplete": 2,
34
+ }
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class AgentRunOutcome:
39
+ """Loop termination result; completed does not assert semantic correctness."""
40
+
41
+ status: AgentRunStatus
42
+ stop_reason: AgentStopReason | None
43
+
44
+ def __post_init__(self) -> None:
45
+ if self.status not in _EXIT_CODES:
46
+ raise ValueError(f"Unknown Agent run status: {self.status!r}")
47
+ if self.stop_reason is not None and self.stop_reason not in _KNOWN_STOP_REASONS:
48
+ raise ValueError(f"Unknown Agent stop reason: {self.stop_reason!r}")
49
+ expected = _status_for_stop_reason(self.stop_reason)
50
+ if self.status != expected:
51
+ raise ValueError(
52
+ "Agent run status does not match stop reason: "
53
+ f"status={self.status!r}, stop_reason={self.stop_reason!r}"
54
+ )
55
+
56
+ @classmethod
57
+ def from_stop_reason(
58
+ cls,
59
+ stop_reason: AgentStopReason | None,
60
+ ) -> AgentRunOutcome:
61
+ return cls(status=_status_for_stop_reason(stop_reason), stop_reason=stop_reason)
62
+
63
+ @classmethod
64
+ def from_json(cls, payload: str) -> AgentRunOutcome:
65
+ try:
66
+ raw = json.loads(payload)
67
+ except json.JSONDecodeError as error:
68
+ raise ValueError("Invalid Agent run outcome JSON") from error
69
+ if not isinstance(raw, dict) or set(raw) != {"status", "stop_reason"}:
70
+ raise ValueError("Agent run outcome must contain only status and stop_reason")
71
+
72
+ status = raw["status"]
73
+ stop_reason = raw["stop_reason"]
74
+ if not isinstance(status, str) or status not in _EXIT_CODES:
75
+ raise ValueError("Invalid Agent run status")
76
+ if stop_reason is not None and (
77
+ not isinstance(stop_reason, str) or stop_reason not in _KNOWN_STOP_REASONS
78
+ ):
79
+ raise ValueError("Invalid Agent stop reason")
80
+ return cls(
81
+ status=cast(AgentRunStatus, status),
82
+ stop_reason=cast(AgentStopReason | None, stop_reason),
83
+ )
84
+
85
+ @property
86
+ def exit_code(self) -> int:
87
+ return _EXIT_CODES[self.status]
88
+
89
+ def to_json(self) -> str:
90
+ return json.dumps(
91
+ {"status": self.status, "stop_reason": self.stop_reason},
92
+ sort_keys=True,
93
+ )
94
+
95
+
96
+ def _status_for_stop_reason(
97
+ stop_reason: AgentStopReason | None,
98
+ ) -> AgentRunStatus:
99
+ if stop_reason in _COMPLETED_REASONS:
100
+ return "completed"
101
+ if stop_reason in _TASK_INCOMPLETE_REASONS:
102
+ return "task_incomplete"
103
+ return "runtime_failure"
@@ -0,0 +1,373 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ import hashlib
4
+ import json
5
+ from pathlib import PurePosixPath
6
+ from typing import Iterable, Literal
7
+
8
+ from mycode.conversation import Conversation
9
+ from mycode.tools.patterns import is_explicit_path_pattern
10
+ from mycode.tools.validation_command import analyze_validation_command
11
+ from mycode.tools.workspace import Workspace, WorkspacePathError
12
+
13
+
14
+ DEFAULT_MAX_TURNS = 50
15
+ MAIN_NEAR_LIMIT_REMAINING_TURNS = 5
16
+ DEFAULT_STAGNATION_REPEAT_LIMIT = 2
17
+ RUN_CHECKPOINT_HEADING = "## 续跑检查点"
18
+
19
+ MAX_TURNS_FINALIZATION_PROMPT = (
20
+ "本轮可用的模型轮次已经用完。请停止继续探索,不要请求或假设任何新的工具结果。"
21
+ "请仅根据当前对话中已有的工具结果,用中文生成可直接供下一次续跑使用的检查点。"
22
+ f"必须以“{RUN_CHECKPOINT_HEADING}”开头,并依次包含:已确认事实、已读取文件及范围、"
23
+ "已修改文件、测试状态、剩余动作、当前阻塞。没有内容的栏目也要写“无”。"
24
+ "不要声称读取过实际未读取的文件,并检查数量、列表和文件名是否前后一致。"
25
+ "内容要简洁、可执行,不要重新展开方案论证;运行状态将由 CLI 单独提示。"
26
+ )
27
+ MAIN_NEAR_LIMIT_PROMPT = (
28
+ "当前距离运行轮次上限还有 5 轮。请重新评估剩余工作,并优先使用剩余轮次"
29
+ "完成最重要的必要步骤。如果无法在剩余轮次内完成任务,请尽量保留已有成果,"
30
+ "并明确说明未完成部分或阻塞原因。"
31
+ )
32
+ RESUME_PROMPT = (
33
+ "这是达到上限后的续跑。请直接使用上一条“续跑检查点”衔接工作,"
34
+ "不要重新读取已经确认的文件,不要重复输出原方案;先核对已有改动和测试状态,"
35
+ "然后执行检查点中的剩余动作。"
36
+ )
37
+
38
+ ToolResultStatus = Literal["success", "failure"] | None
39
+ MutationStatus = Literal["yes", "no", "unknown"]
40
+ ValidationStatus = Literal["none", "pass", "fail", "unknown"]
41
+
42
+
43
+ class RuntimePolicy(str, Enum):
44
+ NO_INTERVENTION = "NO_INTERVENTION"
45
+ CONVERGENCE_GUIDANCE = "CONVERGENCE_GUIDANCE"
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class RuntimeObservation:
50
+ """Facts observed at the model/tool boundary; contains no policy semantics."""
51
+
52
+ tool_result: ToolResultStatus = None
53
+ tool_signature: str | None = None
54
+ resource: str | None = None
55
+ mutation: MutationStatus = "no"
56
+ validation: ValidationStatus = "none"
57
+ result_signature: str | None = None
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class PolicyDecision:
62
+ policy: RuntimePolicy
63
+ guidance: tuple[str, ...] = ()
64
+ notice: str = ""
65
+
66
+
67
+ @dataclass
68
+ class RuntimeState:
69
+ stagnation_turns: int = 0
70
+ same_tool_repeat: int = 0
71
+ same_result_repeat: int = 0
72
+ resource_repeat: int = 0
73
+ convergence_guided: bool = False
74
+ last_observation: RuntimeObservation | None = None
75
+ last_tool_observation: RuntimeObservation | None = None
76
+ last_reason: str = "run_started"
77
+ _previous_tool_pattern: tuple[str, ...] | None = field(default=None, repr=False)
78
+ _previous_result_pattern: tuple[str, ...] | None = field(default=None, repr=False)
79
+ _previous_resource_pattern: tuple[str, ...] | None = field(default=None, repr=False)
80
+
81
+ def observe_tool_turn(
82
+ self,
83
+ observations: tuple[RuntimeObservation, ...],
84
+ ) -> None:
85
+ if not observations:
86
+ return
87
+ had_stagnation = self.stagnation_turns > 0
88
+ for observation in observations:
89
+ self.last_observation = observation
90
+ self.last_tool_observation = observation
91
+
92
+ tool_pattern = _present_pattern(
93
+ observation.tool_signature for observation in observations
94
+ )
95
+ result_pattern = _present_pattern(
96
+ observation.result_signature for observation in observations
97
+ )
98
+ resource_pattern = _present_pattern(
99
+ observation.resource for observation in observations
100
+ )
101
+ self.same_tool_repeat, self._previous_tool_pattern = _next_repeat(
102
+ tool_pattern, self._previous_tool_pattern, self.same_tool_repeat
103
+ )
104
+ self.same_result_repeat, self._previous_result_pattern = _next_repeat(
105
+ result_pattern, self._previous_result_pattern, self.same_result_repeat
106
+ )
107
+ self.resource_repeat, self._previous_resource_pattern = _next_repeat(
108
+ resource_pattern, self._previous_resource_pattern, self.resource_repeat
109
+ )
110
+ repeated = max(
111
+ self.same_tool_repeat,
112
+ self.same_result_repeat,
113
+ self.resource_repeat,
114
+ ) >= DEFAULT_STAGNATION_REPEAT_LIMIT
115
+ if repeated:
116
+ self.stagnation_turns += 1
117
+ self.last_reason = "repetition_observed"
118
+ return
119
+ self.stagnation_turns = max(0, self.stagnation_turns - 1)
120
+ if self.stagnation_turns == 0:
121
+ self.convergence_guided = False
122
+ if had_stagnation:
123
+ self.last_reason = "stagnation_episode_ended"
124
+ elif had_stagnation:
125
+ self.last_reason = "stagnation_decayed"
126
+
127
+ def observe_tool_result(
128
+ *,
129
+ tool_name: str,
130
+ capability: str | None,
131
+ arguments: dict[str, object],
132
+ ok: bool,
133
+ content: str,
134
+ error: str | None,
135
+ metadata: dict[str, object],
136
+ workspace: Workspace | None = None,
137
+ ) -> RuntimeObservation:
138
+ """Build a factual observation from one completed tool call."""
139
+ return RuntimeObservation(
140
+ tool_result="success" if ok else "failure",
141
+ tool_signature=tool_call_signature(tool_name, arguments),
142
+ resource=classify_resource(
143
+ tool_name,
144
+ arguments,
145
+ workspace,
146
+ metadata=metadata,
147
+ ),
148
+ mutation=classify_mutation(
149
+ tool_name=tool_name,
150
+ capability=capability,
151
+ arguments=arguments,
152
+ ok=ok,
153
+ ),
154
+ validation=classify_validation(
155
+ tool_name=tool_name,
156
+ arguments=arguments,
157
+ metadata=metadata,
158
+ ),
159
+ result_signature=tool_result_signature(
160
+ tool_name=tool_name,
161
+ ok=ok,
162
+ content=content,
163
+ error=error,
164
+ metadata=metadata,
165
+ ),
166
+ )
167
+
168
+
169
+ def classify_mutation(
170
+ *,
171
+ tool_name: str,
172
+ capability: str | None,
173
+ arguments: dict[str, object],
174
+ ok: bool,
175
+ ) -> MutationStatus:
176
+ if tool_name == "run_command":
177
+ return "unknown"
178
+ if tool_name == "run_validation":
179
+ return "no"
180
+ if tool_name in {"edit_file", "write_file"}:
181
+ path = arguments.get("path")
182
+ if isinstance(path, str) and _is_temporary_analysis_path(path):
183
+ return "no"
184
+ return "yes" if ok else "no"
185
+ if capability in {"read", "delegate"} or tool_name == "read_artifact":
186
+ return "no"
187
+ return "unknown"
188
+
189
+
190
+ def classify_validation(
191
+ *,
192
+ tool_name: str,
193
+ arguments: dict[str, object],
194
+ metadata: dict[str, object],
195
+ ) -> ValidationStatus:
196
+ if tool_name not in {"run_command", "run_validation"}:
197
+ return "none"
198
+ command = arguments.get("command")
199
+ if not isinstance(command, list) or any(
200
+ not isinstance(part, str) for part in command
201
+ ):
202
+ return "unknown"
203
+ analysis = analyze_validation_command(command)
204
+ if analysis.classification == "non_validation":
205
+ return "none"
206
+ if analysis.classification == "unknown":
207
+ return "unknown"
208
+ exit_code = metadata.get("exit_code")
209
+ if isinstance(exit_code, int) and not isinstance(exit_code, bool):
210
+ return "pass" if exit_code == 0 else "fail"
211
+ return "unknown"
212
+
213
+
214
+ def classify_resource(
215
+ tool_name: str,
216
+ arguments: dict[str, object],
217
+ workspace: Workspace | None,
218
+ *,
219
+ metadata: dict[str, object] | None = None,
220
+ ) -> str | None:
221
+ if workspace is None:
222
+ return None
223
+ if tool_name == "read_file":
224
+ metadata = {} if metadata is None else metadata
225
+ path_value = metadata.get("path")
226
+ start_line = metadata.get("start_line")
227
+ end_line = metadata.get("end_line")
228
+ if (
229
+ not isinstance(start_line, int)
230
+ or isinstance(start_line, bool)
231
+ or not isinstance(end_line, int)
232
+ or isinstance(end_line, bool)
233
+ or end_line < start_line
234
+ ):
235
+ return None
236
+ elif tool_name in {"edit_file", "write_file"}:
237
+ path_value = arguments.get("path")
238
+ elif tool_name == "grep":
239
+ path_value = arguments.get("path_pattern")
240
+ if not isinstance(path_value, str) or not is_explicit_path_pattern(path_value):
241
+ return None
242
+ else:
243
+ return None
244
+ if not isinstance(path_value, str) or not path_value.strip():
245
+ return None
246
+ try:
247
+ resolved = workspace.resolve_path(path_value.replace("\\", "/"))
248
+ relative = resolved.relative_to(workspace.root).as_posix()
249
+ except (OSError, ValueError, WorkspacePathError):
250
+ return None
251
+ if tool_name == "read_file":
252
+ return f"file:{relative}:{start_line}-{end_line}"
253
+ return f"file:{relative}"
254
+
255
+
256
+ def decide_runtime_policy(state: RuntimeState) -> PolicyDecision:
257
+ if (
258
+ state.stagnation_turns >= 2
259
+ and not state.convergence_guided
260
+ ):
261
+ state.convergence_guided = True
262
+ evidence = (
263
+ f"same_tool_repeat={state.same_tool_repeat}, "
264
+ f"same_result_repeat={state.same_result_repeat}, "
265
+ f"resource_repeat={state.resource_repeat}"
266
+ )
267
+ return PolicyDecision(
268
+ policy=RuntimePolicy.CONVERGENCE_GUIDANCE,
269
+ guidance=(
270
+ "Runtime 观察到重复主导的停滞特征("
271
+ f"{evidence})。请重新评估当前假设和路径;"
272
+ "下一步行动由你决定。"
273
+ ,),
274
+ notice="检测到重复停滞特征,已提示 Agent 重新评估当前路径",
275
+ )
276
+ return PolicyDecision(policy=RuntimePolicy.NO_INTERVENTION)
277
+
278
+
279
+ def tool_call_signature(tool_name: str, arguments: dict[str, object]) -> str:
280
+ normalized = json.dumps(arguments, ensure_ascii=False, sort_keys=True, default=str)
281
+ return f"{tool_name}:{normalized}"
282
+
283
+
284
+ def tool_result_signature(
285
+ *,
286
+ tool_name: str,
287
+ ok: bool,
288
+ content: str,
289
+ error: str | None,
290
+ metadata: dict[str, object],
291
+ ) -> str:
292
+ payload = json.dumps(
293
+ {
294
+ "tool": tool_name,
295
+ "ok": ok,
296
+ "content": content,
297
+ "error": error,
298
+ "metadata": metadata,
299
+ },
300
+ ensure_ascii=False,
301
+ sort_keys=True,
302
+ default=str,
303
+ )
304
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
305
+
306
+
307
+ def _present_pattern(values: Iterable[str | None]) -> tuple[str, ...] | None:
308
+ pattern = tuple(value for value in values if value is not None)
309
+ return pattern or None
310
+
311
+
312
+ def _next_repeat(
313
+ pattern: tuple[str, ...] | None,
314
+ previous: tuple[str, ...] | None,
315
+ current: int,
316
+ ) -> tuple[int, tuple[str, ...] | None]:
317
+ if pattern is None:
318
+ return 0, None
319
+ if pattern == previous:
320
+ return current + 1, pattern
321
+ return 1, pattern
322
+
323
+
324
+ def _is_temporary_analysis_path(value: str) -> bool:
325
+ path = PurePosixPath(value.replace("\\", "/"))
326
+ parts = tuple(part.casefold() for part in path.parts if part not in {"", "/"})
327
+ if not parts:
328
+ return False
329
+ ignored_directories = {
330
+ ".cache",
331
+ ".mypy_cache",
332
+ ".pytest_cache",
333
+ ".ruff_cache",
334
+ ".temp",
335
+ ".tmp",
336
+ "__pycache__",
337
+ "build",
338
+ "dist",
339
+ "temp",
340
+ "tmp",
341
+ }
342
+ if any(part in ignored_directories for part in parts[:-1]):
343
+ return True
344
+ name = parts[-1]
345
+ return name.endswith((".tmp", ".temp"))
346
+
347
+
348
+ def resume_guidance(conversation: Conversation, user_message: str) -> str | None:
349
+ if not user_message.strip().startswith("继续"):
350
+ return None
351
+ for message in reversed(conversation.get_messages()):
352
+ if message.role != "assistant":
353
+ continue
354
+ return RESUME_PROMPT if RUN_CHECKPOINT_HEADING in message.content else None
355
+ return None
356
+
357
+
358
+ def normalize_run_checkpoint(content: str) -> str:
359
+ if content.startswith(RUN_CHECKPOINT_HEADING):
360
+ return content
361
+ return f"{RUN_CHECKPOINT_HEADING}\n\n{content}"
362
+
363
+
364
+ def tool_result_evidence(
365
+ *, tool_name: str, ok: bool, content: str, error: str | None
366
+ ) -> str:
367
+ return tool_result_signature(
368
+ tool_name=tool_name,
369
+ ok=ok,
370
+ content=content,
371
+ error=error,
372
+ metadata={},
373
+ )