python-agent-harness 1.5.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 (61) hide show
  1. python_agent_harness/__init__.py +20 -0
  2. python_agent_harness/__main__.py +5 -0
  3. python_agent_harness/agent.py +703 -0
  4. python_agent_harness/cli.py +273 -0
  5. python_agent_harness/client.py +832 -0
  6. python_agent_harness/commands.py +181 -0
  7. python_agent_harness/config.py +464 -0
  8. python_agent_harness/context_manager.py +100 -0
  9. python_agent_harness/diffrender.py +84 -0
  10. python_agent_harness/mcp/__init__.py +21 -0
  11. python_agent_harness/mcp/client.py +161 -0
  12. python_agent_harness/mcp/config.py +130 -0
  13. python_agent_harness/mcp/manager.py +290 -0
  14. python_agent_harness/models.py +149 -0
  15. python_agent_harness/persistence.py +297 -0
  16. python_agent_harness/planmode.py +112 -0
  17. python_agent_harness/prompts/agent.md +362 -0
  18. python_agent_harness/prompts/build-switch.md +5 -0
  19. python_agent_harness/prompts/commands/explain.md +13 -0
  20. python_agent_harness/prompts/compact.md +33 -0
  21. python_agent_harness/prompts/initialize.md +66 -0
  22. python_agent_harness/prompts/plan-mode.md +70 -0
  23. python_agent_harness/prompts/plan.md +26 -0
  24. python_agent_harness/prompts/review.md +100 -0
  25. python_agent_harness/prompts/subagent.md +208 -0
  26. python_agent_harness/prompts/summary.md +11 -0
  27. python_agent_harness/prompts/task-completion-rules.md +50 -0
  28. python_agent_harness/prompts/title.md +44 -0
  29. python_agent_harness/prompts.py +498 -0
  30. python_agent_harness/session.py +781 -0
  31. python_agent_harness/subagent.py +61 -0
  32. python_agent_harness/token_estimator.py +125 -0
  33. python_agent_harness/tool_runner.py +247 -0
  34. python_agent_harness/tools/__init__.py +56 -0
  35. python_agent_harness/tools/agent_tool.py +75 -0
  36. python_agent_harness/tools/base.py +147 -0
  37. python_agent_harness/tools/bash.py +298 -0
  38. python_agent_harness/tools/edit.py +272 -0
  39. python_agent_harness/tools/filesystem.py +180 -0
  40. python_agent_harness/tools/glob.py +161 -0
  41. python_agent_harness/tools/grep.py +149 -0
  42. python_agent_harness/tools/insert.py +61 -0
  43. python_agent_harness/tools/mcp.py +203 -0
  44. python_agent_harness/tools/mkdir.py +30 -0
  45. python_agent_harness/tools/planexit.py +45 -0
  46. python_agent_harness/tools/question.py +70 -0
  47. python_agent_harness/tools/read.py +104 -0
  48. python_agent_harness/tools/skill.py +32 -0
  49. python_agent_harness/tools/todo.py +60 -0
  50. python_agent_harness/tools/write.py +56 -0
  51. python_agent_harness/tui/__init__.py +68 -0
  52. python_agent_harness/tui/commands.py +652 -0
  53. python_agent_harness/tui/core.py +385 -0
  54. python_agent_harness/tui/input.py +412 -0
  55. python_agent_harness/tui/render.py +535 -0
  56. python_agent_harness-1.5.0.dist-info/METADATA +251 -0
  57. python_agent_harness-1.5.0.dist-info/RECORD +61 -0
  58. python_agent_harness-1.5.0.dist-info/WHEEL +5 -0
  59. python_agent_harness-1.5.0.dist-info/entry_points.txt +2 -0
  60. python_agent_harness-1.5.0.dist-info/licenses/LICENSE +21 -0
  61. python_agent_harness-1.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,32 @@
