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.
- python_agent_harness/__init__.py +20 -0
- python_agent_harness/__main__.py +5 -0
- python_agent_harness/agent.py +703 -0
- python_agent_harness/cli.py +273 -0
- python_agent_harness/client.py +832 -0
- python_agent_harness/commands.py +181 -0
- python_agent_harness/config.py +464 -0
- python_agent_harness/context_manager.py +100 -0
- python_agent_harness/diffrender.py +84 -0
- python_agent_harness/mcp/__init__.py +21 -0
- python_agent_harness/mcp/client.py +161 -0
- python_agent_harness/mcp/config.py +130 -0
- python_agent_harness/mcp/manager.py +290 -0
- python_agent_harness/models.py +149 -0
- python_agent_harness/persistence.py +297 -0
- python_agent_harness/planmode.py +112 -0
- python_agent_harness/prompts/agent.md +362 -0
- python_agent_harness/prompts/build-switch.md +5 -0
- python_agent_harness/prompts/commands/explain.md +13 -0
- python_agent_harness/prompts/compact.md +33 -0
- python_agent_harness/prompts/initialize.md +66 -0
- python_agent_harness/prompts/plan-mode.md +70 -0
- python_agent_harness/prompts/plan.md +26 -0
- python_agent_harness/prompts/review.md +100 -0
- python_agent_harness/prompts/subagent.md +208 -0
- python_agent_harness/prompts/summary.md +11 -0
- python_agent_harness/prompts/task-completion-rules.md +50 -0
- python_agent_harness/prompts/title.md +44 -0
- python_agent_harness/prompts.py +498 -0
- python_agent_harness/session.py +781 -0
- python_agent_harness/subagent.py +61 -0
- python_agent_harness/token_estimator.py +125 -0
- python_agent_harness/tool_runner.py +247 -0
- python_agent_harness/tools/__init__.py +56 -0
- python_agent_harness/tools/agent_tool.py +75 -0
- python_agent_harness/tools/base.py +147 -0
- python_agent_harness/tools/bash.py +298 -0
- python_agent_harness/tools/edit.py +272 -0
- python_agent_harness/tools/filesystem.py +180 -0
- python_agent_harness/tools/glob.py +161 -0
- python_agent_harness/tools/grep.py +149 -0
- python_agent_harness/tools/insert.py +61 -0
- python_agent_harness/tools/mcp.py +203 -0
- python_agent_harness/tools/mkdir.py +30 -0
- python_agent_harness/tools/planexit.py +45 -0
- python_agent_harness/tools/question.py +70 -0
- python_agent_harness/tools/read.py +104 -0
- python_agent_harness/tools/skill.py +32 -0
- python_agent_harness/tools/todo.py +60 -0
- python_agent_harness/tools/write.py +56 -0
- python_agent_harness/tui/__init__.py +68 -0
- python_agent_harness/tui/commands.py +652 -0
- python_agent_harness/tui/core.py +385 -0
- python_agent_harness/tui/input.py +412 -0
- python_agent_harness/tui/render.py +535 -0
- python_agent_harness-1.5.0.dist-info/METADATA +251 -0
- python_agent_harness-1.5.0.dist-info/RECORD +61 -0
- python_agent_harness-1.5.0.dist-info/WHEEL +5 -0
- python_agent_harness-1.5.0.dist-info/entry_points.txt +2 -0
- python_agent_harness-1.5.0.dist-info/licenses/LICENSE +21 -0
- python_agent_harness-1.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""CLI entry points: interactive TUI session and configuration.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
run [project] interactive TUI agent session (default)
|
|
5
|
+
config [--init] show effective LLM config / write a template file
|
|
6
|
+
|
|
7
|
+
Custom commands (prompts/commands/*.md) — like init, review,
|
|
8
|
+
sessions, restore and summary/explain — are TUI slash commands only;
|
|
9
|
+
they are NOT registered as CLI subcommands.
|
|
10
|
+
|
|
11
|
+
Configuration (LLM etc.) is read from a JSON file, by default
|
|
12
|
+
~/.config/python-agent-harness/config.json; see `config --init`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from . import config
|
|
22
|
+
from .client import Client
|
|
23
|
+
from .session import Session
|
|
24
|
+
from .tools import default_registry
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_session(
|
|
28
|
+
project_dir: str,
|
|
29
|
+
config_path: str | None = None,
|
|
30
|
+
model: str | None = None,
|
|
31
|
+
stream: bool | None = None,
|
|
32
|
+
) -> Session:
|
|
33
|
+
"""Create a Session from config file + env (no env required).
|
|
34
|
+
|
|
35
|
+
The system prompt defaults to the ported main-agent prompt
|
|
36
|
+
(config.DEFAULT_AGENT_PROMPT_FILE); the sub-agent prompt always
|
|
37
|
+
defaults to config.DEFAULT_SUBAGENT_PROMPT_FILE. Either default
|
|
38
|
+
falls back to no system prompt if its file is unavailable.
|
|
39
|
+
"""
|
|
40
|
+
settings = config.load_llm_config(config_path)
|
|
41
|
+
paths = config.load_paths_config(config_path)
|
|
42
|
+
mcp_config = config.load_mcp_config(config_path)
|
|
43
|
+
model = model or settings["model"]
|
|
44
|
+
# resolve sub-agent overrides against the EFFECTIVE main settings
|
|
45
|
+
# (so a CLI/caller model override is inherited too when the
|
|
46
|
+
# subagent_llm model is unset)
|
|
47
|
+
settings["model"] = model
|
|
48
|
+
subagent_settings = config.load_subagent_llm_config(config_path, main=settings)
|
|
49
|
+
client = Client(
|
|
50
|
+
base_url=settings["base_url"],
|
|
51
|
+
api_key=settings["api_key"],
|
|
52
|
+
model=model,
|
|
53
|
+
timeout=settings["timeout"],
|
|
54
|
+
config_path=config_path,
|
|
55
|
+
)
|
|
56
|
+
# A separate client for sub-agent requests only when a different
|
|
57
|
+
# LLM is configured (mirrors gptel-agent-harness-subagent-model /
|
|
58
|
+
# -backend); otherwise the sub-agent shares the main client.
|
|
59
|
+
subagent_client = None
|
|
60
|
+
if any(
|
|
61
|
+
subagent_settings[k] != settings[k] for k in ("base_url", "api_key", "model", "timeout")
|
|
62
|
+
):
|
|
63
|
+
subagent_client = Client(
|
|
64
|
+
base_url=subagent_settings["base_url"],
|
|
65
|
+
api_key=subagent_settings["api_key"],
|
|
66
|
+
model=subagent_settings["model"],
|
|
67
|
+
timeout=subagent_settings["timeout"],
|
|
68
|
+
config_path=config_path,
|
|
69
|
+
)
|
|
70
|
+
# keep every request of this session (main + sub-agents) in the
|
|
71
|
+
# same LLM log file — the TUI advertises the main client's log
|
|
72
|
+
# path, and a separate sub-agent log would fragment debugging
|
|
73
|
+
subagent_client.log_path = client.log_path
|
|
74
|
+
from .prompts import assemble_agent_prompt, load_agent_prompt
|
|
75
|
+
from .session import find_skill_dir
|
|
76
|
+
|
|
77
|
+
abs_project = os.path.abspath(project_dir)
|
|
78
|
+
skill_dir = find_skill_dir(abs_project, paths.get("skill_path"))
|
|
79
|
+
system_prompt = assemble_agent_prompt(
|
|
80
|
+
abs_project,
|
|
81
|
+
load_agent_prompt(config.DEFAULT_AGENT_PROMPT_FILE, skill_dir=skill_dir),
|
|
82
|
+
context_path=paths.get("context_path"),
|
|
83
|
+
)
|
|
84
|
+
# sub-agents get ONLY their own system prompt — no parent project
|
|
85
|
+
# context and no task-completion rules injected
|
|
86
|
+
subagent_system_prompt = load_agent_prompt(
|
|
87
|
+
config.DEFAULT_SUBAGENT_PROMPT_FILE, skill_dir=skill_dir
|
|
88
|
+
)
|
|
89
|
+
# Resolve the effective stream once: the CLI --no-stream flag wins
|
|
90
|
+
# over the config file for the whole session, sub-agents included
|
|
91
|
+
# (same precedence as the main agent's stream).
|
|
92
|
+
effective_stream = settings["stream"] if stream is None else stream
|
|
93
|
+
model_profiles = config.load_models_config(config_path)
|
|
94
|
+
# Base settings for /model switching: the main llm settings as
|
|
95
|
+
# resolved at session start (incl. the CLI --model/--no-stream
|
|
96
|
+
# overrides above), so a profile's unset keys inherit these
|
|
97
|
+
# instead of values drifted by earlier switches.
|
|
98
|
+
llm_settings = dict(settings)
|
|
99
|
+
llm_settings["stream"] = effective_stream
|
|
100
|
+
return Session(
|
|
101
|
+
project_dir=abs_project,
|
|
102
|
+
client=client,
|
|
103
|
+
model=model,
|
|
104
|
+
backend=settings["backend"],
|
|
105
|
+
system_prompt=system_prompt,
|
|
106
|
+
subagent_system_prompt=subagent_system_prompt,
|
|
107
|
+
temperature=settings["temperature"],
|
|
108
|
+
max_tokens=settings["max_tokens"],
|
|
109
|
+
reasoning_effort=settings["reasoning_effort"],
|
|
110
|
+
stream=effective_stream,
|
|
111
|
+
subagent_client=subagent_client,
|
|
112
|
+
subagent_temperature=subagent_settings["temperature"],
|
|
113
|
+
subagent_max_tokens=subagent_settings["max_tokens"],
|
|
114
|
+
subagent_reasoning_effort=subagent_settings["reasoning_effort"],
|
|
115
|
+
subagent_stream=(effective_stream if stream is not None else subagent_settings["stream"]),
|
|
116
|
+
registry=default_registry(),
|
|
117
|
+
context_path=paths.get("context_path"),
|
|
118
|
+
skill_path=paths.get("skill_path"),
|
|
119
|
+
mcp=mcp_config,
|
|
120
|
+
model_profiles=model_profiles,
|
|
121
|
+
llm_settings=llm_settings,
|
|
122
|
+
config_path=config_path,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def make_session_with_mcp(
|
|
127
|
+
project_dir: str,
|
|
128
|
+
config_path: str | None = None,
|
|
129
|
+
model: str | None = None,
|
|
130
|
+
stream: bool | None = None,
|
|
131
|
+
) -> Session:
|
|
132
|
+
"""Create a Session and connect its configured MCP servers.
|
|
133
|
+
|
|
134
|
+
Wraps ``make_session``: the session's MCP servers are connected and
|
|
135
|
+
their tools registered before the session is returned (discovery
|
|
136
|
+
happens once, at session start). Per-server failures are printed
|
|
137
|
+
to stderr and never prevent the session from running — the agent
|
|
138
|
+
keeps working with the built-in tools.
|
|
139
|
+
"""
|
|
140
|
+
session = make_session(project_dir, config_path=config_path, model=model, stream=stream)
|
|
141
|
+
failures = session.connect_mcp()
|
|
142
|
+
for server, err in failures:
|
|
143
|
+
print(f"python-agent-harness: [{server}] {err}", file=sys.stderr)
|
|
144
|
+
return session
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
148
|
+
from .tui import Tui
|
|
149
|
+
|
|
150
|
+
project_dir = getattr(args, "project", None) or os.getcwd()
|
|
151
|
+
session = make_session_with_mcp(
|
|
152
|
+
project_dir,
|
|
153
|
+
config_path=args.config,
|
|
154
|
+
stream=False if getattr(args, "no_stream", False) else None,
|
|
155
|
+
)
|
|
156
|
+
Tui(session).run()
|
|
157
|
+
session.close()
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_config(args: argparse.Namespace) -> int:
|
|
162
|
+
path = config._config_path(args.path)
|
|
163
|
+
if args.init:
|
|
164
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
if path.exists() and not args.force:
|
|
166
|
+
print(f"config already exists: {path} (use --force to overwrite)")
|
|
167
|
+
return 1
|
|
168
|
+
template = config.CONFIG_TEMPLATE.format(path=path)
|
|
169
|
+
path.write_text(template, encoding="utf-8")
|
|
170
|
+
print(f"wrote config template: {path}")
|
|
171
|
+
return 0
|
|
172
|
+
settings = config.load_llm_config(args.path)
|
|
173
|
+
subagent_settings = config.load_subagent_llm_config(args.path, main=settings)
|
|
174
|
+
paths = config.load_paths_config(args.path)
|
|
175
|
+
mcp_config = config.load_mcp_config(args.path)
|
|
176
|
+
print(f"config file: {path}")
|
|
177
|
+
if not path.exists():
|
|
178
|
+
print("(file does not exist yet — run `python-agent-harness config --init` to create it)")
|
|
179
|
+
for key in ("base_url", "model", "backend"):
|
|
180
|
+
print(f"{key}: {settings[key]}")
|
|
181
|
+
print(f"api_key: {config.mask_secret(settings['api_key'])}")
|
|
182
|
+
print(f"temperature: {settings['temperature']}")
|
|
183
|
+
print(f"max_tokens: {settings['max_tokens']}")
|
|
184
|
+
print(f"reasoning_effort: {settings['reasoning_effort']}")
|
|
185
|
+
print(f"stream: {settings['stream']}")
|
|
186
|
+
print(f"timeout: {settings['timeout']}")
|
|
187
|
+
print(f"context_path: {paths['context_path'] or '(default: <project>/contexts)'}")
|
|
188
|
+
print(f"skill_path: {paths['skill_path'] or '(default: <project>/skills)'}")
|
|
189
|
+
if mcp_config.servers:
|
|
190
|
+
for name, server in mcp_config.servers.items():
|
|
191
|
+
status = "enabled" if server.enabled else "disabled"
|
|
192
|
+
target = (
|
|
193
|
+
f"command={server.command} {' '.join(server.args)}"
|
|
194
|
+
if server.transport == "stdio"
|
|
195
|
+
else f"url={server.url}"
|
|
196
|
+
)
|
|
197
|
+
print(
|
|
198
|
+
f"mcp server {name}: {status}, transport={server.transport}, "
|
|
199
|
+
f"{target}, parallel={server.parallel}"
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
print('mcp: (none configured — add an "mcp" section to the config file)')
|
|
203
|
+
print(
|
|
204
|
+
"subagent_llm: (inherits main)"
|
|
205
|
+
if subagent_settings == settings
|
|
206
|
+
else f"subagent_llm: model={subagent_settings['model']} "
|
|
207
|
+
f"base_url={subagent_settings['base_url']} "
|
|
208
|
+
f"api_key={config.mask_secret(subagent_settings['api_key'])} "
|
|
209
|
+
f"temperature={subagent_settings['temperature']} "
|
|
210
|
+
f"max_tokens={subagent_settings['max_tokens']} "
|
|
211
|
+
f"reasoning_effort={subagent_settings['reasoning_effort']} "
|
|
212
|
+
f"stream={subagent_settings['stream']} timeout={subagent_settings['timeout']}"
|
|
213
|
+
)
|
|
214
|
+
# Show model profiles for /model command
|
|
215
|
+
model_profiles = config.load_models_config(args.path)
|
|
216
|
+
if model_profiles:
|
|
217
|
+
print("models:")
|
|
218
|
+
for name, profile in sorted(model_profiles.items()):
|
|
219
|
+
model_name = profile.get("model", "(inherited)")
|
|
220
|
+
base_url = profile.get("base_url", "(inherited)")
|
|
221
|
+
print(f" {name}: model={model_name}, base_url={base_url}")
|
|
222
|
+
else:
|
|
223
|
+
print("models: (none configured — add a 'models' section to use /model)")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _add_config_arg(parser: argparse.ArgumentParser, suppress: bool = False) -> None:
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"--config",
|
|
230
|
+
metavar="PATH",
|
|
231
|
+
default=argparse.SUPPRESS if suppress else None,
|
|
232
|
+
help="path to config.json (default: ~/.config/python-agent-harness/config.json)",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
237
|
+
parser = argparse.ArgumentParser(
|
|
238
|
+
prog="python-agent-harness",
|
|
239
|
+
description="Python agent execution harness (gptel-agent-harness port)",
|
|
240
|
+
)
|
|
241
|
+
_add_config_arg(parser)
|
|
242
|
+
sub = parser.add_subparsers(dest="command")
|
|
243
|
+
|
|
244
|
+
p_run = sub.add_parser("run", help="interactive TUI agent session")
|
|
245
|
+
_add_config_arg(p_run, suppress=True)
|
|
246
|
+
p_run.add_argument(
|
|
247
|
+
"--no-stream",
|
|
248
|
+
action="store_true",
|
|
249
|
+
help="disable streaming (one-shot responses; overrides config file)",
|
|
250
|
+
)
|
|
251
|
+
p_run.add_argument("project", nargs="?", help="project directory (default: cwd)")
|
|
252
|
+
|
|
253
|
+
p_config = sub.add_parser("config", help="show effective LLM config or write a template file")
|
|
254
|
+
p_config.add_argument("--init", action="store_true", help="write a config template")
|
|
255
|
+
p_config.add_argument("--force", action="store_true", help="overwrite an existing file")
|
|
256
|
+
p_config.add_argument("--path", metavar="PATH", help="config file path")
|
|
257
|
+
p_config.set_defaults(func=cmd_config)
|
|
258
|
+
return parser
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main(argv: list[str] | None = None) -> int:
|
|
262
|
+
parser = build_parser()
|
|
263
|
+
args = parser.parse_args(argv)
|
|
264
|
+
if args.command in (None, "run"):
|
|
265
|
+
return cmd_run(args)
|
|
266
|
+
if args.command == "config":
|
|
267
|
+
return cmd_config(args)
|
|
268
|
+
parser.print_help()
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
sys.exit(main())
|