tamfis-code 0.2.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.2.3/PKG-INFO +78 -0
- tamfis_code-0.2.3/README.md +59 -0
- tamfis_code-0.2.3/pyproject.toml +44 -0
- tamfis_code-0.2.3/setup.cfg +4 -0
- tamfis_code-0.2.3/tamfis_code/__init__.py +58 -0
- tamfis_code-0.2.3/tamfis_code/__main__.py +4 -0
- tamfis_code-0.2.3/tamfis_code/agents.py +371 -0
- tamfis_code-0.2.3/tamfis_code/api_client.py +461 -0
- tamfis_code-0.2.3/tamfis_code/cli.py +2351 -0
- tamfis_code-0.2.3/tamfis_code/completion.py +137 -0
- tamfis_code-0.2.3/tamfis_code/config.py +199 -0
- tamfis_code-0.2.3/tamfis_code/doctor.py +173 -0
- tamfis_code-0.2.3/tamfis_code/enforcer.py +318 -0
- tamfis_code-0.2.3/tamfis_code/indexer.py +567 -0
- tamfis_code-0.2.3/tamfis_code/instructions.py +121 -0
- tamfis_code-0.2.3/tamfis_code/interactive.py +985 -0
- tamfis_code-0.2.3/tamfis_code/local_chat.py +117 -0
- tamfis_code-0.2.3/tamfis_code/local_tools.py +93 -0
- tamfis_code-0.2.3/tamfis_code/mcp.py +538 -0
- tamfis_code-0.2.3/tamfis_code/metrics.py +105 -0
- tamfis_code-0.2.3/tamfis_code/providers.py +423 -0
- tamfis_code-0.2.3/tamfis_code/references.py +336 -0
- tamfis_code-0.2.3/tamfis_code/render.py +575 -0
- tamfis_code-0.2.3/tamfis_code/runner.py +635 -0
- tamfis_code-0.2.3/tamfis_code/runner_local.py +364 -0
- tamfis_code-0.2.3/tamfis_code/safety.py +164 -0
- tamfis_code-0.2.3/tamfis_code/screenshot.py +382 -0
- tamfis_code-0.2.3/tamfis_code/sessions.py +253 -0
- tamfis_code-0.2.3/tamfis_code/state.py +449 -0
- tamfis_code-0.2.3/tamfis_code/tasks.py +42 -0
- tamfis_code-0.2.3/tamfis_code/workspace.py +329 -0
- tamfis_code-0.2.3/tamfis_code.egg-info/PKG-INFO +78 -0
- tamfis_code-0.2.3/tamfis_code.egg-info/SOURCES.txt +66 -0
- tamfis_code-0.2.3/tamfis_code.egg-info/dependency_links.txt +1 -0
- tamfis_code-0.2.3/tamfis_code.egg-info/entry_points.txt +4 -0
- tamfis_code-0.2.3/tamfis_code.egg-info/requires.txt +11 -0
- tamfis_code-0.2.3/tamfis_code.egg-info/top_level.txt +1 -0
- tamfis_code-0.2.3/tests/test_agents.py +248 -0
- tamfis_code-0.2.3/tests/test_agents_delegation.py +83 -0
- tamfis_code-0.2.3/tests/test_cli_commands.py +381 -0
- tamfis_code-0.2.3/tests/test_completion.py +49 -0
- tamfis_code-0.2.3/tests/test_doctor_session_diagnostics.py +111 -0
- tamfis_code-0.2.3/tests/test_enforcer.py +149 -0
- tamfis_code-0.2.3/tests/test_indexer.py +109 -0
- tamfis_code-0.2.3/tests/test_instructions.py +75 -0
- tamfis_code-0.2.3/tests/test_integration_new.py +101 -0
- tamfis_code-0.2.3/tests/test_interactive_standalone.py +173 -0
- tamfis_code-0.2.3/tests/test_local_mode.py +144 -0
- tamfis_code-0.2.3/tests/test_mcp.py +208 -0
- tamfis_code-0.2.3/tests/test_mcp_standalone_degradation.py +46 -0
- tamfis_code-0.2.3/tests/test_metrics.py +80 -0
- tamfis_code-0.2.3/tests/test_plan_steps_and_attempts.py +110 -0
- tamfis_code-0.2.3/tests/test_references.py +202 -0
- tamfis_code-0.2.3/tests/test_render_task_diagnostics.py +53 -0
- tamfis_code-0.2.3/tests/test_runner_helpers.py +155 -0
- tamfis_code-0.2.3/tests/test_runner_local.py +237 -0
- tamfis_code-0.2.3/tests/test_safety.py +158 -0
- tamfis_code-0.2.3/tests/test_sessions.py +140 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_api_client.py +174 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_approval.py +106 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_approval_flow.py +229 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_config.py +215 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_intent.py +71 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_render.py +181 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_repl_exit.py +70 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_tasks.py +77 -0
- tamfis_code-0.2.3/tests/test_tamfis_code_workspace.py +260 -0
- tamfis_code-0.2.3/tests/test_workspace_discovery_gaps.py +119 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tamfis-code
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: TamfisGPT Code -- standalone terminal coding agent (calls HF/NVIDIA NIM/OpenRouter/Ollama directly; --remote for the legacy TamfisGPT Remote backend)
|
|
5
|
+
Project-URL: Homepage, https://github.com/tamfitronics/tamfis-code
|
|
6
|
+
Project-URL: Repository, https://github.com/tamfitronics/tamfis-code
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: click>=8.1
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
13
|
+
Requires-Dist: openai>=1.30
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# tamfis-code
|
|
21
|
+
|
|
22
|
+
A standalone terminal coding agent. By default it calls an LLM provider
|
|
23
|
+
directly — Hugging Face, NVIDIA NIM, OpenRouter, or Ollama — and runs its
|
|
24
|
+
own agent loop, tool execution, and local risk/approval/mutation-ledger
|
|
25
|
+
safety layer, with no separate backend process required.
|
|
26
|
+
|
|
27
|
+
`--remote` (or a persistent `default_backend = "remote"` config setting)
|
|
28
|
+
switches to the original architecture: a thin client to the TamfisGPT
|
|
29
|
+
Remote Workspace backend, for TamfisGPT tenants using their hosted account
|
|
30
|
+
the same way Codex CLI uses a ChatGPT/OpenAI account, kimi-code uses a Kimi
|
|
31
|
+
account, or Claude Code uses a Claude account.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pipx install tamfis-code
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See [USAGE_INSTALL_RELEASE.md](./USAGE_INSTALL_RELEASE.md) for installing
|
|
40
|
+
from source, using a TamfisGPT tenancy, and the release process.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
export OLLAMA_BASE_URL=http://localhost:11434/v1 # or set HF_TOKEN / NVIDIA_API_KEY / OPENROUTER_API_KEY
|
|
46
|
+
tamfis-code doctor # check provider connectivity
|
|
47
|
+
tamfis-code ask "explain what this repo does"
|
|
48
|
+
tamfis-code agent "add a health-check endpoint" # full read/write/execute loop
|
|
49
|
+
tamfis-code # interactive REPL
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Providers
|
|
53
|
+
|
|
54
|
+
| Provider | Env var | Notes |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Ollama | `OLLAMA_BASE_URL` (default `http://localhost:11434/v1`) | Runs fully on-device, no API key |
|
|
57
|
+
| Hugging Face | `HF_TOKEN` | |
|
|
58
|
+
| NVIDIA NIM | `NVIDIA_API_KEY` | |
|
|
59
|
+
| OpenRouter | `OPENROUTER_API_KEY` | |
|
|
60
|
+
|
|
61
|
+
Select one explicitly with `--provider hf|nvidia|openrouter|ollama`, or
|
|
62
|
+
leave it as `auto` (default) to pick the best configured one.
|
|
63
|
+
|
|
64
|
+
## Safety model
|
|
65
|
+
|
|
66
|
+
Every mutating tool call (`write_file`, `edit_file`, `execute_command`) is
|
|
67
|
+
risk-classified locally (`tamfis_code/safety.py`), gated by
|
|
68
|
+
`--approval-policy` (or the `/mode` REPL command), and recorded in a local
|
|
69
|
+
mutation ledger — see `tamfis-code diffs` / `tamfis-code diff` /
|
|
70
|
+
`tamfis-code revert`. There is no sandboxing beyond risk classification;
|
|
71
|
+
review dangerous commands yourself before approving them.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
TBD — decide before the first public PyPI release (a public package still
|
|
76
|
+
needs a stated license; "all rights reserved"/proprietary is a valid choice
|
|
77
|
+
if you don't want redistribution/modification, but it should be explicit
|
|
78
|
+
rather than absent).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# tamfis-code
|
|
2
|
+
|
|
3
|
+
A standalone terminal coding agent. By default it calls an LLM provider
|
|
4
|
+
directly — Hugging Face, NVIDIA NIM, OpenRouter, or Ollama — and runs its
|
|
5
|
+
own agent loop, tool execution, and local risk/approval/mutation-ledger
|
|
6
|
+
safety layer, with no separate backend process required.
|
|
7
|
+
|
|
8
|
+
`--remote` (or a persistent `default_backend = "remote"` config setting)
|
|
9
|
+
switches to the original architecture: a thin client to the TamfisGPT
|
|
10
|
+
Remote Workspace backend, for TamfisGPT tenants using their hosted account
|
|
11
|
+
the same way Codex CLI uses a ChatGPT/OpenAI account, kimi-code uses a Kimi
|
|
12
|
+
account, or Claude Code uses a Claude account.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pipx install tamfis-code
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
See [USAGE_INSTALL_RELEASE.md](./USAGE_INSTALL_RELEASE.md) for installing
|
|
21
|
+
from source, using a TamfisGPT tenancy, and the release process.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
export OLLAMA_BASE_URL=http://localhost:11434/v1 # or set HF_TOKEN / NVIDIA_API_KEY / OPENROUTER_API_KEY
|
|
27
|
+
tamfis-code doctor # check provider connectivity
|
|
28
|
+
tamfis-code ask "explain what this repo does"
|
|
29
|
+
tamfis-code agent "add a health-check endpoint" # full read/write/execute loop
|
|
30
|
+
tamfis-code # interactive REPL
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Providers
|
|
34
|
+
|
|
35
|
+
| Provider | Env var | Notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Ollama | `OLLAMA_BASE_URL` (default `http://localhost:11434/v1`) | Runs fully on-device, no API key |
|
|
38
|
+
| Hugging Face | `HF_TOKEN` | |
|
|
39
|
+
| NVIDIA NIM | `NVIDIA_API_KEY` | |
|
|
40
|
+
| OpenRouter | `OPENROUTER_API_KEY` | |
|
|
41
|
+
|
|
42
|
+
Select one explicitly with `--provider hf|nvidia|openrouter|ollama`, or
|
|
43
|
+
leave it as `auto` (default) to pick the best configured one.
|
|
44
|
+
|
|
45
|
+
## Safety model
|
|
46
|
+
|
|
47
|
+
Every mutating tool call (`write_file`, `edit_file`, `execute_command`) is
|
|
48
|
+
risk-classified locally (`tamfis_code/safety.py`), gated by
|
|
49
|
+
`--approval-policy` (or the `/mode` REPL command), and recorded in a local
|
|
50
|
+
mutation ledger — see `tamfis-code diffs` / `tamfis-code diff` /
|
|
51
|
+
`tamfis-code revert`. There is no sandboxing beyond risk classification;
|
|
52
|
+
review dangerous commands yourself before approving them.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
TBD — decide before the first public PyPI release (a public package still
|
|
57
|
+
needs a stated license; "all rights reserved"/proprietary is a valid choice
|
|
58
|
+
if you don't want redistribution/modification, but it should be explicit
|
|
59
|
+
rather than absent).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tamfis-code"
|
|
3
|
+
version = "0.2.3"
|
|
4
|
+
description = "TamfisGPT Code -- standalone terminal coding agent (calls HF/NVIDIA NIM/OpenRouter/Ollama directly; --remote for the legacy TamfisGPT Remote backend)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"click>=8.1",
|
|
9
|
+
"httpx>=0.27",
|
|
10
|
+
"rich>=13.0",
|
|
11
|
+
"prompt_toolkit>=3.0",
|
|
12
|
+
# Provider calls (providers.py) go through the openai SDK's client against
|
|
13
|
+
# each vendor's OpenAI-compatible endpoint (HF, NVIDIA NIM, OpenRouter,
|
|
14
|
+
# Ollama) -- this was imported but never declared, only working because a
|
|
15
|
+
# copy happened to already be installed system-wide.
|
|
16
|
+
"openai>=1.30",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
tamfis-code = "tamfis_code.cli:main"
|
|
21
|
+
tamgpt-code = "tamfis_code.cli:main"
|
|
22
|
+
tamfis = "tamfis_code.cli:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/tamfitronics/tamfis-code"
|
|
26
|
+
Repository = "https://github.com/tamfitronics/tamfis-code"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"build>=1.0",
|
|
31
|
+
"twine>=5.0",
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
# tests/test_agents.py and tests/test_mcp.py use @pytest.mark.asyncio --
|
|
34
|
+
# this was only working locally because a copy happened to already be
|
|
35
|
+
# installed system-wide (same class of gap as the openai dependency above).
|
|
36
|
+
"pytest-asyncio>=0.24",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["setuptools>=68"]
|
|
41
|
+
build-backend = "setuptools.build_meta"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
packages = ["tamfis_code"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""TamfisGPT Code -- a standalone terminal coding agent.
|
|
2
|
+
|
|
3
|
+
By default, this package calls an LLM provider directly (Hugging Face,
|
|
4
|
+
NVIDIA NIM, OpenRouter, or Ollama -- see providers.py/runner_local.py) and
|
|
5
|
+
runs its own agent loop, tool execution (mcp.py), and local risk
|
|
6
|
+
classification/approval/mutation ledger (safety.py) -- no separate backend
|
|
7
|
+
process required. Every `ask`/`chat`/`audit`/`plan`/`agent`/`exec` command,
|
|
8
|
+
and the interactive REPL, work this way unless `--remote` is passed.
|
|
9
|
+
|
|
10
|
+
`--remote` still supports the original architecture this package started
|
|
11
|
+
as: a thin client to the TamfisGPT Remote Workspace backend (same sessions/
|
|
12
|
+
tasks/tools/approvals/events -- see tier_ii_gateway/api/remote.py), which
|
|
13
|
+
does the equivalent work server-side. That path is kept for continuity but
|
|
14
|
+
is not the primary, developed-going-forward one.
|
|
15
|
+
|
|
16
|
+
See docs/REMOTE_AGENT_MASTER_SPEC.md, Phase 21, for the original --remote
|
|
17
|
+
architecture's spec.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.3"
|
|
21
|
+
|
|
22
|
+
# Bumped whenever a CLI release requires a minimum backend Remote API
|
|
23
|
+
# contract version. Only meaningful for --remote; the standalone path has no
|
|
24
|
+
# server to negotiate a contract version with. There is no server-side
|
|
25
|
+
# version negotiation yet (`tamfis-code doctor --remote` just checks
|
|
26
|
+
# reachability) -- this constant exists so that check has something concrete
|
|
27
|
+
# to compare against once one is added, rather than silently assuming
|
|
28
|
+
# compatibility forever.
|
|
29
|
+
MIN_COMPATIBLE_API_VERSION = "remote-ai-v2"
|
|
30
|
+
|
|
31
|
+
# New exports
|
|
32
|
+
from .completion import ShellCompleter
|
|
33
|
+
from .metrics import MetricsTracker, StreamMetrics
|
|
34
|
+
from .sessions import SessionManager, Session, Message
|
|
35
|
+
from .agents import AgentManager, SubAgent, CodeAnalyzer, TestGenerator, DocGenerator
|
|
36
|
+
from .mcp import MCPServer, ToolDefinition, call_tool
|
|
37
|
+
from .indexer import CodeIndexer, CodeSymbol, CodeFile
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Existing...
|
|
41
|
+
'ShellCompleter',
|
|
42
|
+
'MetricsTracker',
|
|
43
|
+
'StreamMetrics',
|
|
44
|
+
'SessionManager',
|
|
45
|
+
'Session',
|
|
46
|
+
'Message',
|
|
47
|
+
'AgentManager',
|
|
48
|
+
'SubAgent',
|
|
49
|
+
'CodeAnalyzer',
|
|
50
|
+
'TestGenerator',
|
|
51
|
+
'DocGenerator',
|
|
52
|
+
'MCPServer',
|
|
53
|
+
'ToolDefinition',
|
|
54
|
+
'call_tool',
|
|
55
|
+
'CodeIndexer',
|
|
56
|
+
'CodeSymbol',
|
|
57
|
+
'CodeFile',
|
|
58
|
+
]
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Subagent system for autonomous task execution"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import asyncio
|
|
7
|
+
import subprocess
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path # <-- Add this import
|
|
12
|
+
|
|
13
|
+
class AgentStatus(Enum):
|
|
14
|
+
IDLE = "idle"
|
|
15
|
+
RUNNING = "running"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
WAITING = "waiting"
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AgentTask:
|
|
22
|
+
"""A task for a subagent"""
|
|
23
|
+
id: str
|
|
24
|
+
description: str
|
|
25
|
+
parameters: Dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
status: AgentStatus = AgentStatus.IDLE
|
|
27
|
+
result: Optional[Dict[str, Any]] = None
|
|
28
|
+
error: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
class SubAgent:
|
|
31
|
+
"""Base class for subagents"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, name: str, description: str, capabilities: List[str]):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.description = description
|
|
36
|
+
self.capabilities = capabilities
|
|
37
|
+
self.current_task: Optional[AgentTask] = None
|
|
38
|
+
|
|
39
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
40
|
+
"""Execute a task - to be overridden"""
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
def can_handle(self, task_description: str) -> bool:
|
|
44
|
+
"""Check if agent can handle this task"""
|
|
45
|
+
task_lower = task_description.lower()
|
|
46
|
+
for cap in self.capabilities:
|
|
47
|
+
if cap.lower() in task_lower:
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
class CodeAnalyzer(SubAgent):
|
|
52
|
+
"""Analyzes code structure and quality"""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__(
|
|
56
|
+
name="code_analyzer",
|
|
57
|
+
description="Analyzes code for patterns, issues, and complexity",
|
|
58
|
+
capabilities=["analyze", "inspect", "complexity", "quality", "metrics"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
62
|
+
"""Analyze code based on task parameters"""
|
|
63
|
+
file_path = task.parameters.get('file')
|
|
64
|
+
if not file_path:
|
|
65
|
+
return {"error": "No file specified"}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with open(file_path, 'r') as f:
|
|
69
|
+
content = f.read()
|
|
70
|
+
|
|
71
|
+
lines = content.split('\n')
|
|
72
|
+
result = {
|
|
73
|
+
"file": file_path,
|
|
74
|
+
"lines": len(lines),
|
|
75
|
+
"characters": len(content),
|
|
76
|
+
"functions": self._count_functions(content),
|
|
77
|
+
"classes": self._count_classes(content),
|
|
78
|
+
"imports": self._count_imports(content),
|
|
79
|
+
"complexity_score": self._calculate_complexity(content),
|
|
80
|
+
"issues": self._find_issues(content),
|
|
81
|
+
}
|
|
82
|
+
return result
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return {"error": str(e)}
|
|
85
|
+
|
|
86
|
+
def _count_functions(self, content: str) -> int:
|
|
87
|
+
return len(re.findall(r'^\s*def\s+\w+\s*\(', content, re.MULTILINE))
|
|
88
|
+
|
|
89
|
+
def _count_classes(self, content: str) -> int:
|
|
90
|
+
return len(re.findall(r'^\s*class\s+\w+', content, re.MULTILINE))
|
|
91
|
+
|
|
92
|
+
def _count_imports(self, content: str) -> int:
|
|
93
|
+
return len(re.findall(r'^\s*(?:from|import)\s+\w+', content, re.MULTILINE))
|
|
94
|
+
|
|
95
|
+
def _calculate_complexity(self, content: str) -> float:
|
|
96
|
+
lines = content.split('\n')
|
|
97
|
+
complexity = 1
|
|
98
|
+
keywords = ['if', 'elif', 'else', 'for', 'while', 'except', 'case', 'switch', '?']
|
|
99
|
+
for line in lines:
|
|
100
|
+
if any(k in line for k in keywords):
|
|
101
|
+
complexity += 1
|
|
102
|
+
return complexity
|
|
103
|
+
|
|
104
|
+
def _find_issues(self, content: str) -> List[Dict[str, Any]]:
|
|
105
|
+
issues = []
|
|
106
|
+
lines = content.split('\n')
|
|
107
|
+
|
|
108
|
+
for i, line in enumerate(lines, 1):
|
|
109
|
+
if len(line) > 120:
|
|
110
|
+
issues.append({
|
|
111
|
+
"line": i,
|
|
112
|
+
"type": "line_too_long",
|
|
113
|
+
"message": f"Line {i} exceeds 120 characters ({len(line)})"
|
|
114
|
+
})
|
|
115
|
+
if line.strip().startswith('#') and 'TODO' in line:
|
|
116
|
+
issues.append({
|
|
117
|
+
"line": i,
|
|
118
|
+
"type": "todo",
|
|
119
|
+
"message": f"TODO: {line.strip()}"
|
|
120
|
+
})
|
|
121
|
+
if line.strip().startswith('#') and 'FIXME' in line:
|
|
122
|
+
issues.append({
|
|
123
|
+
"line": i,
|
|
124
|
+
"type": "fixme",
|
|
125
|
+
"message": f"FIXME: {line.strip()}"
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return issues
|
|
129
|
+
|
|
130
|
+
class TestGenerator(SubAgent):
|
|
131
|
+
"""Generates tests for code"""
|
|
132
|
+
|
|
133
|
+
def __init__(self):
|
|
134
|
+
super().__init__(
|
|
135
|
+
name="test_generator",
|
|
136
|
+
description="Generates unit tests for code",
|
|
137
|
+
capabilities=["test", "unit test", "coverage", "pytest"]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
141
|
+
"""Generate tests for specified file"""
|
|
142
|
+
file_path = task.parameters.get('file')
|
|
143
|
+
if not file_path:
|
|
144
|
+
return {"error": "No file specified"}
|
|
145
|
+
|
|
146
|
+
# In production, this would use LLM to generate tests
|
|
147
|
+
# For now, return a template
|
|
148
|
+
funcs = self._extract_functions(file_path)
|
|
149
|
+
return {
|
|
150
|
+
"file": file_path,
|
|
151
|
+
"functions_found": len(funcs),
|
|
152
|
+
"functions": funcs,
|
|
153
|
+
"test_file": f"test_{Path(file_path).name}",
|
|
154
|
+
"status": "ready_for_generation"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def _extract_functions(self, file_path: str) -> List[str]:
|
|
158
|
+
try:
|
|
159
|
+
with open(file_path, 'r') as f:
|
|
160
|
+
content = f.read()
|
|
161
|
+
return re.findall(r'def\s+(\w+)\s*\(', content)
|
|
162
|
+
except:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
class DocGenerator(SubAgent):
|
|
166
|
+
"""Generates documentation for code"""
|
|
167
|
+
|
|
168
|
+
def __init__(self):
|
|
169
|
+
super().__init__(
|
|
170
|
+
name="doc_generator",
|
|
171
|
+
description="Generates documentation for code",
|
|
172
|
+
capabilities=["doc", "documentation", "comment", "docstring"]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
176
|
+
"""Generate documentation for specified file"""
|
|
177
|
+
file_path = task.parameters.get('file')
|
|
178
|
+
if not file_path:
|
|
179
|
+
return {"error": "No file specified"}
|
|
180
|
+
|
|
181
|
+
funcs = self._extract_with_docstrings(file_path)
|
|
182
|
+
return {
|
|
183
|
+
"file": file_path,
|
|
184
|
+
"functions": funcs,
|
|
185
|
+
"missing_docs": [f for f in funcs if not f.get('docstring')],
|
|
186
|
+
"status": "ready_for_review"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def _extract_with_docstrings(self, file_path: str) -> List[Dict[str, Any]]:
|
|
190
|
+
try:
|
|
191
|
+
with open(file_path, 'r') as f:
|
|
192
|
+
content = f.read()
|
|
193
|
+
|
|
194
|
+
results = []
|
|
195
|
+
lines = content.split('\n')
|
|
196
|
+
i = 0
|
|
197
|
+
while i < len(lines):
|
|
198
|
+
line = lines[i]
|
|
199
|
+
func_match = re.match(r'^\s*def\s+(\w+)\s*\(', line)
|
|
200
|
+
if func_match:
|
|
201
|
+
func_name = func_match.group(1)
|
|
202
|
+
docstring = None
|
|
203
|
+
# Look ahead for docstring
|
|
204
|
+
j = i + 1
|
|
205
|
+
while j < len(lines) and j < i + 3:
|
|
206
|
+
if '"""' in lines[j] or "'''" in lines[j]:
|
|
207
|
+
docstring = lines[j].strip()
|
|
208
|
+
break
|
|
209
|
+
j += 1
|
|
210
|
+
results.append({
|
|
211
|
+
'name': func_name,
|
|
212
|
+
'line': i + 1,
|
|
213
|
+
'docstring': docstring
|
|
214
|
+
})
|
|
215
|
+
i += 1
|
|
216
|
+
return results
|
|
217
|
+
except:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
class DelegatedCodingAgent(SubAgent):
|
|
221
|
+
"""Delegates its task to the real standalone agent loop (runner_local.py --
|
|
222
|
+
the same one `tamfis-code agent`/`exec`/`ask` and the interactive REPL
|
|
223
|
+
drive), instead of a local heuristic.
|
|
224
|
+
|
|
225
|
+
Unlike CodeAnalyzer/TestGenerator/DocGenerator, this one can actually act
|
|
226
|
+
on an arbitrary objective via the model -- calling a provider directly
|
|
227
|
+
and executing tools locally, with its own workspace/session. It's never
|
|
228
|
+
selected by AgentManager's keyword-based `get_agent()` routing
|
|
229
|
+
(capabilities=[]) since it's only meant to be dispatched explicitly via
|
|
230
|
+
`execute_tasks`.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self, *, manager, provider, model, console, workspace_root: str, session_id: int,
|
|
235
|
+
approval_policy: str = "ask", mode: str = "agent",
|
|
236
|
+
):
|
|
237
|
+
super().__init__(
|
|
238
|
+
name="delegated_coding_agent",
|
|
239
|
+
description="Delegates a sub-objective to the real standalone agent loop",
|
|
240
|
+
capabilities=[],
|
|
241
|
+
)
|
|
242
|
+
self._manager = manager
|
|
243
|
+
self._provider = provider
|
|
244
|
+
self._model = model
|
|
245
|
+
self._console = console
|
|
246
|
+
self._workspace_root = workspace_root
|
|
247
|
+
self._session_id = session_id
|
|
248
|
+
self._approval_policy = approval_policy
|
|
249
|
+
self._mode = mode
|
|
250
|
+
|
|
251
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
252
|
+
from .render import StreamRenderer
|
|
253
|
+
from .runner_local import run_local_agent_turn
|
|
254
|
+
|
|
255
|
+
renderer = StreamRenderer(self._console)
|
|
256
|
+
outcome = await run_local_agent_turn(
|
|
257
|
+
self._manager, self._provider, self._model, [{"role": "user", "content": task.description}],
|
|
258
|
+
self._console, renderer,
|
|
259
|
+
workspace_root=self._workspace_root, session_id=self._session_id,
|
|
260
|
+
approval_policy=self._approval_policy, interactive=False,
|
|
261
|
+
read_only=self._mode in {"chat", "audit", "plan"},
|
|
262
|
+
)
|
|
263
|
+
renderer.finish()
|
|
264
|
+
return {"status": outcome.status, "summary": outcome.summary, "error": outcome.error}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class AgentManager:
|
|
268
|
+
"""Manages subagents and task execution"""
|
|
269
|
+
|
|
270
|
+
def __init__(self):
|
|
271
|
+
self.agents: Dict[str, SubAgent] = {}
|
|
272
|
+
self.tasks: Dict[str, AgentTask] = {}
|
|
273
|
+
self._register_default_agents()
|
|
274
|
+
|
|
275
|
+
def _register_default_agents(self):
|
|
276
|
+
"""Register default agents"""
|
|
277
|
+
self.register(CodeAnalyzer())
|
|
278
|
+
self.register(TestGenerator())
|
|
279
|
+
self.register(DocGenerator())
|
|
280
|
+
|
|
281
|
+
def register(self, agent: SubAgent):
|
|
282
|
+
"""Register a subagent"""
|
|
283
|
+
self.agents[agent.name] = agent
|
|
284
|
+
|
|
285
|
+
def list_agents(self) -> List[Dict[str, Any]]:
|
|
286
|
+
"""List all registered agents"""
|
|
287
|
+
return [
|
|
288
|
+
{
|
|
289
|
+
"name": agent.name,
|
|
290
|
+
"description": agent.description,
|
|
291
|
+
"capabilities": agent.capabilities,
|
|
292
|
+
"status": "ready"
|
|
293
|
+
}
|
|
294
|
+
for agent in self.agents.values()
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
def get_agent(self, task_description: str) -> Optional[SubAgent]:
|
|
298
|
+
"""Find an agent that can handle the task"""
|
|
299
|
+
for agent in self.agents.values():
|
|
300
|
+
if agent.can_handle(task_description):
|
|
301
|
+
return agent
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
async def execute_task(self, description: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
305
|
+
"""Execute a task using the appropriate agent"""
|
|
306
|
+
import uuid
|
|
307
|
+
|
|
308
|
+
task = AgentTask(
|
|
309
|
+
id=str(uuid.uuid4())[:8],
|
|
310
|
+
description=description,
|
|
311
|
+
parameters=parameters,
|
|
312
|
+
status=AgentStatus.RUNNING
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
agent = self.get_agent(description)
|
|
316
|
+
if not agent:
|
|
317
|
+
task.status = AgentStatus.FAILED
|
|
318
|
+
task.error = "No agent can handle this task"
|
|
319
|
+
return {"error": task.error}
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
agent.current_task = task
|
|
323
|
+
result = await agent.execute(task)
|
|
324
|
+
task.result = result
|
|
325
|
+
task.status = AgentStatus.COMPLETED
|
|
326
|
+
return {"task_id": task.id, "agent": agent.name, "result": result}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
task.status = AgentStatus.FAILED
|
|
329
|
+
task.error = str(e)
|
|
330
|
+
return {"error": str(e), "task_id": task.id}
|
|
331
|
+
|
|
332
|
+
async def execute_tasks(
|
|
333
|
+
self, descriptions: List[str], *,
|
|
334
|
+
manager, provider, model, console, workspace_root, approval_policy: str = "ask",
|
|
335
|
+
mode: str = "agent", max_concurrency: int = 1,
|
|
336
|
+
) -> List[Dict[str, Any]]:
|
|
337
|
+
"""Fan out N sub-objectives concurrently (bounded by max_concurrency),
|
|
338
|
+
each delegated to the standalone agent loop in its own local session.
|
|
339
|
+
|
|
340
|
+
Defaults to sequential (max_concurrency=1): whether concurrent tool
|
|
341
|
+
execution against the same workspace is safe in every case (two
|
|
342
|
+
sub-tasks editing overlapping files, concurrent state.json writers
|
|
343
|
+
from separate local sessions) hasn't been stress-tested -- raise the
|
|
344
|
+
cap only once you've validated that for your own workloads.
|
|
345
|
+
"""
|
|
346
|
+
from .workspace import resolve_local_workspace
|
|
347
|
+
|
|
348
|
+
semaphore = asyncio.Semaphore(max(1, max_concurrency))
|
|
349
|
+
|
|
350
|
+
async def run_one(description: str) -> Dict[str, Any]:
|
|
351
|
+
async with semaphore:
|
|
352
|
+
task = AgentTask(id=f"delegated_{uuid.uuid4().hex[:8]}", description=description, status=AgentStatus.RUNNING)
|
|
353
|
+
self.tasks[task.id] = task
|
|
354
|
+
try:
|
|
355
|
+
workspace = resolve_local_workspace(Path(workspace_root), discover=False)
|
|
356
|
+
agent = DelegatedCodingAgent(
|
|
357
|
+
manager=manager, provider=provider, model=model, console=console,
|
|
358
|
+
workspace_root=workspace.workspace_root, session_id=workspace.session_id,
|
|
359
|
+
approval_policy=approval_policy, mode=mode,
|
|
360
|
+
)
|
|
361
|
+
result = await agent.execute(task)
|
|
362
|
+
task.result = result
|
|
363
|
+
task.status = AgentStatus.COMPLETED if result.get("status") == "completed" else AgentStatus.FAILED
|
|
364
|
+
task.error = result.get("error")
|
|
365
|
+
except Exception as e:
|
|
366
|
+
result = {"error": str(e)}
|
|
367
|
+
task.status = AgentStatus.FAILED
|
|
368
|
+
task.error = str(e)
|
|
369
|
+
return {"task_id": task.id, "description": description, "status": task.status.value, "result": result}
|
|
370
|
+
|
|
371
|
+
return list(await asyncio.gather(*(run_one(description) for description in descriptions)))
|