tamfis-code 0.6.13__tar.gz → 0.7.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.
- {tamfis_code-0.6.13/tamfis_code.egg-info → tamfis_code-0.7.0}/PKG-INFO +1 -1
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/pyproject.toml +1 -1
- tamfis_code-0.7.0/tamfis_code/__init__.py +49 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/agents.py +2 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/enforcer.py +2 -0
- tamfis_code-0.7.0/tamfis_code/indexer.py +37 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/interactive.py +75 -22
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/live_input.py +55 -7
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/orchestrator/engine.py +62 -18
- tamfis_code-0.7.0/tamfis_code/orchestrator/planner.py +871 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/render.py +15 -5
- tamfis_code-0.7.0/tamfis_code/routing.py +55 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/runner_local.py +461 -35
- tamfis_code-0.7.0/tamfis_code/runtime/__init__.py +13 -0
- tamfis_code-0.7.0/tamfis_code/runtime/budgets.py +18 -0
- tamfis_code-0.7.0/tamfis_code/runtime/controller.py +147 -0
- tamfis_code-0.7.0/tamfis_code/runtime/evidence.py +77 -0
- tamfis_code-0.7.0/tamfis_code/runtime/state.py +61 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0/tamfis_code.egg-info}/PKG-INFO +1 -1
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code.egg-info/SOURCES.txt +6 -57
- tamfis_code-0.7.0/tests/test_runtime_controller.py +55 -0
- tamfis_code-0.6.13/MANIFEST.in +0 -3
- tamfis_code-0.6.13/USAGE_INSTALL_RELEASE.md +0 -85
- tamfis_code-0.6.13/tamfis_code/__init__.py +0 -54
- tamfis_code-0.6.13/tamfis_code/indexer.py +0 -632
- tamfis_code-0.6.13/tamfis_code/orchestrator/planner.py +0 -195
- tamfis_code-0.6.13/tamfis_code/routing.py +0 -165
- tamfis_code-0.6.13/tests/test_agent_definitions.py +0 -76
- tamfis_code-0.6.13/tests/test_agents.py +0 -248
- tamfis_code-0.6.13/tests/test_agents_delegation.py +0 -83
- tamfis_code-0.6.13/tests/test_build_installation.py +0 -108
- tamfis_code-0.6.13/tests/test_cli_commands.py +0 -601
- tamfis_code-0.6.13/tests/test_clipboard.py +0 -50
- tamfis_code-0.6.13/tests/test_completion.py +0 -49
- tamfis_code-0.6.13/tests/test_context_rollover.py +0 -327
- tamfis_code-0.6.13/tests/test_custom_commands.py +0 -90
- tamfis_code-0.6.13/tests/test_cwd_validation.py +0 -179
- tamfis_code-0.6.13/tests/test_doctor_session_diagnostics.py +0 -210
- tamfis_code-0.6.13/tests/test_enforcer.py +0 -178
- tamfis_code-0.6.13/tests/test_fake_tool_detection.py +0 -171
- tamfis_code-0.6.13/tests/test_find_references.py +0 -122
- tamfis_code-0.6.13/tests/test_github_command_surface.py +0 -15
- tamfis_code-0.6.13/tests/test_hooks.py +0 -144
- tamfis_code-0.6.13/tests/test_indexer.py +0 -185
- tamfis_code-0.6.13/tests/test_instructions.py +0 -75
- tamfis_code-0.6.13/tests/test_integration_new.py +0 -88
- tamfis_code-0.6.13/tests/test_interactive_standalone.py +0 -538
- tamfis_code-0.6.13/tests/test_live_input.py +0 -238
- tamfis_code-0.6.13/tests/test_local_pty.py +0 -36
- tamfis_code-0.6.13/tests/test_mcp.py +0 -611
- tamfis_code-0.6.13/tests/test_mcp_search_bounds.py +0 -131
- tamfis_code-0.6.13/tests/test_mcp_standalone_degradation.py +0 -46
- tamfis_code-0.6.13/tests/test_mcp_stdio_server.py +0 -113
- tamfis_code-0.6.13/tests/test_metrics.py +0 -80
- tamfis_code-0.6.13/tests/test_openhands_runtime.py +0 -113
- tamfis_code-0.6.13/tests/test_openrouter_cost_routing.py +0 -95
- tamfis_code-0.6.13/tests/test_orchestrator.py +0 -315
- tamfis_code-0.6.13/tests/test_parity_contracts.py +0 -152
- tamfis_code-0.6.13/tests/test_plan_mode_gate.py +0 -115
- tamfis_code-0.6.13/tests/test_plan_steps_and_attempts.py +0 -110
- tamfis_code-0.6.13/tests/test_provider_protocols.py +0 -94
- tamfis_code-0.6.13/tests/test_reasoning_effort_capability.py +0 -58
- tamfis_code-0.6.13/tests/test_reasoning_plan.py +0 -310
- tamfis_code-0.6.13/tests/test_references.py +0 -202
- tamfis_code-0.6.13/tests/test_render_task_diagnostics.py +0 -53
- tamfis_code-0.6.13/tests/test_routing.py +0 -268
- tamfis_code-0.6.13/tests/test_runner_helpers.py +0 -155
- tamfis_code-0.6.13/tests/test_runner_local.py +0 -2098
- tamfis_code-0.6.13/tests/test_safety.py +0 -364
- tamfis_code-0.6.13/tests/test_safety_manifest.py +0 -110
- tamfis_code-0.6.13/tests/test_swarm.py +0 -431
- tamfis_code-0.6.13/tests/test_tamfis_code_api_client.py +0 -174
- tamfis_code-0.6.13/tests/test_tamfis_code_approval.py +0 -234
- tamfis_code-0.6.13/tests/test_tamfis_code_approval_flow.py +0 -229
- tamfis_code-0.6.13/tests/test_tamfis_code_config.py +0 -329
- tamfis_code-0.6.13/tests/test_tamfis_code_intent.py +0 -100
- tamfis_code-0.6.13/tests/test_tamfis_code_render.py +0 -550
- tamfis_code-0.6.13/tests/test_tamfis_code_repl_exit.py +0 -70
- tamfis_code-0.6.13/tests/test_tamfis_code_tasks.py +0 -77
- tamfis_code-0.6.13/tests/test_tamfis_code_workspace.py +0 -487
- tamfis_code-0.6.13/tests/test_version_consistency.py +0 -40
- tamfis_code-0.6.13/tests/test_workspace_scope.py +0 -201
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/README.md +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/setup.cfg +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/__main__.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/agent_definitions.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/api_client.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/cli.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/clipboard.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/completion.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/config.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/custom_commands.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/doctor.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/evidence.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/github_commands.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/hooks.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/instructions.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/local_chat.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/local_tools.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/mcp.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/metrics.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/model_registry.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/__init__.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/agent.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/agent_server.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/automation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/compat.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/conversation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/delegation.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/events.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/leases.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/security.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/skills.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/tools.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/openhands/workspace.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/orchestrator/__init__.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/orchestrator/context.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/orchestrator/protocols.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/orchestrator/validator.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/project.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/provider_protocols.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/providers.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/pty.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/references.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/runner.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/safety.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/screenshot.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/state.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/swarm.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/tasks.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/tool_policy.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code/workspace.py +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code.egg-info/dependency_links.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code.egg-info/entry_points.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code.egg-info/requires.txt +0 -0
- {tamfis_code-0.6.13 → tamfis_code-0.7.0}/tamfis_code.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tamfis-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tamfis-Code, a standalone terminal coding agent.
|
|
2
|
+
|
|
3
|
+
The package keeps import-time side effects deliberately minimal. Public
|
|
4
|
+
components are loaded lazily so a missing optional module cannot prevent the
|
|
5
|
+
CLI or deterministic runtime from starting.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__version__ = "0.7.0"
|
|
13
|
+
MIN_COMPATIBLE_API_VERSION = "remote-ai-v2"
|
|
14
|
+
|
|
15
|
+
_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
|
|
16
|
+
"ShellCompleter": (".completion", "ShellCompleter"),
|
|
17
|
+
"MetricsTracker": (".metrics", "MetricsTracker"),
|
|
18
|
+
"StreamMetrics": (".metrics", "StreamMetrics"),
|
|
19
|
+
"AgentManager": (".agents", "AgentManager"),
|
|
20
|
+
"SubAgent": (".agents", "SubAgent"),
|
|
21
|
+
"CodeAnalyzer": (".agents", "CodeAnalyzer"),
|
|
22
|
+
"TestGenerator": (".agents", "TestGenerator"),
|
|
23
|
+
"DocGenerator": (".agents", "DocGenerator"),
|
|
24
|
+
"MCPServer": (".mcp", "MCPServer"),
|
|
25
|
+
"ToolDefinition": (".mcp", "ToolDefinition"),
|
|
26
|
+
"call_tool": (".mcp", "call_tool"),
|
|
27
|
+
"CodeIndexer": (".indexer", "CodeIndexer"),
|
|
28
|
+
"CodeSymbol": (".indexer", "CodeSymbol"),
|
|
29
|
+
"CodeFile": (".indexer", "CodeFile"),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
__all__ = ["__version__", "MIN_COMPATIBLE_API_VERSION", *_LAZY_EXPORTS]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __getattr__(name: str) -> Any:
|
|
36
|
+
target = _LAZY_EXPORTS.get(name)
|
|
37
|
+
if target is None:
|
|
38
|
+
raise AttributeError(name)
|
|
39
|
+
module_name, attribute = target
|
|
40
|
+
try:
|
|
41
|
+
module = import_module(module_name, __name__)
|
|
42
|
+
except ModuleNotFoundError as exc:
|
|
43
|
+
raise AttributeError(
|
|
44
|
+
f"Optional Tamfis-Code component {name!r} is unavailable because "
|
|
45
|
+
f"module {module_name!r} is not installed."
|
|
46
|
+
) from exc
|
|
47
|
+
value = getattr(module, attribute)
|
|
48
|
+
globals()[name] = value
|
|
49
|
+
return value
|
|
@@ -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):
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Small dependency-free code index used by optional public exports."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class CodeSymbol:
|
|
9
|
+
name: str
|
|
10
|
+
kind: str
|
|
11
|
+
line: int
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CodeFile:
|
|
15
|
+
path: str
|
|
16
|
+
language: str
|
|
17
|
+
symbols: list[CodeSymbol] = field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
class CodeIndexer:
|
|
20
|
+
def __init__(self, root: str | Path): self.root=Path(root).resolve()
|
|
21
|
+
def index_file(self, path: str | Path) -> CodeFile:
|
|
22
|
+
p=Path(path).resolve(); text=p.read_text(encoding='utf-8',errors='replace')
|
|
23
|
+
suffix=p.suffix.lower(); language={'.py':'python','.ts':'typescript','.tsx':'typescript','.js':'javascript','.jsx':'javascript'}.get(suffix,'text')
|
|
24
|
+
symbols=[]
|
|
25
|
+
patterns=[('class',re.compile(r'^\s*class\s+([A-Za-z_]\w*)')),('function',re.compile(r'^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)')),('function',re.compile(r'^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)'))]
|
|
26
|
+
for n,line in enumerate(text.splitlines(),1):
|
|
27
|
+
for kind,pat in patterns:
|
|
28
|
+
m=pat.search(line)
|
|
29
|
+
if m: symbols.append(CodeSymbol(m.group(1),kind,n)); break
|
|
30
|
+
return CodeFile(str(p),language,symbols)
|
|
31
|
+
def index(self) -> list[CodeFile]:
|
|
32
|
+
result=[]
|
|
33
|
+
for p in self.root.rglob('*'):
|
|
34
|
+
if p.is_file() and p.suffix.lower() in {'.py','.ts','.tsx','.js','.jsx'} and not any(part.startswith('.') for part in p.relative_to(self.root).parts):
|
|
35
|
+
try: result.append(self.index_file(p))
|
|
36
|
+
except OSError: pass
|
|
37
|
+
return result
|
|
@@ -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,
|
|
@@ -10,6 +10,7 @@ from .context import ContextBundle, build_context_bundle
|
|
|
10
10
|
from .planner import ExecutionPlan, create_plan
|
|
11
11
|
from .protocols import AgentPhase, ToolEnvelope
|
|
12
12
|
from .validator import ValidationReport, validate_completion
|
|
13
|
+
from ..runtime import ExecutionController, GuardDecision, ObservationDecision
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@dataclass
|
|
@@ -26,6 +27,7 @@ class OrchestrationRun:
|
|
|
26
27
|
route: dict[str, Any] = field(default_factory=dict)
|
|
27
28
|
repair_attempts: int = 0
|
|
28
29
|
reasoning_plan: bool = False
|
|
30
|
+
runtime: ExecutionController = field(default_factory=ExecutionController)
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class AgentOrchestrator:
|
|
@@ -60,6 +62,7 @@ class AgentOrchestrator:
|
|
|
60
62
|
if profile.requires_repository_context:
|
|
61
63
|
self.transition(AgentPhase.INSPECT, action="Load or refresh repository context")
|
|
62
64
|
self.run.plan = create_plan(objective, profile)
|
|
65
|
+
self.run.runtime.start_planning()
|
|
63
66
|
plan_dict = self.run.plan.to_dict() if self.run.plan else None
|
|
64
67
|
self.run.context = build_context_bundle(
|
|
65
68
|
session_id=self.session_id, workspace_root=self.workspace_root,
|
|
@@ -85,6 +88,9 @@ class AgentOrchestrator:
|
|
|
85
88
|
for the "here is the new plan" banner; this only handles state.
|
|
86
89
|
"""
|
|
87
90
|
assert self.run is not None
|
|
91
|
+
if not self.run.runtime.record_plan_revision():
|
|
92
|
+
self.fail(self.run.runtime.snapshot.failure_reason)
|
|
93
|
+
return
|
|
88
94
|
saved = local_state.save_plan(
|
|
89
95
|
self.session_id, objective=self.run.objective,
|
|
90
96
|
content="\n".join(f"{s.index}. {s.name}" for s in plan.steps),
|
|
@@ -120,46 +126,79 @@ class AgentOrchestrator:
|
|
|
120
126
|
local_state.save_session_state(self.session_id, selected_provider=provider, selected_model=model)
|
|
121
127
|
|
|
122
128
|
def start_execution(self) -> None:
|
|
129
|
+
assert self.run is not None
|
|
130
|
+
self.run.runtime.start_execution()
|
|
123
131
|
self.transition(AgentPhase.EXECUTE, action="Execute the model/tool loop")
|
|
124
132
|
|
|
133
|
+
def guard_tool_call(self, tool_name: str, arguments: dict[str, Any]) -> GuardDecision:
|
|
134
|
+
assert self.run is not None
|
|
135
|
+
decision = self.run.runtime.guard_action(tool_name, arguments)
|
|
136
|
+
if not decision.allowed:
|
|
137
|
+
self.emit({"event_type": "diagnostics", "payload": {"content": decision.reason}})
|
|
138
|
+
return decision
|
|
139
|
+
|
|
125
140
|
def waiting_for_approval(self, purpose: str) -> None:
|
|
126
141
|
self.transition(AgentPhase.WAITING_FOR_APPROVAL, action=purpose)
|
|
127
142
|
|
|
128
|
-
def record_tool(self, envelope: ToolEnvelope) ->
|
|
143
|
+
def record_tool(self, envelope: ToolEnvelope) -> ObservationDecision:
|
|
129
144
|
assert self.run is not None
|
|
130
145
|
self.run.tool_records.append(envelope)
|
|
131
146
|
self.transition(AgentPhase.OBSERVE, action=f"Observe {envelope.tool_name} result")
|
|
147
|
+
result = {
|
|
148
|
+
"success": bool(envelope.success),
|
|
149
|
+
"result": {
|
|
150
|
+
"stdout": envelope.stdout,
|
|
151
|
+
"stderr": envelope.stderr,
|
|
152
|
+
"exit_code": envelope.exit_code,
|
|
153
|
+
"files_changed": list(envelope.files_changed),
|
|
154
|
+
"path": envelope.arguments.get("path") or envelope.arguments.get("destination"),
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
decision = self.run.runtime.observe(envelope.tool_name, envelope.arguments, result)
|
|
132
158
|
state = local_state.get_session_state(self.session_id)
|
|
133
159
|
records = state.completed_actions + [{"type": "tool", **envelope.to_dict()}]
|
|
134
|
-
local_state.save_session_state(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
local_state.save_session_state(
|
|
161
|
+
self.session_id, completed_actions=records[-250:],
|
|
162
|
+
running_action={
|
|
163
|
+
"purpose": decision.reason or f"Observed {envelope.tool_name}",
|
|
164
|
+
"phase": self.run.runtime.snapshot.phase.value,
|
|
165
|
+
"runtime": self.run.runtime.snapshot.to_dict(),
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
self._advance_plan_step(decision)
|
|
169
|
+
if decision.terminal:
|
|
170
|
+
self.fail(decision.reason)
|
|
171
|
+
return decision
|
|
172
|
+
|
|
173
|
+
def _advance_plan_step(self, decision: ObservationDecision) -> None:
|
|
174
|
+
"""Advance only when a successful observation gained useful evidence."""
|
|
144
175
|
assert self.run is not None
|
|
145
176
|
if self.run.plan is None or not self.run.plan.steps:
|
|
146
177
|
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if
|
|
151
|
-
|
|
152
|
-
if
|
|
153
|
-
|
|
178
|
+
active = next((step for step in self.run.plan.steps if step.status == "in_progress"), None)
|
|
179
|
+
if active is None:
|
|
180
|
+
active = next((step for step in self.run.plan.steps if step.status == "pending"), None)
|
|
181
|
+
if active is not None:
|
|
182
|
+
active.status = "in_progress"
|
|
183
|
+
if active is not None and decision.useful:
|
|
184
|
+
active.status = "completed"
|
|
185
|
+
active.evidence.extend(item for item in decision.evidence if item not in active.evidence)
|
|
186
|
+
nxt = next((step for step in self.run.plan.steps if step.status == "pending"), None)
|
|
187
|
+
if nxt is not None:
|
|
188
|
+
nxt.status = "in_progress"
|
|
154
189
|
self._sync_plan_progress()
|
|
155
190
|
|
|
156
191
|
def mark_repair(self, reason: str) -> None:
|
|
157
192
|
assert self.run is not None
|
|
158
193
|
self.run.repair_attempts += 1
|
|
194
|
+
if not self.run.runtime.record_repair():
|
|
195
|
+
self.fail(self.run.runtime.snapshot.failure_reason)
|
|
196
|
+
return
|
|
159
197
|
self.transition(AgentPhase.REPAIR, action=reason)
|
|
160
198
|
|
|
161
199
|
def validate(self, *, final_text: str, any_mutation: bool) -> ValidationReport:
|
|
162
200
|
assert self.run is not None
|
|
201
|
+
self.run.runtime.begin_validation()
|
|
163
202
|
self.transition(AgentPhase.VALIDATE, action="Validate evidence and completion claims")
|
|
164
203
|
report = validate_completion(
|
|
165
204
|
profile=self.run.profile,
|
|
@@ -200,12 +239,17 @@ class AgentOrchestrator:
|
|
|
200
239
|
)
|
|
201
240
|
self._sync_plan_progress()
|
|
202
241
|
self.transition(AgentPhase.REPORT, action="Report only evidence-supported outcomes")
|
|
242
|
+
if report.severity == "error":
|
|
243
|
+
self.run.runtime.fail("Completion validation failed.")
|
|
244
|
+
else:
|
|
245
|
+
self.run.runtime.complete()
|
|
203
246
|
self.transition(AgentPhase.FAILED if report.severity == "error" else AgentPhase.COMPLETED)
|
|
204
247
|
local_state.checkpoint(self.session_id, reason="orchestrator_complete", summary=final_text[-1000:])
|
|
205
248
|
return report
|
|
206
249
|
|
|
207
250
|
def fail(self, error: str) -> None:
|
|
208
251
|
if self.run is not None:
|
|
252
|
+
self.run.runtime.fail(error)
|
|
209
253
|
if self.run.plan is not None:
|
|
210
254
|
for step in self.run.plan.steps:
|
|
211
255
|
if step.status == "in_progress":
|