Skip to content

Commit 01e58c8

Browse files
committed
Split cli/_handlers into a sub-package with agent-run logic extracted to _agent.py
- Promote _handlers.py to _handlers/ package, extracting agent-run lifecycle functions (execute_agent_task, resume, preflight, status, runs, show, approval handler) into _agent.py
1 parent ac563a2 commit 01e58c8

3 files changed

Lines changed: 236 additions & 214 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to TeaAgent are tracked here.
88
- Split `teaagent/mcp_http.py` (575 → ~400 lines) by extracting OAuth endpoint handlers to `_oauth.py`.
99
- Split `teaagent/telemetry.py` into a `teaagent/telemetry/` package with focused modules: `_availability.py`, `_config.py`, `_audit.py`, `_metrics.py`, and `_transport.py`.
1010
- Split `teaagent/code_mode.py` into a `teaagent/code_mode/` package with focused modules: `_types.py`, `_validation.py`, `_child_process.py`, and `_container.py`.
11+
- Split `teaagent/cli/_handlers.py` into a `teaagent/cli/_handlers/` package and extracted agent-run lifecycle logic into `_agent.py` while preserving command handler imports.
1112
- Made `teaagent.cli.main()` accept injectable `_adapter_factory`, `_serve_mcp_http`, `_check_graphqlite`, `_check_llm`, and `_run_model_conformance` keyword arguments, enabling handler extraction without breaking existing tests.
1213
- Split `teaagent/workspace_tools.py` into a `teaagent/workspace_tools/` package with four focused modules: `_config.py`, `_helpers.py`, `_shell.py`, `_files.py`. Backward-compatible public imports preserved via `__init__.py` re-exports.
1314
- Expanded audit string redaction with patterns for JWT tokens, AWS access keys (`AKIA...`), and GitHub personal access tokens (`ghp_...`, `github_pat_...`).
Lines changed: 22 additions & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,39 @@
44
import json
55
import sys
66
import time
7-
from typing import Any, Optional
7+
from typing import Any
88

9-
from teaagent.chat_agent import ChatAgentConfig, run_chat_agent
10-
from teaagent.graphqlite_store import (
11-
GraphQLiteConfig,
12-
GraphQLiteGraphStore,
13-
)
14-
from teaagent.intent import build_task_spec, clarify_task
15-
from teaagent.llm import (
16-
LLMMessage,
17-
LLMRequest,
18-
available_providers,
19-
)
9+
from teaagent.graphqlite_store import GraphQLiteConfig, GraphQLiteGraphStore
10+
from teaagent.intent import clarify_task
11+
from teaagent.llm import LLMMessage, LLMRequest, available_providers
2012
from teaagent.mcp_http import is_loopback_host
2113
from teaagent.mcp_server import serve_mcp_stdio
2214
from teaagent.memory import MemoryCatalog
2315
from teaagent.model_routing import route_model
2416
from teaagent.policy import parse_permission_mode
25-
from teaagent.preflight import preflight
2617
from teaagent.run_store import RunStore
27-
from teaagent.runner import ApprovalRequest, RunResult
2818
from teaagent.tui import run_tui
2919
from teaagent.ultrawork import UltraworkStore
3020
from teaagent.workspace_tools import build_workspace_tool_registry
3121

32-
# ---------------------------------------------------------------------------
33-
# Command handlers
34-
# ---------------------------------------------------------------------------
22+
from ._agent import (
23+
agent_preflight_command as agent_preflight_command,
24+
)
25+
from ._agent import (
26+
agent_resume_command as agent_resume_command,
27+
)
28+
from ._agent import (
29+
agent_run_show as agent_run_show,
30+
)
31+
from ._agent import (
32+
agent_run_task as agent_run_task,
33+
)
34+
from ._agent import (
35+
agent_runs_list as agent_runs_list,
36+
)
37+
from ._agent import (
38+
agent_status_command as agent_status_command,
39+
)
3540

