tamfis-code 0.6.1__tar.gz → 0.6.3__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.1/tamfis_code.egg-info → tamfis_code-0.6.3}/PKG-INFO +1 -1
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/pyproject.toml +1 -1
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/__init__.py +1 -1
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/model_registry.py +18 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/providers.py +25 -17
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/render.py +35 -17
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/runner.py +8 -1
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/runner_local.py +17 -1
- {tamfis_code-0.6.1 → tamfis_code-0.6.3/tamfis_code.egg-info}/PKG-INFO +1 -1
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_fake_tool_detection.py +11 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_routing.py +26 -9
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_render.py +26 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/MANIFEST.in +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/README.md +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/USAGE_INSTALL_RELEASE.md +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/setup.cfg +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/__main__.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/agent_definitions.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/agents.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/api_client.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/cli.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/clipboard.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/completion.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/config.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/custom_commands.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/doctor.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/enforcer.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/evidence.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/hooks.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/indexer.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/instructions.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/interactive.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/live_input.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/local_chat.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/local_tools.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/mcp.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/metrics.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/__init__.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/agent.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/agent_server.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/automation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/compat.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/conversation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/delegation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/events.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/leases.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/security.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/skills.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/tools.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/openhands/workspace.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/orchestrator/__init__.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/orchestrator/context.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/orchestrator/engine.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/orchestrator/planner.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/orchestrator/protocols.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/orchestrator/validator.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/project.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/provider_protocols.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/pty.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/references.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/routing.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/safety.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/screenshot.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/sessions.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/state.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/swarm.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/tasks.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/tool_policy.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code/workspace.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code.egg-info/SOURCES.txt +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code.egg-info/dependency_links.txt +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code.egg-info/entry_points.txt +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code.egg-info/requires.txt +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tamfis_code.egg-info/top_level.txt +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_agent_definitions.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_agents.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_agents_delegation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_build_installation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_cli_commands.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_clipboard.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_completion.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_context_rollover.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_custom_commands.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_cwd_validation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_doctor_session_diagnostics.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_enforcer.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_find_references.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_hooks.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_indexer.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_instructions.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_integration_new.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_interactive_standalone.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_live_input.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_local_pty.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_mcp.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_mcp_search_bounds.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_mcp_standalone_degradation.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_metrics.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_openhands_runtime.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_openrouter_cost_routing.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_orchestrator.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_plan_mode_gate.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_plan_steps_and_attempts.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_provider_protocols.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_reasoning_effort_capability.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_reasoning_plan.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_references.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_render_task_diagnostics.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_runner_helpers.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_runner_local.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_safety.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_safety_manifest.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_sessions.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_swarm.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_api_client.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_approval.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_approval_flow.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_config.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_intent.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_repl_exit.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_tasks.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_tamfis_code_workspace.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_version_consistency.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_workspace_discovery_gaps.py +0 -0
- {tamfis_code-0.6.1 → tamfis_code-0.6.3}/tests/test_workspace_scope.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.3
|
|
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.3"
|
|
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
|
|
@@ -59,6 +59,24 @@ MODELS: dict[str, ModelRecord] = {
|
|
|
59
59
|
ModelCapabilities(vision=True, long_context=True), 1000000,
|
|
60
60
|
("long_context_review", "repository_search", "vision_assisted_coding"), "high", "medium",
|
|
61
61
|
),
|
|
62
|
+
"qwen/qwen3-coder": ModelRecord(
|
|
63
|
+
"qwen/qwen3-coder", "openrouter",
|
|
64
|
+
ModelCapabilities(parallel_tool_calls=True, long_context=True), 256000,
|
|
65
|
+
("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
|
|
66
|
+
"frontier", "medium",
|
|
67
|
+
),
|
|
68
|
+
"deepseek/deepseek-chat-v3-0324": ModelRecord(
|
|
69
|
+
"deepseek/deepseek-chat-v3-0324", "openrouter",
|
|
70
|
+
ModelCapabilities(parallel_tool_calls=True, long_context=True), 128000,
|
|
71
|
+
("repository_search", "multi_file_edit", "debugging", "planning"),
|
|
72
|
+
"high", "low",
|
|
73
|
+
),
|
|
74
|
+
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": ModelRecord(
|
|
75
|
+
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", "nvidia",
|
|
76
|
+
ModelCapabilities(parallel_tool_calls=True, long_context=True), 128000,
|
|
77
|
+
("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
|
|
78
|
+
"frontier", "medium",
|
|
79
|
+
),
|
|
62
80
|
"Qwen/Qwen2.5-7B-Instruct": ModelRecord(
|
|
63
81
|
"Qwen/Qwen2.5-7B-Instruct", "hf",
|
|
64
82
|
ModelCapabilities(long_context=False), 32768,
|
|
@@ -226,22 +226,14 @@ class ProviderManager:
|
|
|
226
226
|
name="NVIDIA NIM",
|
|
227
227
|
base_url="https://integrate.api.nvidia.com/v1",
|
|
228
228
|
api_key_env="NVIDIA_API_KEY",
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
# the entitlement-shaped error, likely most ordinary NVIDIA API
|
|
238
|
-
# keys -- third-party marketplace models commonly need separate
|
|
239
|
-
# enablement NVIDIA's own first-party models don't) has no
|
|
240
|
-
# actual deployment access to it. A default that 404s outright
|
|
241
|
-
# for typical accounts is strictly worse than nemotron's
|
|
242
|
-
# occasional loop -- confirmed the replacement actually works
|
|
243
|
-
# live before switching to it.
|
|
244
|
-
default_model="meta/llama-3.1-70b-instruct",
|
|
229
|
+
# The plain Llama instruct route can answer fluently but has
|
|
230
|
+
# repeatedly narrated/fabricated local tool results in this CLI.
|
|
231
|
+
# This NVIDIA reasoning model was verified to return genuine
|
|
232
|
+
# tool_calls and to accept reasoning parameters without the
|
|
233
|
+
# indefinite stream stall seen on the plain instruct models.
|
|
234
|
+
# Keep the older models selectable for accounts where this route
|
|
235
|
+
# is not enabled, but do not make them the automatic coding path.
|
|
236
|
+
default_model="nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
|
245
237
|
models=[
|
|
246
238
|
"nvidia/nemotron-3-super-120b-a12b",
|
|
247
239
|
"nvidia/nemotron-3-ultra-550b-a55b",
|
|
@@ -303,7 +295,10 @@ class ProviderManager:
|
|
|
303
295
|
name="OpenRouter",
|
|
304
296
|
base_url="https://openrouter.ai/api/v1",
|
|
305
297
|
api_key_env="OPENROUTER_API_KEY",
|
|
306
|
-
|
|
298
|
+
# Paid coding/repair work gets a coder-tuned route. Routine work
|
|
299
|
+
# still uses the free model below, so this does not turn every
|
|
300
|
+
# inspection into a paid request.
|
|
301
|
+
default_model="qwen/qwen3-coder",
|
|
307
302
|
# Used for plain chat/Q&A/inspection -- anything that isn't
|
|
308
303
|
# deep research or genuinely demanding coding/analysis work --
|
|
309
304
|
# so routine turns don't spend paid OpenRouter credits at all.
|
|
@@ -626,6 +621,17 @@ class ProviderManager:
|
|
|
626
621
|
|
|
627
622
|
status = cls.provider_error_status(exc)
|
|
628
623
|
if status is not None:
|
|
624
|
+
# NVIDIA can return HTTP 400 when a model's registered function
|
|
625
|
+
# is degraded or unavailable. This is a route/model capability
|
|
626
|
+
# failure, not a malformed user request; AUTO must continue with
|
|
627
|
+
# the next eligible provider instead of stopping the task.
|
|
628
|
+
if status == 400:
|
|
629
|
+
message = str(exc).lower()
|
|
630
|
+
if (
|
|
631
|
+
"degraded function cannot be invoked" in message
|
|
632
|
+
or "function cannot be invoked" in message
|
|
633
|
+
):
|
|
634
|
+
return True
|
|
629
635
|
return status in {
|
|
630
636
|
401,
|
|
631
637
|
402,
|
|
@@ -679,6 +685,8 @@ class ProviderManager:
|
|
|
679
685
|
# provider/model is exactly the right response -- this is an
|
|
680
686
|
# account-entitlement failure, not a bad request.
|
|
681
687
|
"not found for account",
|
|
688
|
+
"degraded function cannot be invoked",
|
|
689
|
+
"function cannot be invoked",
|
|
682
690
|
)
|
|
683
691
|
return any(marker in message for marker in retryable_markers)
|
|
684
692
|
|
|
@@ -432,6 +432,32 @@ class StreamRenderer:
|
|
|
432
432
|
if self._live is not None:
|
|
433
433
|
self._live.update(self._build_status())
|
|
434
434
|
|
|
435
|
+
def _print_plan_snapshot(
|
|
436
|
+
self, items: list[dict[str, Any]], *, title: str,
|
|
437
|
+
assumptions: Optional[list[Any]] = None, risks: Optional[list[Any]] = None,
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Print an authoritative plan snapshot to scrollback.
|
|
440
|
+
|
|
441
|
+
Rich Live is intentionally transient for the spinner, so it cannot
|
|
442
|
+
be the source of truth for a plan users need to follow. Every plan
|
|
443
|
+
creation and every status transition gets a durable snapshot here.
|
|
444
|
+
"""
|
|
445
|
+
table = Table.grid(padding=(0, 1))
|
|
446
|
+
table.add_column(width=4, justify="right", style="cyan")
|
|
447
|
+
table.add_column(ratio=1)
|
|
448
|
+
for index, item in enumerate(items, start=1):
|
|
449
|
+
status = str(item.get("status") or "pending")
|
|
450
|
+
marker = "✓" if status == "completed" else (
|
|
451
|
+
"✗" if status == "failed" else ("▶" if status == "in_progress" else "○")
|
|
452
|
+
)
|
|
453
|
+
table.add_row(f"{index}. {marker}", f"{item.get('step') or ''} · {status}")
|
|
454
|
+
body: list[Any] = [table]
|
|
455
|
+
if assumptions:
|
|
456
|
+
body.extend([Text("Assumptions", style="bold"), Text(" • " + "\n • ".join(map(str, assumptions)))])
|
|
457
|
+
if risks:
|
|
458
|
+
body.extend([Text("Risks", style="bold yellow"), Text(" • " + "\n • ".join(map(str, risks)))])
|
|
459
|
+
self.console.print(Panel(Group(*body), title=title, border_style="cyan", expand=False))
|
|
460
|
+
|
|
435
461
|
def _stop_live(self) -> None:
|
|
436
462
|
"""End the progress display before ordinary streamed output begins."""
|
|
437
463
|
if self._live is not None:
|
|
@@ -574,14 +600,15 @@ class StreamRenderer:
|
|
|
574
600
|
return
|
|
575
601
|
|
|
576
602
|
if event_type == "plan_step_progress":
|
|
577
|
-
#
|
|
578
|
-
#
|
|
579
|
-
#
|
|
580
|
-
|
|
603
|
+
# Update both the live view and durable scrollback. The live view
|
|
604
|
+
# may already have been stopped when assistant output began, so
|
|
605
|
+
# refreshing it alone makes progress appear to disappear.
|
|
606
|
+
self._close_assistant()
|
|
581
607
|
items = payload.get("items") if isinstance(payload.get("items"), list) else []
|
|
582
608
|
if items:
|
|
583
609
|
self._plan_steps = [item for item in items if isinstance(item, dict) and item.get("status") != "context"]
|
|
584
610
|
self._refresh_live()
|
|
611
|
+
self._print_plan_snapshot(self._plan_steps, title=payload.get("title") or "Plan progress")
|
|
585
612
|
return
|
|
586
613
|
|
|
587
614
|
if event_type == "plan_created":
|
|
@@ -595,21 +622,12 @@ class StreamRenderer:
|
|
|
595
622
|
# Rich's TTY Live region is transient and is stopped when
|
|
596
623
|
# assistant output begins. Always print a durable snapshot;
|
|
597
624
|
# otherwise the plan disappears at execution start.
|
|
598
|
-
table = Table.grid(padding=(0, 1))
|
|
599
|
-
table.add_column(width=4, justify="right", style="cyan")
|
|
600
|
-
table.add_column(ratio=1)
|
|
601
|
-
for index, item in enumerate(self._plan_steps, start=1):
|
|
602
|
-
status = item.get("status")
|
|
603
|
-
marker = "✓" if status == "completed" else ("▶" if status == "in_progress" else "○")
|
|
604
|
-
table.add_row(f"{index}. {marker}", str(item.get("step") or ""))
|
|
605
625
|
assumptions = payload.get("assumptions") or []
|
|
606
626
|
risks = payload.get("risks") or []
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
body.extend([Text("Risks", style="bold yellow"), Text(" • " + "\n • ".join(map(str, risks)))])
|
|
612
|
-
self.console.print(Panel(Group(*body), title=payload.get("title") or "Execution plan", border_style="cyan", expand=False))
|
|
627
|
+
self._print_plan_snapshot(
|
|
628
|
+
self._plan_steps, title=payload.get("title") or "Execution plan",
|
|
629
|
+
assumptions=assumptions, risks=risks,
|
|
630
|
+
)
|
|
613
631
|
return
|
|
614
632
|
if stage == "tool_execution":
|
|
615
633
|
match = _TOOL_ANNOUNCE_RE.search(content)
|
|
@@ -546,7 +546,7 @@ async def _stream_task(
|
|
|
546
546
|
continue
|
|
547
547
|
|
|
548
548
|
phase_by_event = {
|
|
549
|
-
"plan_created": "plan", "tool_call_requested": "execute",
|
|
549
|
+
"plan_created": "plan", "plan_step_progress": "execute", "tool_call_requested": "execute",
|
|
550
550
|
"command_started": "execute", "file_mutation": "execute",
|
|
551
551
|
"approval_required": "waiting_for_approval", "task_diagnostics": "validate",
|
|
552
552
|
"ai_task_completed": "report", "ai_task_failed": "report",
|
|
@@ -566,6 +566,13 @@ async def _stream_task(
|
|
|
566
566
|
if state.active_plan_id:
|
|
567
567
|
local_state.update_plan_steps(session_id, state.active_plan_id, items)
|
|
568
568
|
|
|
569
|
+
if event_type == "plan_step_progress":
|
|
570
|
+
items = payload.get("items") if isinstance(payload.get("items"), list) else []
|
|
571
|
+
last_plan_items = items
|
|
572
|
+
state = local_state.get_session_state(session_id)
|
|
573
|
+
if state.active_plan_id and items:
|
|
574
|
+
local_state.update_plan_steps(session_id, state.active_plan_id, items)
|
|
575
|
+
|
|
569
576
|
if event_type == "file_mutation":
|
|
570
577
|
state = local_state.get_session_state(session_id)
|
|
571
578
|
mutation_id = str(payload.get("mutation_id") or "")
|
|
@@ -96,6 +96,18 @@ NARRATED_TOOL_CORRECTION = (
|
|
|
96
96
|
"Only provide a final answer after the requested inspection or action actually ran."
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
+
# Keep the user-facing completion readable even when a provider streams a
|
|
100
|
+
# long audit. Plans and live progress already have their own durable renderer
|
|
101
|
+
# panels; the model must not recreate those as a second, drifting bullet list.
|
|
102
|
+
FINAL_RESPONSE_FORMAT_INSTRUCTION = (
|
|
103
|
+
"When you finish, write a concise evidence-backed response using exactly these "
|
|
104
|
+
"sections when applicable: Summary, Changes, Verification, and Remaining issues. "
|
|
105
|
+
"Use one short bullet per concrete fact under a section; do not emit nested or "
|
|
106
|
+
"unrelated bullet lists, do not repeat the execution plan, and do not claim a "
|
|
107
|
+
"tool ran unless a real tool result appears in the conversation. If no files "
|
|
108
|
+
"changed, say so plainly under Summary."
|
|
109
|
+
)
|
|
110
|
+
|
|
99
111
|
# Same one-chance-then-fallback shape as narrated tool intent, for the
|
|
100
112
|
# distinct failure of giving up outright instead of narrating.
|
|
101
113
|
MAX_CAPITULATION_RETRIES_PER_PROVIDER = 1
|
|
@@ -1947,7 +1959,7 @@ def _looks_like_capitulation(text: str) -> bool:
|
|
|
1947
1959
|
# same way: refuse the round, ask for a real tool call, and fall back
|
|
1948
1960
|
# across providers the same one-chance-then-switch way as narrated intent.
|
|
1949
1961
|
_FABRICATED_TOOL_RESULT_RE = re.compile(
|
|
1950
|
-
r"the\s+\w+\s+tool\s+(?:has\s+)?(?:found|returned|shows?|revealed|indicates?)|"
|
|
1962
|
+
r"the\s+\w+\s+tool\s+(?:has\s+)?(?:found|returned|executed|ran|shows?|revealed|indicates?)|"
|
|
1951
1963
|
r"(?:the\s+)?results?\s+(?:suggest|indicate)s?\b|"
|
|
1952
1964
|
r"i\s+encountered\s+an?\s+(?:access|permission)\s+issue|"
|
|
1953
1965
|
r"(?:here\s+are\s+the\s+)?allowed\s+directories\s+(?:are|include|where\s+i\s+can)|"
|
|
@@ -2929,6 +2941,10 @@ async def run_local_agent_turn(
|
|
|
2929
2941
|
# it survives later compaction and remains authoritative in every round.
|
|
2930
2942
|
insert_at = 1 if working_messages and working_messages[0].get("role") == "system" else 0
|
|
2931
2943
|
working_messages.insert(insert_at, scope_message)
|
|
2944
|
+
working_messages.insert(insert_at + 1, {
|
|
2945
|
+
"role": "system",
|
|
2946
|
+
"content": FINAL_RESPONSE_FORMAT_INSTRUCTION,
|
|
2947
|
+
})
|
|
2932
2948
|
if resumed_from_checkpoint or resumed_from_legacy or _requests_autonomous_execution(incoming_objective):
|
|
2933
2949
|
working_messages.insert(insert_at + 1, {"role": "system", "content": RESUME_EXECUTION_INSTRUCTION})
|
|
2934
2950
|
checkpoint_mode = str(prior_checkpoint.get("mode") or ("read_only" if read_only else "execute"))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tamfis-code
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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
|
|
@@ -25,6 +25,7 @@ from tamfis_code.runner_local import (
|
|
|
25
25
|
_looks_like_narrated_tool_intent,
|
|
26
26
|
_requests_autonomous_execution,
|
|
27
27
|
_requests_no_confirmation,
|
|
28
|
+
_looks_like_fabricated_tool_result,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
|
|
@@ -55,6 +56,16 @@ class FakeToolCallDetectionTests(unittest.TestCase):
|
|
|
55
56
|
def test_unregistered_function_looking_text_is_not_flagged(self):
|
|
56
57
|
self.assertFalse(_looks_like_fake_tool_call("def calculate_total(items): return sum(items)"))
|
|
57
58
|
|
|
59
|
+
def test_transcript_style_list_directory_claim_is_fabricated_result(self):
|
|
60
|
+
self.assertTrue(_looks_like_fabricated_tool_result(
|
|
61
|
+
"The list_directory tool has found the following files and directories."
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
def test_transcript_style_execute_command_claim_is_fabricated_result(self):
|
|
65
|
+
self.assertTrue(_looks_like_fabricated_tool_result(
|
|
66
|
+
"The execute_command tool has executed the npm test command."
|
|
67
|
+
))
|
|
68
|
+
|
|
58
69
|
|
|
59
70
|
class NarratedToolIntentDetectionTests(unittest.TestCase):
|
|
60
71
|
def test_let_me_check_is_detected(self):
|
|
@@ -96,17 +96,13 @@ def test_openrouter_default_is_not_openai_family():
|
|
|
96
96
|
assert all(not model.startswith("openai/") for model in cfg.models)
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# permanent 404 -- listed in NVIDIA's general /v1/models catalog but
|
|
104
|
-
# with no actual deployment access on an ordinary API key. A default
|
|
105
|
-
# that 404s outright is strictly worse than nemotron's occasional loop.
|
|
99
|
+
def test_nvidia_default_model_is_tool_capable_and_not_unentitled_kimi():
|
|
100
|
+
# The plain Llama route is fluent but has been observed fabricating local
|
|
101
|
+
# tool results. Use the verified NVIDIA reasoning/tool route instead;
|
|
102
|
+
# never use the account-unentitled Kimi route as the default.
|
|
106
103
|
default_model = ProviderManager.PROVIDERS[ProviderType.NVIDIA].default_model
|
|
107
|
-
assert default_model != "nvidia/nemotron-3-super-120b-a12b"
|
|
108
104
|
assert default_model != "moonshotai/kimi-k2.6"
|
|
109
|
-
assert default_model == "
|
|
105
|
+
assert default_model == "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
|
|
110
106
|
|
|
111
107
|
|
|
112
108
|
def test_kimi_k2_6_is_still_selectable_on_openrouter_and_hf():
|
|
@@ -126,6 +122,12 @@ def test_kimi_k2_6_is_still_selectable_on_openrouter_and_hf():
|
|
|
126
122
|
assert ProviderManager.PROVIDERS[ProviderType.HF].default_model != "moonshotai/Kimi-K2.6"
|
|
127
123
|
|
|
128
124
|
|
|
125
|
+
def test_openrouter_paid_coding_default_is_qwen_coder():
|
|
126
|
+
config = ProviderManager.PROVIDERS[ProviderType.OPENROUTER]
|
|
127
|
+
assert config.default_model == "qwen/qwen3-coder"
|
|
128
|
+
assert config.default_model in config.models
|
|
129
|
+
|
|
130
|
+
|
|
129
131
|
def test_remote_fallback_candidates_stay_in_policy_order():
|
|
130
132
|
manager = _manager_with(ProviderType.NVIDIA, ProviderType.OPENROUTER, ProviderType.HF)
|
|
131
133
|
# Use a tool-requiring task that does not require long context, so HF is
|
|
@@ -154,6 +156,21 @@ def test_generic_open_stream_failure_is_retryable():
|
|
|
154
156
|
assert ProviderManager.is_retryable_provider_error(exc)
|
|
155
157
|
|
|
156
158
|
|
|
159
|
+
def test_nvidia_degraded_function_400_is_retryable():
|
|
160
|
+
exc = RuntimeError(
|
|
161
|
+
"Error code: 400 - {'status': 400, 'title': 'Bad Request', "
|
|
162
|
+
"'detail': \"Function id 'abc': DEGRADED function cannot be invoked\"}"
|
|
163
|
+
)
|
|
164
|
+
assert ProviderManager.is_retryable_provider_error(exc)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_unrelated_http_400_is_not_retryable():
|
|
168
|
+
class BadRequest(Exception):
|
|
169
|
+
status_code = 400
|
|
170
|
+
|
|
171
|
+
assert not ProviderManager.is_retryable_provider_error(BadRequest("invalid request"))
|
|
172
|
+
|
|
173
|
+
|
|
157
174
|
def test_nim_account_entitlement_404_is_retryable():
|
|
158
175
|
# Live-reported: moonshotai/kimi-k2.6 returns a real 404 with this exact
|
|
159
176
|
# shape when the account has no deployment access to that specific
|
|
@@ -396,6 +396,32 @@ class StreamRendererTests(unittest.TestCase):
|
|
|
396
396
|
renderer.finish()
|
|
397
397
|
self.assertIn("inspect mission pipeline", console.file.getvalue())
|
|
398
398
|
|
|
399
|
+
def test_plan_progress_is_durable_after_live_panel_stops(self):
|
|
400
|
+
console = Console(file=StringIO(), no_color=True, width=200, force_terminal=True)
|
|
401
|
+
renderer = StreamRenderer(console)
|
|
402
|
+
try:
|
|
403
|
+
renderer.handle_event({
|
|
404
|
+
"event_type": "plan_created",
|
|
405
|
+
"payload": {"title": "Plan", "items": [
|
|
406
|
+
{"step": "Inspect", "status": "pending"},
|
|
407
|
+
{"step": "Fix", "status": "pending"},
|
|
408
|
+
]},
|
|
409
|
+
})
|
|
410
|
+
renderer.handle_event({"event_type": "assistant_delta", "payload": {"content": "Working."}})
|
|
411
|
+
renderer.handle_event({
|
|
412
|
+
"event_type": "plan_step_progress",
|
|
413
|
+
"payload": {"items": [
|
|
414
|
+
{"step": "Inspect", "status": "completed"},
|
|
415
|
+
{"step": "Fix", "status": "in_progress"},
|
|
416
|
+
]},
|
|
417
|
+
})
|
|
418
|
+
finally:
|
|
419
|
+
renderer.finish()
|
|
420
|
+
output = console.file.getvalue()
|
|
421
|
+
self.assertIn("Plan progress", output)
|
|
422
|
+
self.assertIn("Inspect · completed", output)
|
|
423
|
+
self.assertIn("Fix · in_progress", output)
|
|
424
|
+
|
|
399
425
|
def test_approval_gate_suspends_and_resumes_the_live_status_line(self):
|
|
400
426
|
# Regression guard: Rich's Live redraws on its own timer, independent
|
|
401
427
|
# of a blocking console.input() approval prompt -- without suspending
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|