kryth 1.0.0__tar.gz
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.
- kryth-1.0.0/LICENSE +21 -0
- kryth-1.0.0/PKG-INFO +31 -0
- kryth-1.0.0/pyproject.toml +51 -0
- kryth-1.0.0/setup.cfg +4 -0
- kryth-1.0.0/src/agent/__init__.py +141 -0
- kryth-1.0.0/src/agent/_checkpoint.py +43 -0
- kryth-1.0.0/src/agent/_common.py +44 -0
- kryth-1.0.0/src/agent/_critique.py +114 -0
- kryth-1.0.0/src/agent/_debug.py +55 -0
- kryth-1.0.0/src/agent/_file_ops.py +563 -0
- kryth-1.0.0/src/agent/_git.py +293 -0
- kryth-1.0.0/src/agent/_memory.py +122 -0
- kryth-1.0.0/src/agent/_plan.py +22 -0
- kryth-1.0.0/src/agent/_project_runner.py +212 -0
- kryth-1.0.0/src/agent/_results.py +88 -0
- kryth-1.0.0/src/agent/_search.py +273 -0
- kryth-1.0.0/src/agent/_shell.py +231 -0
- kryth-1.0.0/src/agent/_specs.py +820 -0
- kryth-1.0.0/src/agent/_subagent.py +163 -0
- kryth-1.0.0/src/agent/_task_graph.py +212 -0
- kryth-1.0.0/src/agent/_todos.py +46 -0
- kryth-1.0.0/src/agent/_verify.py +26 -0
- kryth-1.0.0/src/agent/agent_loop.py +714 -0
- kryth-1.0.0/src/agent/bridge/__init__.py +115 -0
- kryth-1.0.0/src/agent/bridge/providers/__init__.py +35 -0
- kryth-1.0.0/src/agent/bridge/providers/openai_browser.py +176 -0
- kryth-1.0.0/src/agent/bridge/server.py +357 -0
- kryth-1.0.0/src/agent/context.py +63 -0
- kryth-1.0.0/src/agent/env.py +87 -0
- kryth-1.0.0/src/agent/hooks.py +68 -0
- kryth-1.0.0/src/agent/io.py +57 -0
- kryth-1.0.0/src/agent/llm.py +1193 -0
- kryth-1.0.0/src/agent/model_router.py +150 -0
- kryth-1.0.0/src/agent/permissions.py +177 -0
- kryth-1.0.0/src/agent/persistence.py +563 -0
- kryth-1.0.0/src/agent/profiles.py +237 -0
- kryth-1.0.0/src/agent/project_context.py +292 -0
- kryth-1.0.0/src/agent/prompts.py +106 -0
- kryth-1.0.0/src/agent/repo_index.py +262 -0
- kryth-1.0.0/src/agent/retriever.py +229 -0
- kryth-1.0.0/src/agent/session.py +154 -0
- kryth-1.0.0/src/agent/settings.py +86 -0
- kryth-1.0.0/src/agent/skill_library.py +549 -0
- kryth-1.0.0/src/agent/skills.py +289 -0
- kryth-1.0.0/src/agent/snapshots.py +193 -0
- kryth-1.0.0/src/agent/tools/__init__.py +141 -0
- kryth-1.0.0/src/agent/tools/_checkpoint.py +43 -0
- kryth-1.0.0/src/agent/tools/_common.py +44 -0
- kryth-1.0.0/src/agent/tools/_critique.py +114 -0
- kryth-1.0.0/src/agent/tools/_debug.py +55 -0
- kryth-1.0.0/src/agent/tools/_file_ops.py +563 -0
- kryth-1.0.0/src/agent/tools/_git.py +293 -0
- kryth-1.0.0/src/agent/tools/_memory.py +122 -0
- kryth-1.0.0/src/agent/tools/_plan.py +22 -0
- kryth-1.0.0/src/agent/tools/_project_runner.py +212 -0
- kryth-1.0.0/src/agent/tools/_results.py +88 -0
- kryth-1.0.0/src/agent/tools/_search.py +273 -0
- kryth-1.0.0/src/agent/tools/_shell.py +231 -0
- kryth-1.0.0/src/agent/tools/_specs.py +820 -0
- kryth-1.0.0/src/agent/tools/_subagent.py +163 -0
- kryth-1.0.0/src/agent/tools/_task_graph.py +212 -0
- kryth-1.0.0/src/agent/tools/_todos.py +46 -0
- kryth-1.0.0/src/agent/tools/_verify.py +26 -0
- kryth-1.0.0/src/agent/ui/__init__.py +344 -0
- kryth-1.0.0/src/agent/ui/activity.py +87 -0
- kryth-1.0.0/src/agent/ui/command_panel.py +77 -0
- kryth-1.0.0/src/agent/ui/components.py +417 -0
- kryth-1.0.0/src/agent/ui/console.py +65 -0
- kryth-1.0.0/src/agent/ui/diff_renderer.py +172 -0
- kryth-1.0.0/src/agent/ui/events.py +170 -0
- kryth-1.0.0/src/agent/ui/hud.py +226 -0
- kryth-1.0.0/src/agent/ui/input.py +213 -0
- kryth-1.0.0/src/agent/ui/keyread.py +85 -0
- kryth-1.0.0/src/agent/ui/logger.py +158 -0
- kryth-1.0.0/src/agent/ui/motion.py +185 -0
- kryth-1.0.0/src/agent/ui/panels.py +85 -0
- kryth-1.0.0/src/agent/ui/progress.py +68 -0
- kryth-1.0.0/src/agent/ui/renderer.py +462 -0
- kryth-1.0.0/src/agent/ui/run_summary.py +131 -0
- kryth-1.0.0/src/agent/ui/status_manager.py +47 -0
- kryth-1.0.0/src/agent/ui/streaming.py +168 -0
- kryth-1.0.0/src/agent/ui/summarizer.py +249 -0
- kryth-1.0.0/src/agent/ui/syntax.py +101 -0
- kryth-1.0.0/src/agent/ui/theme.py +166 -0
- kryth-1.0.0/src/agent/ui/updates.py +92 -0
- kryth-1.0.0/src/agent/validators.py +132 -0
- kryth-1.0.0/src/kryth/__init__.py +4 -0
- kryth-1.0.0/src/kryth/_repl_main.py +1123 -0
- kryth-1.0.0/src/kryth/config.py +371 -0
- kryth-1.0.0/src/kryth/main.py +109 -0
- kryth-1.0.0/src/kryth.egg-info/PKG-INFO +31 -0
- kryth-1.0.0/src/kryth.egg-info/SOURCES.txt +94 -0
- kryth-1.0.0/src/kryth.egg-info/dependency_links.txt +1 -0
- kryth-1.0.0/src/kryth.egg-info/entry_points.txt +3 -0
- kryth-1.0.0/src/kryth.egg-info/requires.txt +10 -0
- kryth-1.0.0/src/kryth.egg-info/top_level.txt +2 -0
kryth-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Navadeep
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kryth-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kryth
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: KRYTH CLI: Autonomous Coding Intelligence — terminal AI coding agent with tools, sessions, and OpenAI-compatible model support
|
|
5
|
+
Author: Navadeep
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ai,coding-agent,ai-coder,cli,openai-compatible,developer-tools
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Classifier: Topic :: Terminals
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: openai>=1.0.0
|
|
23
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
+
Provides-Extra: bridge
|
|
27
|
+
Requires-Dist: fastapi>=0.110.0; extra == "bridge"
|
|
28
|
+
Requires-Dist: uvicorn>=0.29.0; extra == "bridge"
|
|
29
|
+
Requires-Dist: websockets>=12.0; extra == "bridge"
|
|
30
|
+
Requires-Dist: playwright>=1.44.0; extra == "bridge"
|
|
31
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kryth"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "KRYTH CLI: Autonomous Coding Intelligence — terminal AI coding agent with tools, sessions, and OpenAI-compatible model support"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Navadeep" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "coding-agent", "ai-coder", "cli", "openai-compatible", "developer-tools"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development",
|
|
27
|
+
"Topic :: Terminals",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = [
|
|
31
|
+
"openai>=1.0.0",
|
|
32
|
+
"prompt_toolkit>=3.0.0",
|
|
33
|
+
"rich>=13.0.0",
|
|
34
|
+
"python-dotenv>=1.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
bridge = [
|
|
39
|
+
"fastapi>=0.110.0",
|
|
40
|
+
"uvicorn>=0.29.0",
|
|
41
|
+
"websockets>=12.0",
|
|
42
|
+
"playwright>=1.44.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
|
|
47
|
+
kryth = "kryth.main:main"
|
|
48
|
+
kryth-cli = "kryth.main:main"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
kryth-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Tool registry for the agent loop.
|
|
2
|
+
|
|
3
|
+
Public surface:
|
|
4
|
+
|
|
5
|
+
- ``TOOLS`` — name → callable. Used by the dispatcher.
|
|
6
|
+
- ``TOOL_SPECS`` — JSON-schema list passed to the LLM via ``tools=``.
|
|
7
|
+
- ``READ_ONLY_TOOLS`` — names safe in plan mode.
|
|
8
|
+
- ``RUN_COMMAND_ERROR_MARKER`` — sentinel used to flag non-zero exits.
|
|
9
|
+
|
|
10
|
+
Implementations live in private submodules (``_file_ops``, ``_search``,
|
|
11
|
+
``_shell``, ``_todos``, ``_plan``, ``_subagent``). ``_specs`` carries
|
|
12
|
+
the LLM-facing schemas. The registry is assembled here so callers can
|
|
13
|
+
keep doing ``from agent.tools import TOOLS, TOOL_SPECS``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from agent.tools._common import RUN_COMMAND_ERROR_MARKER
|
|
19
|
+
from agent.tools._file_ops import (
|
|
20
|
+
read_file,
|
|
21
|
+
write_file,
|
|
22
|
+
edit_file,
|
|
23
|
+
multi_edit,
|
|
24
|
+
delete_file,
|
|
25
|
+
list_files,
|
|
26
|
+
rollback_file,
|
|
27
|
+
)
|
|
28
|
+
from agent.tools._memory import add_memory
|
|
29
|
+
from agent.tools._search import (
|
|
30
|
+
search_code,
|
|
31
|
+
grep,
|
|
32
|
+
glob_files,
|
|
33
|
+
semantic_search,
|
|
34
|
+
lookup_symbol,
|
|
35
|
+
lookup_imports,
|
|
36
|
+
lookup_dependents,
|
|
37
|
+
)
|
|
38
|
+
from agent.tools._shell import run_command, task_output
|
|
39
|
+
from agent.tools._todos import todo_write, todo_read
|
|
40
|
+
from agent.tools._plan import exit_plan_mode
|
|
41
|
+
from agent.tools._subagent import spawn_agent, spawn_agents_parallel
|
|
42
|
+
from agent.tools._critique import self_critique
|
|
43
|
+
from agent.tools._git import git_op
|
|
44
|
+
from agent.tools._verify import verify_files
|
|
45
|
+
from agent.tools._debug import diagnose_error
|
|
46
|
+
from agent.tools._project_runner import run_tests, run_install
|
|
47
|
+
from agent.tools._checkpoint import checkpoint
|
|
48
|
+
from agent.tools._task_graph import run_task_graph
|
|
49
|
+
from agent.tools._specs import TOOL_SPECS
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
TOOLS = {
|
|
53
|
+
"read_file": read_file,
|
|
54
|
+
"write_file": write_file,
|
|
55
|
+
"edit_file": edit_file,
|
|
56
|
+
"multi_edit": multi_edit,
|
|
57
|
+
"delete_file": delete_file,
|
|
58
|
+
"list_files": list_files,
|
|
59
|
+
"rollback_file": rollback_file,
|
|
60
|
+
"run_command": run_command,
|
|
61
|
+
"task_output": task_output,
|
|
62
|
+
"search_code": search_code,
|
|
63
|
+
"grep": grep,
|
|
64
|
+
"glob": glob_files,
|
|
65
|
+
"semantic_search": semantic_search,
|
|
66
|
+
"lookup_symbol": lookup_symbol,
|
|
67
|
+
"lookup_imports": lookup_imports,
|
|
68
|
+
"lookup_dependents": lookup_dependents,
|
|
69
|
+
"todo_write": todo_write,
|
|
70
|
+
"todo_read": todo_read,
|
|
71
|
+
"exit_plan_mode": exit_plan_mode,
|
|
72
|
+
"spawn_agent": spawn_agent,
|
|
73
|
+
"spawn_agents_parallel": spawn_agents_parallel,
|
|
74
|
+
"self_critique": self_critique,
|
|
75
|
+
"git_op": git_op,
|
|
76
|
+
"verify_files": verify_files,
|
|
77
|
+
"diagnose_error": diagnose_error,
|
|
78
|
+
"run_tests": run_tests,
|
|
79
|
+
"run_install": run_install,
|
|
80
|
+
"checkpoint": checkpoint,
|
|
81
|
+
"run_task_graph": run_task_graph,
|
|
82
|
+
"add_memory": add_memory,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
READ_ONLY_TOOLS = {
|
|
87
|
+
"read_file",
|
|
88
|
+
"list_files",
|
|
89
|
+
"search_code",
|
|
90
|
+
"grep",
|
|
91
|
+
"glob",
|
|
92
|
+
"semantic_search",
|
|
93
|
+
"lookup_symbol",
|
|
94
|
+
"lookup_imports",
|
|
95
|
+
"lookup_dependents",
|
|
96
|
+
"todo_read",
|
|
97
|
+
"todo_write",
|
|
98
|
+
"task_output",
|
|
99
|
+
"exit_plan_mode",
|
|
100
|
+
"self_critique",
|
|
101
|
+
"verify_files",
|
|
102
|
+
"diagnose_error",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Tools that emit their own visual representation (panel, diff, todo
|
|
107
|
+
# list) and don't need the generic "⎿ first-line preview" tee under
|
|
108
|
+
# their tool header. The agent loop reads this set when deciding
|
|
109
|
+
# whether to emit ``tool_result``.
|
|
110
|
+
SELF_RENDERED_TOOLS = {
|
|
111
|
+
"write_file",
|
|
112
|
+
"edit_file",
|
|
113
|
+
"multi_edit",
|
|
114
|
+
"run_command",
|
|
115
|
+
"todo_write",
|
|
116
|
+
"exit_plan_mode",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Sanity check at import: the spec list and the registry must agree.
|
|
121
|
+
# Drift here would silently advertise non-existent tools to the model
|
|
122
|
+
# (or hide real ones), so we fail fast instead.
|
|
123
|
+
_spec_names = {t["function"]["name"] for t in TOOL_SPECS}
|
|
124
|
+
_registry_names = set(TOOLS.keys())
|
|
125
|
+
if _spec_names != _registry_names:
|
|
126
|
+
missing_specs = _registry_names - _spec_names
|
|
127
|
+
missing_tools = _spec_names - _registry_names
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
"tools registry / specs mismatch — "
|
|
130
|
+
f"in TOOLS but not TOOL_SPECS: {sorted(missing_specs)}; "
|
|
131
|
+
f"in TOOL_SPECS but not TOOLS: {sorted(missing_tools)}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"TOOLS",
|
|
137
|
+
"TOOL_SPECS",
|
|
138
|
+
"READ_ONLY_TOOLS",
|
|
139
|
+
"SELF_RENDERED_TOOLS",
|
|
140
|
+
"RUN_COMMAND_ERROR_MARKER",
|
|
141
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Milestone-checkpoint tool.
|
|
2
|
+
|
|
3
|
+
Lets the agent record "I just finished phase X" markers into the
|
|
4
|
+
persisted session transcript. Useful for long-horizon tasks: the
|
|
5
|
+
operator can scan checkpoints on ``/resume`` and see structured
|
|
6
|
+
milestones instead of scrolling through raw tool calls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from agent.tools._results import err
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def checkpoint(label, summary: str = "", modified_files=None):
|
|
15
|
+
"""Write a milestone marker into the session transcript.
|
|
16
|
+
|
|
17
|
+
``label`` — short title (kebab-case recommended, max 120 chars).
|
|
18
|
+
``summary`` — one paragraph describing what was just completed
|
|
19
|
+
and any decisions worth remembering on resume.
|
|
20
|
+
``modified_files`` — optional list of paths touched by this milestone.
|
|
21
|
+
"""
|
|
22
|
+
if not isinstance(label, str) or not label.strip():
|
|
23
|
+
return err("BAD_ARGS", "checkpoint: label must be a non-empty string")
|
|
24
|
+
|
|
25
|
+
if modified_files is not None and not isinstance(modified_files, list):
|
|
26
|
+
return err("BAD_ARGS", "checkpoint: modified_files must be a list of strings")
|
|
27
|
+
|
|
28
|
+
from agent.persistence import session_store
|
|
29
|
+
store = session_store()
|
|
30
|
+
cp = store.append_checkpoint(
|
|
31
|
+
label=label.strip(),
|
|
32
|
+
summary=str(summary or ""),
|
|
33
|
+
modified_files=[p for p in (modified_files or []) if isinstance(p, str)],
|
|
34
|
+
)
|
|
35
|
+
if cp is None:
|
|
36
|
+
return "(checkpoint not persisted — session store unavailable)"
|
|
37
|
+
return (
|
|
38
|
+
f"checkpoint recorded: {cp['label']!s} at turn {cp['turn_index']}"
|
|
39
|
+
+ (f" ({len(cp['modified_files'])} files)" if cp.get("modified_files") else "")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["checkpoint"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Shared helpers used across tool implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agent.tools._results import TOOL_ERROR_PREFIX, err
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Legacy alias. New code should call ``err("NON_ZERO_EXIT", ...)`` from
|
|
9
|
+
# ``_results``; this name remains so the rest of the codebase (and any
|
|
10
|
+
# saved transcripts) can still detect "a tool error happened" by
|
|
11
|
+
# substring match. Both the new ``[ERROR ...]`` prefix and historical
|
|
12
|
+
# ``[NON_ZERO_EXIT]`` results match it.
|
|
13
|
+
RUN_COMMAND_ERROR_MARKER = TOOL_ERROR_PREFIX
|
|
14
|
+
|
|
15
|
+
# Max chars stored for a single tool result. Head + tail are preserved
|
|
16
|
+
# so both setup context and error tails (stack traces, last-test-failure
|
|
17
|
+
# line) survive trimming. read_file is exempt — it has its own
|
|
18
|
+
# offset/limit pagination.
|
|
19
|
+
TOOL_OUTPUT_BUDGET = 3000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def trim_head_tail(text: str, budget: int = TOOL_OUTPUT_BUDGET) -> str:
|
|
23
|
+
"""Trim long output to head + tail with an elision marker.
|
|
24
|
+
|
|
25
|
+
Returns ``text`` unchanged when under budget. Otherwise keeps the
|
|
26
|
+
first ``budget // 2`` chars and the last ``budget // 2`` chars,
|
|
27
|
+
joined by a single-line marker showing how much was dropped.
|
|
28
|
+
"""
|
|
29
|
+
if len(text) <= budget:
|
|
30
|
+
return text
|
|
31
|
+
half = budget // 2
|
|
32
|
+
dropped = len(text) - 2 * half
|
|
33
|
+
head = text[:half]
|
|
34
|
+
tail = text[-half:]
|
|
35
|
+
return f"{head}\n...[truncated {dropped} chars]...\n{tail}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"RUN_COMMAND_ERROR_MARKER",
|
|
40
|
+
"TOOL_ERROR_PREFIX",
|
|
41
|
+
"TOOL_OUTPUT_BUDGET",
|
|
42
|
+
"err",
|
|
43
|
+
"trim_head_tail",
|
|
44
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Self-critique tool.
|
|
2
|
+
|
|
3
|
+
Lets the agent pause after a non-trivial edit batch and ask a cheap
|
|
4
|
+
reviewer model to grade its own work. The reviewer reads a unified diff
|
|
5
|
+
between the most recent snapshot and the current file (or the diff for
|
|
6
|
+
a list of files) and returns ``[BUG]`` / ``[RISK]`` / ``[SUSPECT]``
|
|
7
|
+
findings — or ``LGTM`` if nothing stands out.
|
|
8
|
+
|
|
9
|
+
Why this is its own tool rather than a hook on every write:
|
|
10
|
+
- Critique costs a model call. Auto-running on every micro-edit would
|
|
11
|
+
burn quota.
|
|
12
|
+
- The agent chooses when it has reached a meaningful checkpoint
|
|
13
|
+
(feature done, refactor finished). That's the right granularity.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import difflib
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from agent import snapshots
|
|
23
|
+
from agent.tools._results import err
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read_text(path: str) -> str | None:
|
|
27
|
+
try:
|
|
28
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
29
|
+
return f.read()
|
|
30
|
+
except (OSError, UnicodeDecodeError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _diff_against_snapshot(path: str) -> tuple[str, str]:
|
|
35
|
+
"""Return ``(diff_text, status)`` for ``path``.
|
|
36
|
+
|
|
37
|
+
``status`` describes what kind of comparison happened: ``ok``,
|
|
38
|
+
``no-snapshot``, ``unreadable``. The diff is empty whenever the file
|
|
39
|
+
contents match the latest snapshot (no change to review).
|
|
40
|
+
"""
|
|
41
|
+
backups = snapshots.list_snapshots(path)
|
|
42
|
+
current = _read_text(path)
|
|
43
|
+
if current is None:
|
|
44
|
+
return "", "unreadable"
|
|
45
|
+
if not backups:
|
|
46
|
+
return "", "no-snapshot"
|
|
47
|
+
|
|
48
|
+
backup_path = backups[0]["backup_path"]
|
|
49
|
+
try:
|
|
50
|
+
with open(backup_path, "r", encoding="utf-8") as f:
|
|
51
|
+
previous = f.read()
|
|
52
|
+
except (OSError, UnicodeDecodeError):
|
|
53
|
+
return "", "unreadable"
|
|
54
|
+
|
|
55
|
+
if previous == current:
|
|
56
|
+
return "", "unchanged"
|
|
57
|
+
|
|
58
|
+
diff = "\n".join(difflib.unified_diff(
|
|
59
|
+
previous.splitlines(),
|
|
60
|
+
current.splitlines(),
|
|
61
|
+
fromfile=f"a/{path}",
|
|
62
|
+
tofile=f"b/{path}",
|
|
63
|
+
lineterm="",
|
|
64
|
+
))
|
|
65
|
+
return diff, "ok"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def self_critique(paths, intent: str = ""):
|
|
69
|
+
"""Ask the reviewer model to grade recent edits to ``paths``.
|
|
70
|
+
|
|
71
|
+
``paths`` may be a single path or a list. Each file is diffed against
|
|
72
|
+
its most recent snapshot; the combined diff is sent to the critic.
|
|
73
|
+
|
|
74
|
+
``intent`` is a one-line description of the goal the edits were
|
|
75
|
+
meant to achieve — gives the reviewer the success criterion to
|
|
76
|
+
measure against.
|
|
77
|
+
"""
|
|
78
|
+
from agent.llm import critique
|
|
79
|
+
|
|
80
|
+
if isinstance(paths, str):
|
|
81
|
+
path_list = [paths]
|
|
82
|
+
elif isinstance(paths, list):
|
|
83
|
+
path_list = [p for p in paths if isinstance(p, str) and p.strip()]
|
|
84
|
+
else:
|
|
85
|
+
return err(
|
|
86
|
+
"BAD_ARGS",
|
|
87
|
+
"self_critique: paths must be a string or list of strings",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not path_list:
|
|
91
|
+
return err("BAD_ARGS", "self_critique: paths is empty")
|
|
92
|
+
|
|
93
|
+
diffs: list[str] = []
|
|
94
|
+
statuses: dict[str, str] = {}
|
|
95
|
+
for p in path_list:
|
|
96
|
+
d, status = _diff_against_snapshot(p)
|
|
97
|
+
statuses[p] = status
|
|
98
|
+
if d:
|
|
99
|
+
diffs.append(d)
|
|
100
|
+
|
|
101
|
+
if not diffs:
|
|
102
|
+
nothing = ", ".join(f"{p}={statuses[p]}" for p in path_list)
|
|
103
|
+
return f"(no diff to review — {nothing})"
|
|
104
|
+
|
|
105
|
+
combined = "\n".join(diffs)
|
|
106
|
+
findings = critique(combined, intent=intent)
|
|
107
|
+
if not findings:
|
|
108
|
+
return "(critique unavailable — proceeding without review)"
|
|
109
|
+
|
|
110
|
+
header = f"critic reviewed {len(diffs)} file(s) vs latest snapshot:"
|
|
111
|
+
return header + "\n" + findings
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["self_critique"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Autonomous-debug helper tool.
|
|
2
|
+
|
|
3
|
+
Lets the agent ask a cheap reviewer model to triage a failed command —
|
|
4
|
+
"what's broken and what should I try next" — without burning the main
|
|
5
|
+
model's context on long error traces. The agent stays in the driver's
|
|
6
|
+
seat: this tool gives a diagnosis, the agent decides what to do.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from agent.tools._results import err
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def diagnose_error(
|
|
15
|
+
command,
|
|
16
|
+
stdout: str = "",
|
|
17
|
+
stderr: str = "",
|
|
18
|
+
exit_code: int = 1,
|
|
19
|
+
context_hint: str = "",
|
|
20
|
+
):
|
|
21
|
+
"""Diagnose a failed command. Returns the model's CAUSE/FIX/CONFIDENCE
|
|
22
|
+
block, or a structured error if inputs are unusable.
|
|
23
|
+
|
|
24
|
+
``command`` — the literal command that ran (string).
|
|
25
|
+
``stdout/stderr`` — captured output; tails are kept, head dropped.
|
|
26
|
+
``exit_code`` — the non-zero exit code.
|
|
27
|
+
``context_hint`` — optional one-liner: what the agent was trying
|
|
28
|
+
to accomplish, so the diagnosis isn't context-free.
|
|
29
|
+
"""
|
|
30
|
+
if not isinstance(command, str) or not command.strip():
|
|
31
|
+
return err("BAD_ARGS", "diagnose_error: command must be a non-empty string")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
ec = int(exit_code)
|
|
35
|
+
except (TypeError, ValueError):
|
|
36
|
+
return err("BAD_ARGS", "diagnose_error: exit_code must be an integer")
|
|
37
|
+
|
|
38
|
+
if ec == 0 and not (stderr or "").strip():
|
|
39
|
+
return "(command appears to have succeeded — nothing to diagnose)"
|
|
40
|
+
|
|
41
|
+
from agent.llm import diagnose_error as _ask
|
|
42
|
+
|
|
43
|
+
findings = _ask(
|
|
44
|
+
command=command.strip(),
|
|
45
|
+
stdout=str(stdout or ""),
|
|
46
|
+
stderr=str(stderr or ""),
|
|
47
|
+
exit_code=ec,
|
|
48
|
+
context_hint=str(context_hint or ""),
|
|
49
|
+
)
|
|
50
|
+
if not findings:
|
|
51
|
+
return "(diagnosis unavailable — proceed manually)"
|
|
52
|
+
return findings
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["diagnose_error"]
|