3641

3742
def doctor_graphqlite(args: argparse.Namespace) -> int:
@@ -87,203 +92,6 @@ def memory_show_command(args: argparse.Namespace) -> int:
8792
return 0
8893

8994

90-
def agent_run_task(args: argparse.Namespace) -> int:
91-
return _execute_agent_task(args, args.task)
92-
93-
94-
def agent_resume_command(args: argparse.Namespace) -> int:
95-
store = RunStore(args.root)
96-
try:
97-
original_task = store.task_for_run(args.run_id)
98-
except (FileNotFoundError, ValueError) as exc:
99-
print_json({'status': 'error', 'message': str(exc)})
100-
return 1
101-
102-
initial_observations: list[dict[str, Any]] = []
103-
auto_approved: Optional[str] = None
104-
if not args.fresh_restart:
105-
initial_observations = store.observations_for_run(args.run_id)
106-
pending = store.pending_approval_for_run(args.run_id)
107-
if pending and pending['call_id'] not in args.approve_call_id:
108-
args.approve_call_id = list(args.approve_call_id) + [pending['call_id']]
109-
auto_approved = pending['call_id']
110-
111-
return _execute_agent_task(
112-
args,
113-
original_task,
114-
resumed_from=args.run_id,
115-
initial_observations=initial_observations,
116-
auto_approved_call_id=auto_approved,
117-
)
118-
119-
120-
def _execute_agent_task(
121-
args: argparse.Namespace,
122-
task: str,
123-
*,
124-
resumed_from: Optional[str] = None,
125-
initial_observations: Optional[list[dict[str, Any]]] = None,
126-
auto_approved_call_id: Optional[str] = None,
127-
) -> int:
128-
task_spec = None
129-
if args.clarify:
130-
clarification = clarify_task(task)
131-
if clarification.needs_clarification:
132-
print_json(
133-
{
134-
'status': 'needs_clarification',
135-
'clarification': clarification.to_dict(),
136-
}
137-
)
138-
return 2
139-
task_spec = build_task_spec(task, clarification)
140-
141-
routing = (
142-
route_model(task, provider=args.provider, model=args.model)
143-
if args.route_model
144-
else None
145-
)
146-
selected_model = routing.model if routing else args.model
147-
adapter = args._adapter_factory(args.provider, model=selected_model) # type: ignore[attr-defined]
148-
store = RunStore(args.root)
149-
audit = store.audit_logger()
150-
151-
# --- OpenTelemetry wiring ---
152-
_telemetry_sink = None
153-
if getattr(args, 'telemetry_otlp_endpoint', None) or getattr(
154-
args, 'telemetry_console', False
155-
):
156-
try:
157-
from teaagent.telemetry import (
158-
TelemetryConfig,
159-
TracingHTTPTransport,
160-
configure_telemetry,
161-
)
162-
163-
cfg = TelemetryConfig(
164-
service_name=getattr(args, 'telemetry_service_name', 'teaagent'),
165-
otlp_endpoint=getattr(args, 'telemetry_otlp_endpoint', None),
166-
console=getattr(args, 'telemetry_console', False),
167-
)
168-
_telemetry_sink, tracer = configure_telemetry(cfg)
169-
audit.add_sink(_telemetry_sink.handle_event)
170-
adapter = args._adapter_factory( # type: ignore[attr-defined]
171-
args.provider,
172-
model=selected_model,
173-
transport=TracingHTTPTransport(adapter.transport, tracer), # type: ignore[attr-defined]
174-
)
175-
except Exception as exc:
176-
print(f'Telemetry setup failed: {exc}', file=sys.stderr)
177-
# --- end telemetry wiring ---
178-
179-
approval_handler = cli_approval_handler if args.hitl_approval else None
180-
result = run_chat_agent(
181-
task=task,
182-
adapter=adapter,
183-
config=ChatAgentConfig.from_root(
184-
args.root,
185-
max_iterations=args.max_iterations,
186-
max_tool_calls=args.max_tool_calls,
187-
allow_destructive=args.allow_destructive,
188-
model=selected_model,
189-
permission_mode=parse_permission_mode(args.permission_mode),
190-
approved_call_ids=frozenset(args.approve_call_id),
191-
enable_subagent=args.subagent,
192-
max_subagent_depth=args.max_subagent_depth,
193-
heartbeat_seconds=args.heartbeat,
194-
approval_handler=approval_handler,
195-
),
196-
audit=audit,
197-
task_spec=task_spec,
198-
initial_observations=initial_observations,
199-
)
200-
store.logger_for_result(result, audit)
201-
if _telemetry_sink is not None:
202-
from contextlib import suppress
203-
204-
with suppress(Exception):
205-
_telemetry_sink.force_flush()
206-
payload = run_result_payload(result, routing=routing.to_dict() if routing else None)
207-
if resumed_from:
208-
payload['resumed_from'] = resumed_from
209-
payload['task'] = task
210-
if initial_observations:
211-
payload['replayed_observations'] = len(initial_observations)
212-
if auto_approved_call_id is not None:
213-
payload['auto_approved_call_id'] = auto_approved_call_id
214-
print_json(payload)
215-
return 0 if result.status == 'completed' else 1
216-
217-
218-
def run_result_payload(
219-
result: RunResult, *, routing: Optional[dict[str, Any]]
220-
) -> dict[str, Any]:
221-
payload: dict[str, Any] = {
222-
'run_id': result.run_id,
223-
'status': result.status,
224-
'iterations': result.iterations,
225-
'tool_calls': result.tool_calls,
226-
'routing': routing,
227-
'final_answer': result.final_answer.content if result.final_answer else None,
228-
}
229-
if 'approval' in result.metadata:
230-
payload['approval'] = result.metadata['approval']
231-
return payload
232-
233-
234-
def cli_approval_handler(request: ApprovalRequest) -> bool:
235-
print(
236-
json.dumps(
237-
{'status': 'approval_required', 'approval': request.to_dict()},
238-
sort_keys=True,
239-
),
240-
file=sys.stderr,
241-
)
242-
print(
243-
f'Approve destructive tool call {request.call_id} ({request.tool_name})? [y/N] ',
244-
end='',
245-
file=sys.stderr,
246-
)
247-
answer = input()
248-
return answer.strip().lower() in {'y', 'yes'}
249-
250-
251-
def agent_preflight_command(args: argparse.Namespace) -> int:
252-
report = preflight(
253-
args.task,
254-
root=args.root,
255-
provider=args.provider,
256-
model=args.model,
257-
permission_mode=parse_permission_mode(args.permission_mode),
258-
route=args.route_model,
259-
memory_limit=args.memory_limit,
260-
)
261-
print_json(report.to_dict())
262-
return 0 if report.to_dict()['ready'] else 2
263-
264-
265-
def agent_status_command(args: argparse.Namespace) -> int:
266-
store = RunStore(args.root)
267-
try:
268-
print_json(store.heartbeat_for_run(args.run_id))
269-
except FileNotFoundError as exc:
270-
print_json({'status': 'error', 'message': str(exc)})
271-
return 1
272-
return 0
273-
274-
275-
def agent_runs_list(args: argparse.Namespace) -> int:
276-
store = RunStore(args.root)
277-
print_json([summary.to_dict() for summary in store.list_runs(limit=args.limit)])
278-
return 0
279-
280-
281-
def agent_run_show(args: argparse.Namespace) -> int:
282-
store = RunStore(args.root)
283-
print_json(store.show_run(args.run_id))
284-
return 0
285-
286-
28795
def doctor_model(args: argparse.Namespace) -> int:
28896
ok, message = args._check_llm(args.provider) # type: ignore[attr-defined]
28997
print(

0 commit comments

Comments
 (0)