1
+ """Skill tool: load a skill into the current conversation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from .base import Tool, ToolContext
8
+
9
+
10
+ class Skill(Tool):
11
+ name = "Skill"
12
+ description = (
13
+ "Load a skill to get detailed instructions for a specific task. "
14
+ "Use this when a task matches an available skill's description. "
15
+ "Invoke with the skill name and optional args."
16
+ )
17
+ parameters = {
18
+ "type": "object",
19
+ "properties": {
20
+ "skill": {"type": "string", "description": "Name of the skill to load"},
21
+ "args": {"type": "string", "description": "Optional arguments for the skill"},
22
+ },
23
+ "required": ["skill"],
24
+ }
25
+
26
+ def run(self, args: dict, ctx: ToolContext) -> str:
27
+ name = args.get("skill", "")
28
+ skill_path = ctx.find_skill(name)
29
+ if not skill_path:
30
+ return f"Error: skill {name!r} not found"
31
+ text = Path(skill_path).read_text(encoding="utf-8", errors="replace")
32
+ return f"[Skill: {name}]\n\n{text}"
@@ -0,0 +1,60 @@
1
+ """TodoWrite tool: maintain a structured task list during execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from .base import Tool, ToolContext
8
+
9
+ DESCRIPTION = (
10
+ "Create and manage a structured task list for your session. Helps track "
11
+ "progress on complex, multi-step tasks.\n\n"
12
+ "Use it when:\n"
13
+ "- The task has 3+ distinct steps or phases\n"
14
+ "- The task is non-trivial and benefits from planning\n"
15
+ "- You start a task (mark it in_progress) or finish a task (mark it completed)\n\n"
16
+ "Task states: pending (not started), in_progress (currently working on), "
17
+ "completed (finished). Only one task can be in_progress at a time. Send "
18
+ "the entire todo list with each call (not just changed items)."
19
+ )
20
+
21
+ PARAMETERS = {
22
+ "type": "object",
23
+ "properties": {
24
+ "todos": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "object",
28
+ "properties": {
29
+ "content": {
30
+ "type": "string",
31
+ "minLength": 1,
32
+ "description": "Task description (imperative form)",
33
+ },
34
+ "status": {
35
+ "type": "string",
36
+ "enum": ["pending", "in_progress", "completed"],
37
+ },
38
+ "activeForm": {
39
+ "type": "string",
40
+ "minLength": 1,
41
+ "description": "Present continuous form",
42
+ },
43
+ },
44
+ "required": ["content", "status"],
45
+ },
46
+ }
47
+ },
48
+ "required": ["todos"],
49
+ }
50
+
51
+
52
+ class TodoWrite(Tool):
53
+ name = "TodoWrite"
54
+ description = DESCRIPTION
55
+ parameters = PARAMETERS
56
+
57
+ def run(self, args: dict, ctx: ToolContext) -> str:
58
+ todos = args.get("todos") or []
59
+ ctx.update_todos(todos)
60
+ return json.dumps({"todos": todos, "count": len(todos)}, ensure_ascii=False)
@@ -0,0 +1,56 @@
1
+ """Write tool: create/overwrite a file, recording a diff for the UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from ..diffrender import unified_diff
8
+ from .base import Tool, ToolContext
9
+
10
+
11
+ class Write(Tool):
12
+ name = "Write"
13
+ description = (
14
+ "Create a new file with the given content. Overwrites an existing file — use with care!"
15
+ )
16
+ parameters = {
17
+ "type": "object",
18
+ "properties": {
19
+ "path": {"type": "string", "description": "Directory for the file"},
20
+ "filename": {"type": "string", "description": "File name"},
21
+ "content": {"type": "string", "description": "Full file content"},
22
+ },
23
+ "required": ["path", "filename", "content"],
24
+ }
25
+
26
+ def run(self, args: dict, ctx: ToolContext) -> str:
27
+ dir_path = args.get("path") or "."
28
+ filename = args.get("filename") or ""
29
+ content = args.get("content") or ""
30
+ # LLM may put the full file path in "filename" or in "path"
31
+ if filename:
32
+ path = os.path.realpath(os.path.abspath(os.path.join(dir_path, filename)))
33
+ else:
34
+ path = os.path.realpath(os.path.abspath(dir_path))
35
+ if not filename:
36
+ filename = os.path.basename(path) or os.path.basename(dir_path)
37
+ existed = os.path.exists(path)
38
+ old_content = ""
39
+ if existed:
40
+ try:
41
+ with open(path, encoding="utf-8") as f:
42
+ old_content = f.read()
43
+ except OSError:
44
+ old_content = ""
45
+ try:
46
+ parent = os.path.dirname(path)
47
+ if parent:
48
+ os.makedirs(parent, exist_ok=True)
49
+ with open(path, "w", encoding="utf-8") as f:
50
+ f.write(content)
51
+ except OSError as e:
52
+ return f"Error: {e}"
53
+ diff_text = unified_diff(old_content, content, path)
54
+ if diff_text:
55
+ ctx.record_diff(diff_text)
56
+ return f"Created file {filename} in {dir_path}"
@@ -0,0 +1,68 @@
1
+ """TUI package for the agent harness.
2
+
3
+ Re-exports the public API so that ``from python_agent_harness.tui import Tui``
4
+ and other imports continue to work unchanged after the split into
5
+ submodules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .commands import CommandMixin
11
+ from .core import Tui
12
+ from .input import (
13
+ SLASH_COMMANDS,
14
+ SlashCompleter,
15
+ UiQuestion,
16
+ _custom_slash_commands,
17
+ _history_path,
18
+ _make_key_bindings,
19
+ _make_prompt_session,
20
+ _resolve_keyed_choice,
21
+ _resolve_numbered_choice,
22
+ )
23
+ from .render import (
24
+ _FC_DANGLING_RE,
25
+ _FC_HEADER,
26
+ _FC_LABEL,
27
+ _FINAL_CHECK_RE,
28
+ ASSISTANT_STYLE,
29
+ SPINNER_FRAMES,
30
+ USER_STYLE,
31
+ _head_chars,
32
+ _head_lines,
33
+ _is_injected_user_text,
34
+ _strip_final_check,
35
+ _strip_reasoning,
36
+ _tail_chars,
37
+ _tail_lines,
38
+ _tool_result_preview,
39
+ )
40
+
41
+ __all__ = [
42
+ "CommandMixin",
43
+ "Tui",
44
+ "UiQuestion",
45
+ "SlashCompleter",
46
+ "SLASH_COMMANDS",
47
+ "SPINNER_FRAMES",
48
+ "USER_STYLE",
49
+ "ASSISTANT_STYLE",
50
+ "_resolve_keyed_choice",
51
+ "_resolve_numbered_choice",
52
+ "_make_key_bindings",
53
+ "_make_prompt_session",
54
+ "_history_path",
55
+ "_custom_slash_commands",
56
+ "_strip_final_check",
57
+ "_strip_reasoning",
58
+ "_is_injected_user_text",
59
+ "_tail_lines",
60
+ "_tail_chars",
61
+ "_head_lines",
62
+ "_head_chars",
63
+ "_tool_result_preview",
64
+ "_FINAL_CHECK_RE",
65
+ "_FC_DANGLING_RE",
66
+ "_FC_HEADER",
67
+ "_FC_LABEL",
68
+ ]