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,206 @@
1
+ from pathlib import Path
2
+ import shutil
3
+ import sys
4
+
5
+ from pydantic import Field, field_validator
6
+
7
+ from mycode.permissions import PermissionChecker, PermissionDecision, PermissionRequest
8
+ from mycode.skills import ActiveSkillState, SkillPathError, SkillRegistry
9
+ from mycode.tools.base import PydanticTool, ToolArgs, ToolResult
10
+ from mycode.tools.command_executor import CommandExecutionArgs, execute_command
11
+ from mycode.tools.permission_metadata import with_permission_metadata
12
+ from mycode.tools.run_command import (
13
+ DEFAULT_MAX_OUTPUT_CHARS,
14
+ DEFAULT_TIMEOUT_SECONDS,
15
+ MAX_OUTPUT_CHARS,
16
+ MAX_TIMEOUT_SECONDS,
17
+ )
18
+ from mycode.tools.workspace import Workspace
19
+
20
+
21
+ class RunSkillScriptArgs(ToolArgs):
22
+ skill: str = Field(min_length=1)
23
+ script: str = Field(min_length=1)
24
+ args: list[str] = Field(default_factory=list)
25
+ timeout_seconds: float = Field(
26
+ default=DEFAULT_TIMEOUT_SECONDS, gt=0, le=MAX_TIMEOUT_SECONDS
27
+ )
28
+ max_output_chars: int = Field(
29
+ default=DEFAULT_MAX_OUTPUT_CHARS, ge=0, le=MAX_OUTPUT_CHARS
30
+ )
31
+
32
+ @field_validator("args")
33
+ @classmethod
34
+ def validate_args(cls, value: list[str]) -> list[str]:
35
+ if any(not isinstance(part, str) or part == "" for part in value):
36
+ raise ValueError("Script arguments must be non-empty strings.")
37
+ if any("\x00" in part for part in value):
38
+ raise ValueError("Script arguments must not contain null bytes.")
39
+ return value
40
+
41
+
42
+ class RunSkillScriptTool(PydanticTool[RunSkillScriptArgs]):
43
+ name = "run_skill_script"
44
+ description = "在当前 workspace 中运行已激活 Skill 的受支持脚本。"
45
+ args_model = RunSkillScriptArgs
46
+ capability = "command"
47
+ risk = "high"
48
+
49
+ def __init__(
50
+ self,
51
+ workspace: Workspace,
52
+ registry: SkillRegistry,
53
+ state: ActiveSkillState,
54
+ ) -> None:
55
+ self.workspace = workspace
56
+ self.registry = registry
57
+ self.state = state
58
+
59
+ def build_permission_request(self, args: RunSkillScriptArgs) -> PermissionRequest:
60
+ logical_script = _logical_script_path(args.script)
61
+ return PermissionRequest(
62
+ tool_name=self.name,
63
+ capability=self.capability,
64
+ action=self.name,
65
+ target=f"{args.skill}:{logical_script}",
66
+ arguments=args.model_dump(),
67
+ description=(
68
+ f'Skill "{args.skill}" 请求在 workspace 中执行 {logical_script},'
69
+ f"参数为 {args.args!r}。"
70
+ ),
71
+ )
72
+
73
+ def check_permission(
74
+ self,
75
+ args: RunSkillScriptArgs,
76
+ permission_checker: PermissionChecker,
77
+ ) -> tuple[PermissionRequest, PermissionDecision]:
78
+ request = self.build_permission_request(args)
79
+ skill = self.registry.get(args.skill)
80
+ metadata = {
81
+ "skill_name": args.skill,
82
+ "script": _logical_script_path(args.script),
83
+ "cwd": str(self.workspace.root),
84
+ "arguments": list(args.args),
85
+ }
86
+ if skill is None or not self.state.is_active(args.skill):
87
+ return request, PermissionDecision.deny(
88
+ reason="unsupported_operation",
89
+ message=f"Skill is not active: {args.skill}",
90
+ metadata=metadata,
91
+ )
92
+ metadata["skill_source"] = skill.source
93
+ try:
94
+ script_path = self.registry.resolve_script(skill, args.script)
95
+ except SkillPathError as error:
96
+ return request, PermissionDecision.deny(
97
+ reason="outside_workspace", message=str(error), metadata=metadata
98
+ )
99
+ if not script_path.exists():
100
+ return request, PermissionDecision.deny(
101
+ reason="unsupported_operation",
102
+ message=f"Skill script not found: {args.script}",
103
+ metadata=metadata,
104
+ )
105
+ if not script_path.is_file():
106
+ return request, PermissionDecision.deny(
107
+ reason="unsupported_operation",
108
+ message=f"Skill script is not a file: {args.script}",
109
+ metadata=metadata,
110
+ )
111
+ runtime = _runtime_for_script(script_path)
112
+ if runtime is None:
113
+ return request, PermissionDecision.deny(
114
+ reason="unsupported_operation",
115
+ message=f"Unsupported Skill script type: {script_path.suffix or '<none>'}",
116
+ metadata=metadata,
117
+ )
118
+ executable = runtime[0]
119
+ if Path(executable).is_absolute():
120
+ runtime_available = Path(executable).is_file()
121
+ else:
122
+ runtime_available = shutil.which(executable) is not None
123
+ if not runtime_available:
124
+ return request, PermissionDecision.deny(
125
+ reason="unsupported_operation",
126
+ message=f"Required Skill script runtime is unavailable: {executable}",
127
+ metadata=metadata,
128
+ )
129
+ decision = permission_checker.check(request, self.get_permission_profile())
130
+ decision = with_permission_metadata(decision, metadata)
131
+ if decision.status == "ask":
132
+ decision = PermissionDecision.ask(
133
+ reason=decision.reason,
134
+ message=(
135
+ f'Skill "{skill.name}" 请求执行 '
136
+ f"{_logical_script_path(args.script)}。"
137
+ ),
138
+ metadata=decision.metadata,
139
+ )
140
+ return request, decision
141
+
142
+ def run_authorized(
143
+ self,
144
+ args: RunSkillScriptArgs,
145
+ decision: PermissionDecision,
146
+ ) -> ToolResult:
147
+ try:
148
+ skill = self.registry.require(args.skill)
149
+ if not self.state.is_active(skill.name):
150
+ raise ValueError(f"Skill is not active: {skill.name}")
151
+ script_path = self.registry.resolve_script(skill, args.script)
152
+ if not script_path.exists() or not script_path.is_file():
153
+ raise ValueError(f"Skill script is unavailable: {args.script}")
154
+ runtime = _runtime_for_script(script_path)
155
+ if runtime is None:
156
+ raise ValueError(
157
+ f"Unsupported Skill script type: {script_path.suffix or '<none>'}"
158
+ )
159
+ command = [*runtime, str(script_path), *args.args]
160
+ return execute_command(
161
+ args=CommandExecutionArgs(
162
+ command=command,
163
+ timeout_seconds=args.timeout_seconds,
164
+ max_output_chars=args.max_output_chars,
165
+ ),
166
+ cwd=self.workspace.root,
167
+ permission_metadata=decision.metadata,
168
+ permission_status=decision.status,
169
+ )
170
+ except Exception as error:
171
+ return ToolResult.failure(
172
+ error=f"Tool execution failed: {error}",
173
+ metadata={
174
+ **decision.metadata,
175
+ "permission_status": decision.status,
176
+ "exception_type": type(error).__name__,
177
+ },
178
+ )
179
+
180
+ def _run(self, args: RunSkillScriptArgs) -> ToolResult:
181
+ return ToolResult.failure(
182
+ error="run_skill_script must be run through ToolRegistry.run_tool().",
183
+ metadata={
184
+ "skill_name": args.skill,
185
+ "script": _logical_script_path(args.script),
186
+ "reason": "permission_required",
187
+ },
188
+ )
189
+
190
+
191
+ def _runtime_for_script(script: Path) -> list[str] | None:
192
+ suffix = script.suffix.lower()
193
+ if suffix == ".py":
194
+ return [sys.executable]
195
+ if suffix in {".js", ".mjs"}:
196
+ return ["node"]
197
+ if suffix == ".sh":
198
+ return ["bash"]
199
+ if suffix == ".ps1":
200
+ return ["pwsh", "-File"]
201
+ return None
202
+
203
+
204
+ def _logical_script_path(script: str) -> str:
205
+ normalized = script.replace("\\", "/")
206
+ return normalized if normalized.startswith("scripts/") else f"scripts/{normalized}"
@@ -0,0 +1,107 @@
1
+ import subprocess
2
+
3
+ from pydantic import Field, field_validator
4
+
5
+ from mycode.permissions import PermissionChecker, PermissionDecision, PermissionRequest
6
+ from mycode.subagents.limits import (
7
+ MAX_VALIDATION_COMMAND_CHARS,
8
+ MAX_VALIDATION_COMMAND_PART_CHARS,
9
+ MAX_VALIDATION_COMMAND_PARTS,
10
+ )
11
+ from mycode.tools.base import ToolResult
12
+ from mycode.tools.permission_metadata import with_permission_metadata
13
+ from mycode.tools.run_command import RunCommandArgs, RunCommandTool
14
+ from mycode.tools.validation_command import analyze_validation_command
15
+ from mycode.tools.workspace import Workspace
16
+
17
+
18
+ class RunValidationArgs(RunCommandArgs):
19
+ command: list[str] = Field(
20
+ min_length=1,
21
+ max_length=MAX_VALIDATION_COMMAND_PARTS,
22
+ )
23
+ cwd: str = Field(default=".", min_length=1, max_length=500)
24
+
25
+ @field_validator("command")
26
+ @classmethod
27
+ def validation_command_must_be_bounded(cls, value: list[str]) -> list[str]:
28
+ if any(len(part) > MAX_VALIDATION_COMMAND_PART_CHARS for part in value):
29
+ raise ValueError(
30
+ "Validation command parts must not exceed "
31
+ f"{MAX_VALIDATION_COMMAND_PART_CHARS} characters."
32
+ )
33
+ if sum(len(part) for part in value) > MAX_VALIDATION_COMMAND_CHARS:
34
+ raise ValueError(
35
+ "Validation command must not exceed "
36
+ f"{MAX_VALIDATION_COMMAND_CHARS} total characters."
37
+ )
38
+ return value
39
+
40
+
41
+ class RunValidationTool(RunCommandTool):
42
+ name = "run_validation"
43
+ description = (
44
+ "Run a non-interactive validation command inside the workspace using "
45
+ "the same safety and permission checks as run_command."
46
+ )
47
+ args_model = RunValidationArgs
48
+
49
+ def __init__(
50
+ self,
51
+ workspace: Workspace,
52
+ *,
53
+ restrict_to_known_validators: bool = True,
54
+ ) -> None:
55
+ super().__init__(workspace)
56
+ self.restrict_to_known_validators = restrict_to_known_validators
57
+
58
+ def build_permission_request(self, args: RunCommandArgs) -> PermissionRequest:
59
+ return PermissionRequest(
60
+ tool_name=self.name,
61
+ capability=self.capability,
62
+ action=self.name,
63
+ target=subprocess.list2cmdline(args.command),
64
+ arguments=args.model_dump(),
65
+ description="Run a validation command.",
66
+ )
67
+
68
+ def check_permission(
69
+ self,
70
+ args: RunCommandArgs,
71
+ permission_checker: PermissionChecker,
72
+ ) -> tuple[PermissionRequest, PermissionDecision]:
73
+ if not self.restrict_to_known_validators:
74
+ return super().check_permission(args, permission_checker)
75
+
76
+ request = self.build_permission_request(args)
77
+ validation = analyze_validation_command(args.command)
78
+ validation_metadata = {
79
+ "validation_allowed": validation.allowed,
80
+ "validation_classification": validation.classification,
81
+ "validation_category": validation.category,
82
+ "validation_reason": validation.reason,
83
+ }
84
+ if not validation.allowed:
85
+ return request, PermissionDecision.deny(
86
+ reason="unsupported_operation",
87
+ message=validation.reason,
88
+ metadata={
89
+ "tool_name": self.name,
90
+ "command": list(args.command),
91
+ "cwd": args.cwd,
92
+ **validation_metadata,
93
+ },
94
+ )
95
+
96
+ request, decision = super().check_permission(args, permission_checker)
97
+ return request, with_permission_metadata(decision, validation_metadata)
98
+
99
+ def _run(self, args: RunCommandArgs) -> ToolResult:
100
+ return ToolResult.failure(
101
+ error="run_validation must be run through ToolRegistry.run_tool().",
102
+ metadata={
103
+ "command": args.command,
104
+ "cwd": args.cwd,
105
+ "reason": "permission_required",
106
+ },
107
+ )
@@ -0,0 +1,93 @@
1
+ from collections.abc import Callable
2
+ from typing import Generic, TypeVar
3
+
4
+ from mycode.permissions import ToolRisk
5
+ from mycode.subagents.contracts import BoundedResultArgs, SubAgentRole
6
+ from mycode.tools.base import PydanticTool, ToolResult
7
+
8
+
9
+ DEFAULT_MAX_SUBMITTED_RESULT_CHARS = 24000
10
+
11
+ ResultArgsT = TypeVar("ResultArgsT", bound=BoundedResultArgs)
12
+
13
+
14
+ class SubmitResultTool(PydanticTool[ResultArgsT], Generic[ResultArgsT]):
15
+ name = "submit_result"
16
+ description = "Submit the role-specific structured result and finish the SubAgent run."
17
+ capability = "control"
18
+ risk: ToolRisk = "low"
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ role: SubAgentRole,
24
+ result_model: type[ResultArgsT],
25
+ max_result_chars: int = DEFAULT_MAX_SUBMITTED_RESULT_CHARS,
26
+ acceptance_validator: Callable[[ResultArgsT], str | None] | None = None,
27
+ ) -> None:
28
+ if max_result_chars < 1:
29
+ raise ValueError("max_result_chars must be at least 1.")
30
+ self.role = role
31
+ self.args_model = result_model
32
+ self.max_result_chars = max_result_chars
33
+ self.acceptance_validator = acceptance_validator
34
+ self.submitted_result: ResultArgsT | None = None
35
+
36
+ def _run(self, args: ResultArgsT) -> ToolResult:
37
+ if self.submitted_result is not None:
38
+ return ToolResult.failure(
39
+ error="A structured SubAgent result has already been submitted.",
40
+ metadata={"role": self.role, "reason": "result_already_submitted"},
41
+ )
42
+
43
+ serialized = args.model_dump_json()
44
+ result_chars = len(serialized)
45
+ if result_chars > self.max_result_chars:
46
+ return ToolResult.failure(
47
+ error=(
48
+ "Structured SubAgent result exceeds the allowed size: "
49
+ f"{result_chars}/{self.max_result_chars} characters."
50
+ ),
51
+ metadata={
52
+ "role": self.role,
53
+ "reason": "result_too_large",
54
+ "result_chars": result_chars,
55
+ "max_result_chars": self.max_result_chars,
56
+ },
57
+ )
58
+
59
+ if self.acceptance_validator is not None:
60
+ try:
61
+ validation_error = self.acceptance_validator(args)
62
+ except Exception as error:
63
+ return ToolResult.failure(
64
+ error="Runtime result validation failed unexpectedly.",
65
+ metadata={
66
+ "role": self.role,
67
+ "reason": "result_runtime_validation_error",
68
+ "exception_type": type(error).__name__,
69
+ },
70
+ )
71
+ if validation_error is not None:
72
+ return ToolResult.failure(
73
+ error=validation_error,
74
+ metadata={
75
+ "role": self.role,
76
+ "reason": "result_runtime_validation_failed",
77
+ },
78
+ )
79
+
80
+ self.submitted_result = args
81
+ outcome = getattr(args, "status", None)
82
+ if outcome is None:
83
+ outcome = getattr(args, "recommendation", None)
84
+ return ToolResult.success(
85
+ content="Structured SubAgent result accepted.",
86
+ metadata={
87
+ "role": self.role,
88
+ "outcome": outcome,
89
+ "result_chars": result_chars,
90
+ "truncated": args.truncated,
91
+ "omitted_count": args.omitted_count,
92
+ },
93
+ )
mycode/tools/text.py ADDED
@@ -0,0 +1,15 @@
1
+ SUPPORTED_TEXT_ENCODINGS = ("utf-8", "gbk")
2
+
3
+
4
+ def contains_nul_byte(raw_content: bytes) -> bool:
5
+ return b"\x00" in raw_content
6
+
7
+
8
+ def decode_text(raw_content: bytes) -> tuple[str, str] | None:
9
+ for encoding in SUPPORTED_TEXT_ENCODINGS:
10
+ try:
11
+ return raw_content.decode(encoding), encoding
12
+ except UnicodeDecodeError:
13
+ continue
14
+
15
+ return None