tamfis-code 0.6.13__tar.gz → 0.6.14__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.
- {tamfis_code-0.6.13/tamfis_code.egg-info → tamfis_code-0.6.14}/PKG-INFO +1 -1
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/pyproject.toml +1 -1
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/__init__.py +1 -1
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/agents.py +2 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/enforcer.py +2 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/interactive.py +75 -22
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/live_input.py +55 -7
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/engine.py +18 -13
- tamfis_code-0.6.14/tamfis_code/orchestrator/planner.py +871 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/render.py +15 -5
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/runner_local.py +427 -33
- {tamfis_code-0.6.13 → tamfis_code-0.6.14/tamfis_code.egg-info}/PKG-INFO +1 -1
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/SOURCES.txt +1 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_github_command_surface.py +5 -2
- tamfis_code-0.6.14/tests/test_intelligent_planner.py +304 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_live_input.py +116 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_orchestrator.py +7 -2
- tamfis_code-0.6.14/tests/test_workspace_scope.py +497 -0
- tamfis_code-0.6.13/tamfis_code/orchestrator/planner.py +0 -195
- tamfis_code-0.6.13/tests/test_workspace_scope.py +0 -201
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/MANIFEST.in +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/README.md +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/USAGE_INSTALL_RELEASE.md +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/setup.cfg +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/__main__.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/agent_definitions.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/api_client.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/cli.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/clipboard.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/completion.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/config.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/custom_commands.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/doctor.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/evidence.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/github_commands.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/hooks.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/indexer.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/instructions.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/local_chat.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/local_tools.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/mcp.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/metrics.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/model_registry.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/__init__.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/agent.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/agent_server.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/automation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/compat.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/conversation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/delegation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/events.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/leases.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/security.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/skills.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/tools.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/workspace.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/__init__.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/context.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/protocols.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/validator.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/project.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/provider_protocols.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/providers.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/pty.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/references.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/routing.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/runner.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/safety.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/screenshot.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/state.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/swarm.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/tasks.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/tool_policy.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/workspace.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/dependency_links.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/entry_points.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/requires.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/top_level.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_agent_definitions.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_agents.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_agents_delegation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_build_installation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_cli_commands.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_clipboard.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_completion.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_context_rollover.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_custom_commands.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_cwd_validation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_doctor_session_diagnostics.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_enforcer.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_fake_tool_detection.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_find_references.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_hooks.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_indexer.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_instructions.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_integration_new.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_interactive_standalone.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_local_pty.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp_search_bounds.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp_standalone_degradation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_metrics.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_openhands_runtime.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_openrouter_cost_routing.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_parity_contracts.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_plan_mode_gate.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_plan_steps_and_attempts.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_provider_protocols.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_reasoning_effort_capability.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_reasoning_plan.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_references.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_render_task_diagnostics.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_routing.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_runner_helpers.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_runner_local.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_safety.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_safety_manifest.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_swarm.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_api_client.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval_flow.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_config.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_intent.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_render.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_repl_exit.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_tasks.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_workspace.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_version_consistency.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tamfis-code
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.14
|
|
4
4
|
Summary: TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals
|
|
5
5
|
Project-URL: Homepage, https://github.com/tamfitronics/tamfis-code
|
|
6
6
|
Project-URL: Repository, https://github.com/tamfitronics/tamfis-code
|
|
@@ -17,7 +17,7 @@ See docs/REMOTE_AGENT_MASTER_SPEC.md, Phase 21, for the original --remote
|
|
|
17
17
|
architecture's spec.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
__version__ = "0.6.
|
|
20
|
+
__version__ = "0.6.14"
|
|
21
21
|
|
|
22
22
|
# Bumped whenever a CLI release requires a minimum backend Remote API
|
|
23
23
|
# contract version. Only meaningful for --remote; the standalone path has no
|
|
@@ -20,6 +20,8 @@ except ImportError:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class TestEnforcer:
|
|
23
|
+
# Application component, not a pytest test container.
|
|
24
|
+
__test__ = False
|
|
23
25
|
"""Detects and runs the current workspace's own test/build commands."""
|
|
24
26
|
|
|
25
27
|
def __init__(self, workspace_root: Optional[Path] = None):
|
|
@@ -6,6 +6,7 @@ management.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import re
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Optional
|
|
@@ -43,7 +44,13 @@ from .custom_commands import (
|
|
|
43
44
|
from .doctor import run_doctor
|
|
44
45
|
from .live_input import LiveInputListener
|
|
45
46
|
from .render import StreamRenderer, print_banner, print_error, print_recent_thread, print_resume_plan_status, print_unified_diff
|
|
46
|
-
from .runner import
|
|
47
|
+
from .runner import (
|
|
48
|
+
TaskOutcome,
|
|
49
|
+
resolve_approval_decision_async,
|
|
50
|
+
retry_task_and_stream,
|
|
51
|
+
run_ai_task_and_stream,
|
|
52
|
+
run_shell_command,
|
|
53
|
+
)
|
|
47
54
|
from .runner_local import run_local_agent_turn, run_local_shell_command
|
|
48
55
|
from .pty import LocalPtyBroker
|
|
49
56
|
from .safety import revert_mutation as local_revert_mutation
|
|
@@ -61,6 +68,48 @@ from .workspace import (
|
|
|
61
68
|
# those tools only collapse genuinely large pastes.
|
|
62
69
|
PASTE_COLLAPSE_LINE_THRESHOLD = 3
|
|
63
70
|
|
|
71
|
+
async def _run_cancellable_local_turn(
|
|
72
|
+
*,
|
|
73
|
+
session_id: int,
|
|
74
|
+
renderer: StreamRenderer,
|
|
75
|
+
config: Config,
|
|
76
|
+
turn_coro,
|
|
77
|
+
) -> TaskOutcome:
|
|
78
|
+
"""Run one standalone agent turn with immediate terminal cancellation."""
|
|
79
|
+
|
|
80
|
+
agent_task = asyncio.create_task(turn_coro)
|
|
81
|
+
|
|
82
|
+
def _interrupt(_classification: str) -> None:
|
|
83
|
+
if not agent_task.done():
|
|
84
|
+
agent_task.cancel()
|
|
85
|
+
|
|
86
|
+
live_input = LiveInputListener(
|
|
87
|
+
session_id=session_id,
|
|
88
|
+
renderer=renderer,
|
|
89
|
+
cli_config=config,
|
|
90
|
+
interrupt_callback=_interrupt,
|
|
91
|
+
)
|
|
92
|
+
live_input.start()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
return await agent_task
|
|
96
|
+
except asyncio.CancelledError:
|
|
97
|
+
classification = live_input.interrupt_classification or "cancel"
|
|
98
|
+
|
|
99
|
+
if classification == "exit":
|
|
100
|
+
return TaskOutcome(
|
|
101
|
+
status="exited",
|
|
102
|
+
error="Exit requested by user",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return TaskOutcome(
|
|
106
|
+
status="cancelled",
|
|
107
|
+
error="Task cancelled by user.",
|
|
108
|
+
)
|
|
109
|
+
finally:
|
|
110
|
+
live_input.stop()
|
|
111
|
+
|
|
112
|
+
|
|
64
113
|
HELP_TEXT = """\
|
|
65
114
|
Natural-language text submits a full coding-agent task (mode: coding).
|
|
66
115
|
$ <command> explicit shell command
|
|
@@ -469,19 +518,19 @@ async def run_interactive(
|
|
|
469
518
|
model_state = local_state.get_session_state(workspace.session_id)
|
|
470
519
|
plan_objective = local_state.plan_execution_objective(plan)
|
|
471
520
|
if standalone:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
521
|
+
outcome = await _run_cancellable_local_turn(
|
|
522
|
+
session_id=workspace.session_id,
|
|
523
|
+
renderer=renderer,
|
|
524
|
+
config=config,
|
|
525
|
+
turn_coro=run_local_agent_turn(
|
|
476
526
|
provider_manager, provider_type, model,
|
|
477
527
|
[*conversation_history, {"role": "user", "content": plan_objective}],
|
|
478
528
|
console, renderer,
|
|
479
529
|
workspace_root=workspace.workspace_root, session_id=workspace.session_id,
|
|
480
530
|
approval_policy=config.approval_policy, interactive=True, cli_config=config,
|
|
481
531
|
allow_swarm_tool=True,
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
live_input.stop()
|
|
532
|
+
),
|
|
533
|
+
)
|
|
485
534
|
_append_turn_to_history(
|
|
486
535
|
conversation_history, objective=plan_objective,
|
|
487
536
|
answer=outcome.summary if outcome.status == "completed" else None,
|
|
@@ -1294,10 +1343,11 @@ async def run_interactive(
|
|
|
1294
1343
|
print_error(console, "Existing uncommitted changes detected; retry is blocked to preserve user edits.")
|
|
1295
1344
|
continue
|
|
1296
1345
|
renderer = StreamRenderer(console, mode_label=mode_label_for_policy(config.approval_policy))
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1346
|
+
outcome = await _run_cancellable_local_turn(
|
|
1347
|
+
session_id=workspace.session_id,
|
|
1348
|
+
renderer=renderer,
|
|
1349
|
+
config=config,
|
|
1350
|
+
turn_coro=run_local_agent_turn(
|
|
1301
1351
|
provider_manager, provider_type, model,
|
|
1302
1352
|
[*conversation_history, {"role": "user", "content": objective}],
|
|
1303
1353
|
console, renderer,
|
|
@@ -1305,9 +1355,8 @@ async def run_interactive(
|
|
|
1305
1355
|
approval_policy=config.approval_policy, interactive=True,
|
|
1306
1356
|
read_only=mode in {"chat", "audit", "plan"}, cli_config=config,
|
|
1307
1357
|
allow_swarm_tool=True,
|
|
1308
|
-
)
|
|
1309
|
-
|
|
1310
|
-
live_input.stop()
|
|
1358
|
+
),
|
|
1359
|
+
)
|
|
1311
1360
|
renderer.finish()
|
|
1312
1361
|
_append_turn_to_history(
|
|
1313
1362
|
conversation_history, objective=objective,
|
|
@@ -1404,10 +1453,11 @@ async def run_interactive(
|
|
|
1404
1453
|
"payload": {"content": submitted_text},
|
|
1405
1454
|
})
|
|
1406
1455
|
if standalone:
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1456
|
+
outcome = await _run_cancellable_local_turn(
|
|
1457
|
+
session_id=workspace.session_id,
|
|
1458
|
+
renderer=renderer,
|
|
1459
|
+
config=config,
|
|
1460
|
+
turn_coro=run_local_agent_turn(
|
|
1411
1461
|
provider_manager, provider_type, model,
|
|
1412
1462
|
[*conversation_history, {"role": "user", "content": intent.objective}],
|
|
1413
1463
|
console, renderer,
|
|
@@ -1415,15 +1465,18 @@ async def run_interactive(
|
|
|
1415
1465
|
approval_policy=config.approval_policy, interactive=True,
|
|
1416
1466
|
read_only=intent.mode in {"chat", "audit", "plan"}, cli_config=config,
|
|
1417
1467
|
allow_swarm_tool=True,
|
|
1418
|
-
)
|
|
1419
|
-
|
|
1420
|
-
live_input.stop()
|
|
1468
|
+
),
|
|
1469
|
+
)
|
|
1421
1470
|
renderer.finish()
|
|
1422
1471
|
_append_turn_to_history(
|
|
1423
1472
|
conversation_history, objective=intent.objective,
|
|
1424
1473
|
answer=outcome.summary if outcome.status == "completed" else None,
|
|
1425
1474
|
)
|
|
1426
1475
|
last_turn = (intent.objective, intent.mode)
|
|
1476
|
+
if outcome.status == "exited":
|
|
1477
|
+
if local_pty is not None:
|
|
1478
|
+
local_pty.close()
|
|
1479
|
+
break
|
|
1427
1480
|
if intent.mode == "plan" and outcome.status == "completed" and outcome.summary:
|
|
1428
1481
|
saved = local_state.save_plan(workspace.session_id, objective=intent.objective, content=outcome.summary)
|
|
1429
1482
|
console.print(f"[green]Plan saved[/green] · {saved.id}")
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import contextlib
|
|
14
14
|
import sys
|
|
15
|
-
from typing import Optional
|
|
15
|
+
from typing import Callable, Optional
|
|
16
16
|
|
|
17
17
|
from prompt_toolkit.formatted_text import HTML
|
|
18
18
|
|
|
@@ -30,10 +30,19 @@ _CTRL_Y = b"\x19"
|
|
|
30
30
|
class LiveInputListener:
|
|
31
31
|
"""Run a persistent, asynchronous follow-up editor during a task."""
|
|
32
32
|
|
|
33
|
-
def __init__(
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
session_id: int,
|
|
37
|
+
renderer: StreamRenderer,
|
|
38
|
+
cli_config: Config,
|
|
39
|
+
interrupt_callback: Optional[Callable[[str], None]] = None,
|
|
40
|
+
) -> None:
|
|
34
41
|
self.session_id = session_id
|
|
35
42
|
self.renderer = renderer
|
|
36
43
|
self.cli_config = cli_config
|
|
44
|
+
self._interrupt_callback = interrupt_callback
|
|
45
|
+
self._interrupt_classification: Optional[str] = None
|
|
37
46
|
self._is_tty = bool(getattr(sys.stdin, "isatty", lambda: False)())
|
|
38
47
|
self._input_task: Optional[asyncio.Task] = None
|
|
39
48
|
self._interject_task: Optional[asyncio.Task] = None
|
|
@@ -136,11 +145,15 @@ class LiveInputListener:
|
|
|
136
145
|
|
|
137
146
|
@bindings.add("escape")
|
|
138
147
|
def _cancel_running_turn(event) -> None:
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
# Escape cancels the active turn immediately and returns control
|
|
149
|
+
# to the ordinary REPL without terminating Tamfis-Code.
|
|
150
|
+
if self._paused or not self._active:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
self._request_interrupt("cancel")
|
|
154
|
+
|
|
155
|
+
if not event.app.is_done:
|
|
156
|
+
event.app.exit(result="")
|
|
144
157
|
|
|
145
158
|
session = PromptSession(key_bindings=bindings)
|
|
146
159
|
self._prompt_session = session
|
|
@@ -163,9 +176,44 @@ class LiveInputListener:
|
|
|
163
176
|
return
|
|
164
177
|
except EOFError:
|
|
165
178
|
return
|
|
179
|
+
|
|
180
|
+
# Escape sets _paused before exiting the prompt, so its empty
|
|
181
|
+
# result must not be queued or cause another editor to start.
|
|
182
|
+
if self._paused:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
# A genuine line already returned by prompt_async remains valid
|
|
186
|
+
# even when the listener was deactivated during that prompt.
|
|
187
|
+
# Queue it first, then let the loop terminate cleanly.
|
|
166
188
|
self._enqueue(text)
|
|
189
|
+
|
|
190
|
+
if not self._active:
|
|
191
|
+
break
|
|
192
|
+
|
|
167
193
|
self._prompt_session = None
|
|
168
194
|
|
|
195
|
+
@property
|
|
196
|
+
def interrupt_classification(self) -> Optional[str]:
|
|
197
|
+
return self._interrupt_classification
|
|
198
|
+
|
|
199
|
+
def _request_interrupt(self, classification: str) -> None:
|
|
200
|
+
"""Record and immediately propagate one terminal interrupt.
|
|
201
|
+
|
|
202
|
+
The durable queue item remains useful for history and recovery, but
|
|
203
|
+
immediate cancellation is delivered through interrupt_callback so a
|
|
204
|
+
blocked provider request or long-running tool does not need to reach
|
|
205
|
+
another orchestration boundary first.
|
|
206
|
+
"""
|
|
207
|
+
if self._interrupt_classification is not None:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
self._interrupt_classification = classification
|
|
211
|
+
self._paused = True
|
|
212
|
+
self._enqueue_control(classification)
|
|
213
|
+
|
|
214
|
+
if self._interrupt_callback is not None:
|
|
215
|
+
self._interrupt_callback(classification)
|
|
216
|
+
|
|
169
217
|
def _enqueue_control(self, classification: str) -> None:
|
|
170
218
|
item = local_state.enqueue_instruction(
|
|
171
219
|
self.session_id, "", classification=classification,
|
|
@@ -135,22 +135,27 @@ class AgentOrchestrator:
|
|
|
135
135
|
self._advance_plan_step()
|
|
136
136
|
|
|
137
137
|
def _advance_plan_step(self) -> None:
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
"""Expose execution progress without fabricating completion.
|
|
139
|
+
|
|
140
|
+
A tool result proves only that a tool call occurred. It does not prove
|
|
141
|
+
that a particular plan step completed, that a file changed, or that a
|
|
142
|
+
validation command passed. Therefore this method may mark one pending
|
|
143
|
+
step as in progress, but it must never mark a step completed.
|
|
144
|
+
|
|
145
|
+
Final completion is assigned only by complete() after validation.
|
|
146
|
+
"""
|
|
144
147
|
assert self.run is not None
|
|
145
148
|
if self.run.plan is None or not self.run.plan.steps:
|
|
146
149
|
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
|
|
151
|
+
# Keep at most one step active. Additional tool calls are evidence
|
|
152
|
+
# that work continues, not that later plan steps have started.
|
|
153
|
+
if not any(step.status == "in_progress" for step in self.run.plan.steps):
|
|
154
|
+
for step in self.run.plan.steps:
|
|
155
|
+
if step.status == "pending":
|
|
156
|
+
step.status = "in_progress"
|
|
157
|
+
break
|
|
158
|
+
|
|
154
159
|
self._sync_plan_progress()
|
|
155
160
|
|
|
156
161
|
def mark_repair(self, reason: str) -> None:
|