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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/agent/__init__.py
ADDED
|
@@ -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)
|
mycode/agent/outcome.py
ADDED
|
@@ -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"
|
mycode/agent/progress.py
ADDED
|
@@ -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
|
+
)
|