jac-coder 0.2.0__tar.gz → 0.2.1__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.
- {jac_coder-0.2.0 → jac_coder-0.2.1}/PKG-INFO +1 -1
- jac_coder-0.2.1/README.md +158 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/api.impl.jac +18 -29
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/api.jac +1 -6
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/core/nodes.impl.jac +12 -1
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/core/nodes.jac +11 -17
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/core/walkers.impl.jac +0 -12
- jac_coder-0.2.1/jac_coder/infra/kv.jac +111 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/infra/mcp_manager.impl.jac +11 -12
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/lib/coder.impl.jac +91 -11
- jac_coder-0.2.1/jac_coder/runtime/cost_tracker.impl.jac +223 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/cost_tracker.jac +12 -12
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/events.jac +69 -5
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/prompt.jac +19 -4
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/skills.impl.jac +5 -5
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/skills.jac +7 -2
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/ROADMAP.md +1 -1
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-components/SKILL.md +3 -1
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-core-cheatsheet/SKILL.md +1 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-fullstack-patterns/SKILL.md +3 -1
- jac_coder-0.2.1/jac_coder/skills/jac-scaffold/SKILL.md +71 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/__init__.jac +0 -1
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/git.impl.jac +15 -14
- jac_coder-0.2.1/jac_coder/tool/git.jac +34 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/delegation.impl.jac +6 -1
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/delegation.jac +1 -0
- jac_coder-0.2.1/jac_coder/tool/meta/think.impl.jac +19 -0
- jac_coder-0.2.1/jac_coder/tool/meta/think.jac +9 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/filesystem.impl.jac +15 -11
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/write/checked.impl.jac +3 -5
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder.egg-info/PKG-INFO +1 -1
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder.egg-info/SOURCES.txt +2 -11
- {jac_coder-0.2.0 → jac_coder-0.2.1}/pyproject.toml +1 -1
- jac_coder-0.2.0/README.md +0 -141
- jac_coder-0.2.0/jac_coder/runtime/cost_tracker.impl.jac +0 -134
- jac_coder-0.2.0/jac_coder/skills/jac-scaffold/SKILL.md +0 -50
- jac_coder-0.2.0/jac_coder/tool/git.jac +0 -18
- jac_coder-0.2.0/jac_coder/tool/meta/think.impl.jac +0 -4
- jac_coder-0.2.0/jac_coder/tool/meta/think.jac +0 -5
- jac_coder-0.2.0/jac_coder/tool/write/scaffold.impl.jac +0 -236
- jac_coder-0.2.0/jac_coder/tool/write/scaffold.jac +0 -12
- jac_coder-0.2.0/tests/test_context.py +0 -53
- jac_coder-0.2.0/tests/test_events.py +0 -40
- jac_coder-0.2.0/tests/test_graph.py +0 -33
- jac_coder-0.2.0/tests/test_interact.py +0 -40
- jac_coder-0.2.0/tests/test_jaccoder.py +0 -72
- jac_coder-0.2.0/tests/test_memory.py +0 -53
- jac_coder-0.2.0/tests/test_selfcorrect.py +0 -72
- jac_coder-0.2.0/tests/test_tools.py +0 -45
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/__init__.py +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/cli_entry.py +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/core/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/core/walkers.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/infra/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/infra/config.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/infra/config.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/infra/mcp_manager.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/lib/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/lib/coder.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/context.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/context.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/memory.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/memory.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/permission.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/permission.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/runtime/prompt.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/serve_entry.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/server.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-by-llm/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-auth/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-organization/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-routing/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-has-fields/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-impl-files/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-node-edge-patterns/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-sv-auth/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-sv-endpoints/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-sv-persistence/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-types/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/skills/jac-walker-patterns/SKILL.md +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/mcp.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/mcp.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/question.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/question.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/task.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/task.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/todo.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/todo.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/validate.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/meta/validate.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/net/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/net/preview.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/net/preview.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/net/web.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/net/web.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/filesystem.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/jac_analyzer.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/load_jac_skill.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/load_jac_skill.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/search.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/read/search.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/guarded.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/guarded.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/jac_tools.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/shell.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/run/shell.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/write/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/tool/write/checked.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/util/__init__.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/util/colors.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/util/sandbox.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/util/sandbox.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/util/tool_output.impl.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder/util/tool_output.jac +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder.egg-info/dependency_links.txt +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder.egg-info/entry_points.txt +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder.egg-info/requires.txt +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/jac_coder.egg-info/top_level.txt +0 -0
- {jac_coder-0.2.0 → jac_coder-0.2.1}/setup.cfg +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# JacCoder
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
AI coding agent for the Jaseci stack, built entirely in [Jac](https://github.com/jaseci-labs/jaseci) using Object-Spatial Programming. Features an orchestrator-worker architecture with compiler-level Jac Intelligence, self-correcting code writes, and in-process SubAgent delegation.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Orchestrator-Worker Architecture** — MainAgent (27 tools) handles tasks directly or delegates to focused WorkerAgent / ExplorerAgent walkers
|
|
14
|
+
- **Jac Intelligence** — compiler-level AST analysis via jaclang (`analyze_project`, `find_symbol`)
|
|
15
|
+
- **Skills System** — Claude-Code-compatible `SKILL.md` directories, lazy-loaded so the LLM gets authoritative Jac syntax instead of stale training data
|
|
16
|
+
- **Self-Correcting Writes** — automatic JS-to-Jac sanitization, in-process syntax check, and `.jac-server.log` error monitoring on every write
|
|
17
|
+
- **Browser Validation Loop** — `browser_validate` shells out to agent-browser with cross-turn FAIL escalation (1st: hint → 2nd: warning → 3rd+: mandatory bisect)
|
|
18
|
+
- **In-Process SubAgents** — walkers, not subprocesses; share event stream + cost tracker + graph context
|
|
19
|
+
- **MCP Integration** — built-in `jac-mcp` plus user-added stdio/http/sse servers, configs persisted in the graph
|
|
20
|
+
- **Multi-Provider LLM** — 100+ models via [byllm](https://github.com/jaseci-labs/byllm); per-session model + API-key override is thread-local (no `os.environ` mutation)
|
|
21
|
+
- **Public API** — clean interface for CLI, VS Code extension, JacBuilder, library mode
|
|
22
|
+
- **Context Management** — smart tiered compaction with LLM-summary fallback; mode-aware dynamic prompt assembly
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- Python 3.12+
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install jac-coder
|
|
30
|
+
# or for development:
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Dependencies (auto-installed): `jaclang`, `byllm`, `mcp>=1.0.0`, `jac-mcp`, `python-dotenv`.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Set API key
|
|
40
|
+
export OPENAI_API_KEY="sk-..."
|
|
41
|
+
|
|
42
|
+
# Interactive REPL
|
|
43
|
+
jac cli.jac
|
|
44
|
+
|
|
45
|
+
# Single prompt (non-interactive)
|
|
46
|
+
jac cli.jac run "build a hello world jac app at /tmp/myapp"
|
|
47
|
+
|
|
48
|
+
# Resume a session
|
|
49
|
+
jac cli.jac session <id-prefix>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Root → Session → MainAgent
|
|
56
|
+
├── handles simple tasks directly (read, search, edit, git, browser)
|
|
57
|
+
└── spawn_agent() → WorkerAgent (write+run) or ExplorerAgent (read-only)
|
|
58
|
+
└── walker runs in-process, returns result to MainAgent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **MainAgent (node)** — orchestrator with 27 tools. Handles simple tasks directly, delegates complex work via `spawn_agent`. `max_react_iterations=80`.
|
|
62
|
+
- **WorkerAgent (walker)** — in-process SubAgent with 14 tools (can write/edit/run). For known changes.
|
|
63
|
+
- **ExplorerAgent (walker)** — in-process SubAgent with 10 tools (read-only + web search). For root-cause investigation.
|
|
64
|
+
- **Session (node)** — persistent chat state, history, active files, errors, mode hint.
|
|
65
|
+
- **ProjectMemory (node)** — AST-derived codebase knowledge (nodes, walkers, edges, imports). Backed by `.jaccoder/progress.md` as primary source of truth.
|
|
66
|
+
- **McpRegistry (node)** — persisted MCP server configs.
|
|
67
|
+
|
|
68
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full picture.
|
|
69
|
+
|
|
70
|
+
## Tools
|
|
71
|
+
|
|
72
|
+
MainAgent has 27 tools, grouped by domain:
|
|
73
|
+
|
|
74
|
+
| Group | Tool | Description |
|
|
75
|
+
|-------|------|-------------|
|
|
76
|
+
| Reason | `think` | Explicit reasoning step (loop-guarded) |
|
|
77
|
+
| Read | `read_file` | Read files with line numbers and pagination |
|
|
78
|
+
| Read | `list_files` | List directory contents |
|
|
79
|
+
| Read | `grep_search` | Regex search across files |
|
|
80
|
+
| Read | `find_files` | Find files by glob pattern |
|
|
81
|
+
| Read | `analyze_project` | Full AST analysis (nodes, walkers, edges, imports) |
|
|
82
|
+
| Read | `find_symbol` | Find symbol definition, fields, usages, exact import |
|
|
83
|
+
| Read | `load_jac_skill` | Load full body of a Jac skill by name |
|
|
84
|
+
| Write | `write_code` | Write file — anti-pattern blocking, auto sanitize, syntax check |
|
|
85
|
+
| Write | `edit_code` | Find-and-replace with the same self-correcting pipeline |
|
|
86
|
+
| Run | `run_command` | Execute shell commands (permission-guarded, auto-detects servers → background) |
|
|
87
|
+
| Run | `jac_run` | Run `.jac` files |
|
|
88
|
+
| Git | `git_status`, `git_diff`, `git_log`, `git_commit` | First-class git ops (`git_commit` is the only sanctioned way to commit) |
|
|
89
|
+
| Web | `web_fetch`, `web_search` | HTTP fetch + DuckDuckGo search |
|
|
90
|
+
| Browser | `browser_open`, `browser_do`, `browser_state`, `browser_validate`, `browser_close` | Visual QA via agent-browser; `browser_validate` is the primary pass/fail check |
|
|
91
|
+
| Delegate | `spawn_agent` | In-process WorkerAgent / ExplorerAgent walker |
|
|
92
|
+
| Interact | `ask_question`, `update_todos` | User prompt + multi-step task tracking |
|
|
93
|
+
| MCP | `mcp_call` | Call any tool from a connected MCP server |
|
|
94
|
+
|
|
95
|
+
## Public API
|
|
96
|
+
|
|
97
|
+
External apps import only from `jac_coder.api`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from jac_coder.api import initialize, create_session, chat, close_session
|
|
101
|
+
|
|
102
|
+
initialize("web")
|
|
103
|
+
session = create_session("/path/to/project", title="My App")
|
|
104
|
+
result = chat(session["session_id"], "build a calculator")
|
|
105
|
+
print(result["response"])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
Config sources (highest priority first):
|
|
111
|
+
|
|
112
|
+
1. Environment: `MODEL`, `TEMPERATURE`, `MAX_TOKENS`, `MAX_REACT_ITERATIONS`
|
|
113
|
+
2. Project: `./jaccoder.json`
|
|
114
|
+
3. Global: `~/.jaccoder/config.json`
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python -m pytest tests/ -v # 10 unit suites
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Project Structure
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
jac-code/
|
|
126
|
+
├── cli.jac # CLI entry point (REPL + subcommands)
|
|
127
|
+
├── jac_coder/ # Core package
|
|
128
|
+
│ ├── api.jac # Public API — the only module external apps import
|
|
129
|
+
│ ├── server.jac # JSON-RPC stdio server for the VS Code extension
|
|
130
|
+
│ ├── core/
|
|
131
|
+
│ │ ├── nodes.jac # MainAgent, WorkerAgent, ExplorerAgent, Session, ProjectMemory
|
|
132
|
+
│ │ └── walkers.jac # interact, new_session, list_sessions, etc.
|
|
133
|
+
│ ├── lib/
|
|
134
|
+
│ │ └── coder.jac # JacCoder library-mode class (stateless + service modes)
|
|
135
|
+
│ ├── infra/
|
|
136
|
+
│ │ ├── config.jac # Multi-source config + SessionAwareModel (thread-local LLM override)
|
|
137
|
+
│ │ └── mcp_manager.jac # MCP server registry (graph-persisted)
|
|
138
|
+
│ ├── runtime/
|
|
139
|
+
│ │ ├── context.jac # Tiered compaction with LLM-summary fallback
|
|
140
|
+
│ │ ├── events.jac # Event bus, doom-loop detection, abort signal
|
|
141
|
+
│ │ ├── memory.jac # AST-first ProjectMemory (LLM fallback)
|
|
142
|
+
│ │ ├── permission.jac # Permission rule engine (allow / ask / deny)
|
|
143
|
+
│ │ ├── prompt.jac # Mode-aware dynamic prompt assembly
|
|
144
|
+
│ │ ├── skills.jac # SKILL.md registry + listing injection
|
|
145
|
+
│ │ └── cost_tracker.jac # Opt-in token + USD cost tracking
|
|
146
|
+
│ ├── tool/ # 28 tools organized by domain
|
|
147
|
+
│ │ ├── meta/ # think, spawn_agent, update_todos, ask_question, validate
|
|
148
|
+
│ │ ├── read/ # filesystem, search, jac_analyzer, load_jac_skill
|
|
149
|
+
│ │ ├── write/ # checked (write_code/edit_code), scaffold
|
|
150
|
+
│ │ ├── run/ # shell, guarded, jac_tools
|
|
151
|
+
│ │ ├── net/ # web, preview (browser_*)
|
|
152
|
+
│ │ ├── git.jac # git_status, git_diff, git_log, git_commit
|
|
153
|
+
│ │ └── mcp.jac # mcp_call
|
|
154
|
+
│ └── skills/ # Bundled SKILL.md directories (Claude-Code-compatible)
|
|
155
|
+
├── tests/ # 10 unit suites (pytest wrappers + check_*.jac)
|
|
156
|
+
├── vscode-jac-coder/ # VS Code extension (TypeScript)
|
|
157
|
+
└── docs/ # ARCHITECTURE, ROADMAP, PROGRESS, LIBRARY_MODE
|
|
158
|
+
```
|
|
@@ -107,9 +107,6 @@ impl create_session(directory: str, title: str = "", agent: str = "main") -> dic
|
|
|
107
107
|
return {"error": "Failed to create session"};
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
# Store in registry so background threads can find it
|
|
111
|
-
_session_registry[session_id] = session_obj;
|
|
112
|
-
|
|
113
110
|
return {"session_id": session_id, "title": session_title, "status": "created"};
|
|
114
111
|
}
|
|
115
112
|
|
|
@@ -166,7 +163,6 @@ impl close_session(session_id: str) -> dict {
|
|
|
166
163
|
}
|
|
167
164
|
matches[0].status = "closed";
|
|
168
165
|
matches[0].updated_at = datetime.now().isoformat();
|
|
169
|
-
_session_registry.pop(session_id, None);
|
|
170
166
|
return {"status": "closed", "id": matches[0].id};
|
|
171
167
|
}
|
|
172
168
|
|
|
@@ -185,15 +181,9 @@ impl chat(
|
|
|
185
181
|
edit_mode: str = "auto",
|
|
186
182
|
env_overrides: dict = {}
|
|
187
183
|
) -> dict {
|
|
188
|
-
# Find session —
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
matches = [root()-->][?:Session][?id==session_id];
|
|
192
|
-
if matches {
|
|
193
|
-
session = matches[0];
|
|
194
|
-
_session_registry[session_id] = session;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
184
|
+
# Find session — query the graph directly. The graph is shared across pods
|
|
185
|
+
# via MongoDB persistence, so this works in single-pod and multi-pod alike.
|
|
186
|
+
session = _find_session(session_id);
|
|
197
187
|
if not session {
|
|
198
188
|
return {"error": "Session not found", "agent": "error"};
|
|
199
189
|
}
|
|
@@ -203,7 +193,6 @@ impl chat(
|
|
|
203
193
|
try {
|
|
204
194
|
_ = session.id;
|
|
205
195
|
} except Exception {
|
|
206
|
-
_session_registry.pop(session_id, None);
|
|
207
196
|
return {"error": "Session is stale after a server update. Please close this session and open a new one.", "agent": "error"};
|
|
208
197
|
}
|
|
209
198
|
|
|
@@ -220,8 +209,12 @@ impl chat(
|
|
|
220
209
|
if env_overrides {
|
|
221
210
|
import from jac_coder.infra.config { set_session_llm }
|
|
222
211
|
import logging as _chatlog;
|
|
223
|
-
_api_key = str(env_overrides.get("OPENAI_API_KEY", env_overrides.get("ANTHROPIC_API_KEY", "")));
|
|
224
212
|
_model_name = str(env_overrides.get("MODEL", ""));
|
|
213
|
+
if _model_name.startswith("claude-") {
|
|
214
|
+
_api_key = str(env_overrides.get("ANTHROPIC_API_KEY", ""));
|
|
215
|
+
} else {
|
|
216
|
+
_api_key = str(env_overrides.get("OPENAI_API_KEY", env_overrides.get("ANTHROPIC_API_KEY", "")));
|
|
217
|
+
}
|
|
225
218
|
_chatlog.getLogger("jac_coder.api").info(f"env_overrides received: model={_model_name} has_key={bool(_api_key)}");
|
|
226
219
|
if _api_key or _model_name {
|
|
227
220
|
import from jac_coder.infra.config { llm as _current_llm }
|
|
@@ -292,7 +285,7 @@ impl chat(
|
|
|
292
285
|
ctx_history.insert(0, {"role": "system", "content": agent_context});
|
|
293
286
|
}
|
|
294
287
|
|
|
295
|
-
# Inject available MCP tools into context (
|
|
288
|
+
# Inject available MCP tools into context (rebuilt every turn)
|
|
296
289
|
mcp_msg = _get_mcp_context_msg();
|
|
297
290
|
if mcp_msg {
|
|
298
291
|
ctx_history.insert(0, mcp_msg);
|
|
@@ -557,7 +550,7 @@ impl api_mcp_list() -> list {
|
|
|
557
550
|
|
|
558
551
|
|
|
559
552
|
impl abort_session(session_id: str) -> None {
|
|
560
|
-
session =
|
|
553
|
+
session = _find_session(session_id);
|
|
561
554
|
if session {
|
|
562
555
|
session.chat_history.append({
|
|
563
556
|
"role": "assistant",
|
|
@@ -567,9 +560,14 @@ impl abort_session(session_id: str) -> None {
|
|
|
567
560
|
}
|
|
568
561
|
|
|
569
562
|
|
|
570
|
-
|
|
563
|
+
"""Build MCP tools context message for the LLM system prompt.
|
|
571
564
|
|
|
572
|
-
|
|
565
|
+
Rebuilt every turn — no module-level cache. The previous in-process cache
|
|
566
|
+
(`_mcp_ctx_cache`) was keyed on `frozenset(tool_names)`, so descriptions or
|
|
567
|
+
inputSchemas could change on another pod without invalidating the cache,
|
|
568
|
+
causing stale system prompts. Rebuild cost is ~50us of string formatting,
|
|
569
|
+
dominated by the LLM call that follows.
|
|
570
|
+
"""
|
|
573
571
|
def _get_mcp_context_msg() -> dict | None {
|
|
574
572
|
try {
|
|
575
573
|
mcp_tools = mcp_get_tools();
|
|
@@ -579,12 +577,6 @@ def _get_mcp_context_msg() -> dict | None {
|
|
|
579
577
|
if not mcp_tools {
|
|
580
578
|
return None;
|
|
581
579
|
}
|
|
582
|
-
# Check if tools changed since last call
|
|
583
|
-
current_names = frozenset(f"{t['server']}::{t['name']}" for t in mcp_tools);
|
|
584
|
-
if _mcp_ctx_cache.get("tool_names") == current_names {
|
|
585
|
-
return _mcp_ctx_cache.get("msg");
|
|
586
|
-
}
|
|
587
|
-
# Rebuild
|
|
588
580
|
lines: list = [
|
|
589
581
|
"Available MCP tools — call via mcp_call(server_name, tool_name, arguments_json):",
|
|
590
582
|
"Check each tool's inputSchema for required arguments before calling."
|
|
@@ -600,10 +592,7 @@ def _get_mcp_context_msg() -> dict | None {
|
|
|
600
592
|
}
|
|
601
593
|
lines.append(f" - server={t['server']} tool={t['name']} {t['description']}{schema_hint}");
|
|
602
594
|
}
|
|
603
|
-
|
|
604
|
-
_mcp_ctx_cache["msg"] = msg;
|
|
605
|
-
_mcp_ctx_cache["tool_names"] = current_names;
|
|
606
|
-
return msg;
|
|
595
|
+
return {"role": "system", "content": "\n".join(lines)};
|
|
607
596
|
}
|
|
608
597
|
|
|
609
598
|
|
|
@@ -13,7 +13,7 @@ import from jac_coder.util.tool_output { tool_end }
|
|
|
13
13
|
import from jac_coder.runtime.permission { permission_engine }
|
|
14
14
|
import from jac_coder.util.sandbox { set_sandbox_root, set_browser_exec }
|
|
15
15
|
import from jac_coder.runtime.context { build_context, ContextConfig }
|
|
16
|
-
import from jac_coder.core.walkers { new_session, ensure_main_agent, _consume_llm_stream }
|
|
16
|
+
import from jac_coder.core.walkers { new_session, ensure_main_agent, _consume_llm_stream, _find_session }
|
|
17
17
|
import from jac_coder.infra.mcp_manager {
|
|
18
18
|
mcp_add_server,
|
|
19
19
|
mcp_disconnect_server,
|
|
@@ -43,11 +43,6 @@ import from jac_coder.runtime.events {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
# Thread-safe session registry — graph root() is per-request in jac-cloud,
|
|
47
|
-
# so background threads can't find sessions via [root()-->]. This dict
|
|
48
|
-
# is module-level and accessible from any thread.
|
|
49
|
-
glob _session_registry: dict = {};
|
|
50
|
-
|
|
51
46
|
"""Initialize jac-coder."""
|
|
52
47
|
def initialize(mode: str = "web") -> None;
|
|
53
48
|
|
|
@@ -106,7 +106,17 @@ Respond to what the user is asking RIGHT NOW. Don't recap. Concise by default, d
|
|
|
106
106
|
|
|
107
107
|
## Delegation
|
|
108
108
|
- Simple (1-2 files, questions, git): handle directly.
|
|
109
|
-
- Multi-file or
|
|
109
|
+
- Multi-file, investigation, or complex: spawn_agent().
|
|
110
|
+
|
|
111
|
+
SubAgent cannot see this conversation. Every task string must be fully self-contained.
|
|
112
|
+
|
|
113
|
+
Before spawning worker: use think + read_file + grep_search until you know the exact change needed. Task string must include: file path:line, function/node name, exact problem, what NOT to touch.
|
|
114
|
+
Good: `Fix delegation.impl.jac:112 — has_errs hardcoded False, extract from result.errors. Don't touch above line 108.`
|
|
115
|
+
Bad: `Fix the error tracking bug` / `Based on what you found, fix it`
|
|
116
|
+
|
|
117
|
+
Mode: `explorer` = root cause unknown → investigate first, then YOU synthesize findings and spawn `worker`. `worker` = exact file and line known → implement directly.
|
|
118
|
+
|
|
119
|
+
Never delegate understanding. Task strings must prove you already know what to change.
|
|
110
120
|
""";
|
|
111
121
|
|
|
112
122
|
|
|
@@ -118,6 +128,7 @@ sem WorkerAgent.do_work = """
|
|
|
118
128
|
|
|
119
129
|
Expert Jac coder executing a delegated task. Never edit `.jac/` (compiled output).
|
|
120
130
|
|
|
131
|
+
|
|
121
132
|
## Iteration Budget
|
|
122
133
|
Build breadth-first — get all files written and app running before polishing. If a component fails after 2 fix attempts, write a minimal working version and move on. Never spend 10+ iterations on one file.
|
|
123
134
|
|
|
@@ -9,11 +9,10 @@ import from jac_coder.runtime.events { is_abort_requested }
|
|
|
9
9
|
|
|
10
10
|
# MainAgent tools — full orchestrator set
|
|
11
11
|
import from jac_coder.tool.meta.think { think }
|
|
12
|
-
import from jac_coder.tool.meta.delegation { spawn_agent }
|
|
12
|
+
import from jac_coder.tool.meta.delegation { spawn_agent, set_iter_count }
|
|
13
13
|
import from jac_coder.tool.meta.todo { update_todos }
|
|
14
14
|
import from jac_coder.tool.read.load_jac_skill { load_jac_skill }
|
|
15
15
|
import from jac_coder.tool.meta.question { ask_question }
|
|
16
|
-
import from jac_coder.tool.write.scaffold { scaffold_project }
|
|
17
16
|
import from jac_coder.tool.net.web { web_fetch, web_search }
|
|
18
17
|
import from jac_coder.tool.run.guarded { run_command }
|
|
19
18
|
import from jac_coder.tool.run.jac_tools { jac_check, jac_run }
|
|
@@ -163,7 +162,6 @@ node MainAgent {
|
|
|
163
162
|
write_code,
|
|
164
163
|
run_command,
|
|
165
164
|
jac_run,
|
|
166
|
-
scaffold_project,
|
|
167
165
|
# Git — first-class version control
|
|
168
166
|
git_status,
|
|
169
167
|
git_diff,
|
|
@@ -200,8 +198,9 @@ node MainAgent {
|
|
|
200
198
|
# SubAgent walkers — spawned on MainAgent node for task delegation
|
|
201
199
|
# ---------------------------------------------------------------------------
|
|
202
200
|
|
|
203
|
-
"""on_iteration callback — checks abort flag
|
|
201
|
+
"""on_iteration callback — checks abort flag and tracks real iteration count."""
|
|
204
202
|
def _iteration_hook(ctx: IterationContext) -> IterationAction {
|
|
203
|
+
set_iter_count(ctx.iteration);
|
|
205
204
|
if is_abort_requested() {
|
|
206
205
|
return IterationAction.ABORT;
|
|
207
206
|
}
|
|
@@ -217,6 +216,9 @@ walker WorkerAgent {
|
|
|
217
216
|
|
|
218
217
|
def do_work(task_str: str) -> str by llm(
|
|
219
218
|
tools=[
|
|
219
|
+
# Reason
|
|
220
|
+
think,
|
|
221
|
+
# Understand
|
|
220
222
|
load_jac_skill,
|
|
221
223
|
analyze_project,
|
|
222
224
|
find_symbol,
|
|
@@ -224,26 +226,18 @@ walker WorkerAgent {
|
|
|
224
226
|
list_files,
|
|
225
227
|
grep_search,
|
|
226
228
|
find_files,
|
|
229
|
+
# Act
|
|
227
230
|
write_code,
|
|
228
231
|
edit_code,
|
|
229
232
|
run_command,
|
|
230
233
|
jac_run,
|
|
231
|
-
|
|
234
|
+
# Git — status only, workers don't commit
|
|
232
235
|
git_status,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
git_log,
|
|
236
|
-
web_fetch,
|
|
237
|
-
web_search,
|
|
238
|
-
browser_open,
|
|
239
|
-
browser_do,
|
|
240
|
-
browser_state,
|
|
241
|
-
browser_close,
|
|
242
|
-
browser_validate,
|
|
243
|
-
mcp_call
|
|
236
|
+
# Validate
|
|
237
|
+
browser_validate
|
|
244
238
|
],
|
|
245
239
|
on_iteration=_iteration_hook,
|
|
246
|
-
max_react_iterations=
|
|
240
|
+
max_react_iterations=30,
|
|
247
241
|
temperature=0.2,
|
|
248
242
|
max_tokens=8192,
|
|
249
243
|
stream=True,
|
|
@@ -46,10 +46,6 @@ impl new_session.create with Root entry {
|
|
|
46
46
|
here ++> session;
|
|
47
47
|
agent = ensure_main_agent();
|
|
48
48
|
session ++> agent;
|
|
49
|
-
# Persist the new session and the root→session edge so it survives server restarts
|
|
50
|
-
save(here);
|
|
51
|
-
save(session);
|
|
52
|
-
commit();
|
|
53
49
|
report {"session_id": session.id, "title": session.title, "status": "created"};
|
|
54
50
|
}
|
|
55
51
|
|
|
@@ -103,8 +99,6 @@ impl close_session.close with Root entry {
|
|
|
103
99
|
if matches {
|
|
104
100
|
matches[0].status = "closed";
|
|
105
101
|
matches[0].updated_at = datetime.now().isoformat();
|
|
106
|
-
save(matches[0]);
|
|
107
|
-
commit();
|
|
108
102
|
report {"status": "closed", "id": matches[0].id};
|
|
109
103
|
} else {
|
|
110
104
|
report {"error": "Session not found"};
|
|
@@ -183,8 +177,6 @@ impl interact.enter_session with Session entry {
|
|
|
183
177
|
here.chat_history.append({"role": "user", "content": self.message});
|
|
184
178
|
here.updated_at = datetime.now().isoformat();
|
|
185
179
|
self.chat_history = here.chat_history;
|
|
186
|
-
save(here);
|
|
187
|
-
commit();
|
|
188
180
|
|
|
189
181
|
# Visit MainAgent
|
|
190
182
|
visit [-->][?:MainAgent] else {
|
|
@@ -244,8 +236,6 @@ impl _persist_response(
|
|
|
244
236
|
session.chat_history.append(record);
|
|
245
237
|
session.last_agent = agent_mode;
|
|
246
238
|
session.updated_at = datetime.now().isoformat();
|
|
247
|
-
save(session);
|
|
248
|
-
commit();
|
|
249
239
|
}
|
|
250
240
|
|
|
251
241
|
|
|
@@ -415,8 +405,6 @@ impl _respond_and_persist(agent: MainAgent, ctx: interact) -> None {
|
|
|
415
405
|
# Save mode hint for next turn's dynamic prompt assembly
|
|
416
406
|
import from jac_coder.runtime.prompt { infer_mode_hint }
|
|
417
407
|
session.last_mode_hint = infer_mode_hint(tools_used, files_modified);
|
|
418
|
-
save(session);
|
|
419
|
-
commit();
|
|
420
408
|
}
|
|
421
409
|
|
|
422
410
|
# If aborted, skip persistence — abort note already persisted by stop_jaccoder
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Cross-pod key-value cache backed by jac-scale's managed Redis.
|
|
2
|
+
|
|
3
|
+
Used for ephemeral state that must be consistent across horizontally-scaled
|
|
4
|
+
jac-coder pods (e.g. the MCP tool list cache, cross-pod abort signals). Falls
|
|
5
|
+
back to a no-op when Redis is not configured — that case applies to local CLI /
|
|
6
|
+
stdio mode and to dev environments where the kvstore URL isn't set. A no-op
|
|
7
|
+
fallback is correct (just slower, since each pod recomputes); a None return from
|
|
8
|
+
`kv_get` is treated by callers as a cache miss, and `get_kv()` returns a no-op
|
|
9
|
+
stub so B2 abort callers can invoke it unconditionally without None checks.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys;
|
|
14
|
+
import threading;
|
|
15
|
+
|
|
16
|
+
# Lazy singleton — created on first access. None means "Redis unavailable,
|
|
17
|
+
# operate in no-cache mode".
|
|
18
|
+
glob _kv: Any = None;
|
|
19
|
+
glob _kv_init_done: bool = False;
|
|
20
|
+
glob _kv_init_lock: Any = threading.Lock();
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
"""Silent no-op fallback returned by get_kv() when Redis is unavailable.
|
|
24
|
+
Satisfies the .get() / .set_with_ttl() / .delete() interface so B2 abort
|
|
25
|
+
callers never need to check for None."""
|
|
26
|
+
obj _NoOpKv {
|
|
27
|
+
def set_with_ttl(key: str, value: dict, ttl: int) -> bool { return False; }
|
|
28
|
+
def get(key: str) -> (dict | None) { return None; }
|
|
29
|
+
def delete(key: str) -> int { return 0; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
"""Initialize the kvstore handle once. Safe to call repeatedly."""
|
|
34
|
+
def _try_init() -> None {
|
|
35
|
+
global _kv, _kv_init_done;
|
|
36
|
+
if _kv_init_done {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
with _kv_init_lock {
|
|
40
|
+
if _kv_init_done {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
_kv_init_done = True;
|
|
44
|
+
try {
|
|
45
|
+
import from jac_scale.lib { kvstore }
|
|
46
|
+
_kv = kvstore(db_name="jac_coder", db_type="redis");
|
|
47
|
+
} except Exception as e {
|
|
48
|
+
# Redis not configured — common in CLI/stdio mode and local dev.
|
|
49
|
+
# Cross-pod cache is disabled; callers fall through to recompute.
|
|
50
|
+
sys.stderr.write(
|
|
51
|
+
f"[kv] Redis unavailable, cross-pod cache disabled: {e}\n"
|
|
52
|
+
);
|
|
53
|
+
_kv = None;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
"""Get a cached dict by key, or None if missing/unavailable."""
|
|
60
|
+
def kv_get(key: str) -> dict | None {
|
|
61
|
+
_try_init();
|
|
62
|
+
if _kv is None {
|
|
63
|
+
return None;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return _kv.get(key);
|
|
67
|
+
} except Exception as e {
|
|
68
|
+
sys.stderr.write(f"[kv] get({key}) failed: {e}\n");
|
|
69
|
+
return None;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
"""Store a dict under key with TTL in seconds. No-op if Redis unavailable."""
|
|
75
|
+
def kv_set_ttl(key: str, value: dict, ttl: int) -> None {
|
|
76
|
+
_try_init();
|
|
77
|
+
if _kv is None {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
_kv.set_with_ttl(key, value, ttl=ttl);
|
|
82
|
+
} except Exception as e {
|
|
83
|
+
sys.stderr.write(f"[kv] set_with_ttl({key}) failed: {e}\n");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
"""Delete a key. No-op if Redis unavailable or key missing."""
|
|
89
|
+
def kv_delete(key: str) -> None {
|
|
90
|
+
_try_init();
|
|
91
|
+
if _kv is None {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
_kv.delete(key);
|
|
96
|
+
} except Exception as e {
|
|
97
|
+
sys.stderr.write(f"[kv] delete({key}) failed: {e}\n");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
"""Return the kvstore instance or a no-op stub — never returns None.
|
|
103
|
+
Used by B2 (cross-pod abort) in events.jac where callers invoke
|
|
104
|
+
.get() / .set_with_ttl() / .delete() directly without None-checking."""
|
|
105
|
+
def get_kv() -> Any {
|
|
106
|
+
_try_init();
|
|
107
|
+
if _kv is None {
|
|
108
|
+
return _NoOpKv();
|
|
109
|
+
}
|
|
110
|
+
return _kv;
|
|
111
|
+
}
|
|
@@ -17,6 +17,7 @@ import from mcp { ClientSession }
|
|
|
17
17
|
import from mcp.client.stdio { stdio_client, StdioServerParameters }
|
|
18
18
|
import from mcp.client.streamable_http { streamablehttp_client }
|
|
19
19
|
import from mcp.client.sse { sse_client }
|
|
20
|
+
import from jac_coder.infra.kv { kv_get, kv_set_ttl, kv_delete }
|
|
20
21
|
|
|
21
22
|
# Cached update info for jac-mcp (populated by background thread on startup).
|
|
22
23
|
glob _jac_mcp_update_info: dict = {};
|
|
@@ -27,7 +28,7 @@ glob _mgr_thread: Any = None;
|
|
|
27
28
|
glob _mgr_lock: Any = threading.Lock();
|
|
28
29
|
glob _sessions: dict = {}; # name -> ClientSession (open inside _mgr_loop)
|
|
29
30
|
glob _exit_stacks: dict = {}; # name -> AsyncExitStack (keeps transports alive)
|
|
30
|
-
glob
|
|
31
|
+
glob _TOOLS_CACHE_KEY: str = "jc:mcp:tools";
|
|
31
32
|
glob _TOOLS_CACHE_TTL: int = 30; # seconds before tool list is re-fetched
|
|
32
33
|
glob _builtin_names: set = set(); # names registered via mcp_register_builtin
|
|
33
34
|
glob _disconnected_names: set = set(); # names intentionally disconnected by user
|
|
@@ -84,8 +85,6 @@ def _load_configs() -> dict {
|
|
|
84
85
|
def _save_configs(configs: dict) -> None {
|
|
85
86
|
reg = _get_registry();
|
|
86
87
|
reg.servers = configs;
|
|
87
|
-
save(reg);
|
|
88
|
-
commit();
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
|
|
@@ -277,7 +276,7 @@ impl mcp_add_server(name: str, config: dict) -> dict {
|
|
|
277
276
|
configs = _load_configs();
|
|
278
277
|
configs[name] = config;
|
|
279
278
|
_save_configs(configs);
|
|
280
|
-
|
|
279
|
+
kv_delete(_TOOLS_CACHE_KEY); # Invalidate cross-pod cache — new server added
|
|
281
280
|
return {
|
|
282
281
|
"status": "connected",
|
|
283
282
|
"name": name,
|
|
@@ -299,7 +298,7 @@ impl mcp_disconnect_server(name: str) -> dict {
|
|
|
299
298
|
}
|
|
300
299
|
_disconnected_names.add(name);
|
|
301
300
|
_submit(_close_connection(name));
|
|
302
|
-
|
|
301
|
+
kv_delete(_TOOLS_CACHE_KEY);
|
|
303
302
|
return {"status": "disconnected", "name": name};
|
|
304
303
|
}
|
|
305
304
|
|
|
@@ -317,7 +316,7 @@ impl mcp_reconnect_server(name: str) -> dict {
|
|
|
317
316
|
# produces Unknown in the Jac type checker — the for pattern is used
|
|
318
317
|
# consistently throughout this file).
|
|
319
318
|
_submit(_close_connection(name));
|
|
320
|
-
|
|
319
|
+
kv_delete(_TOOLS_CACHE_KEY);
|
|
321
320
|
for (n, config) in configs.items() {
|
|
322
321
|
if n == name {
|
|
323
322
|
try {
|
|
@@ -346,7 +345,7 @@ impl mcp_delete_server(name: str) -> dict {
|
|
|
346
345
|
_submit(_close_connection(name));
|
|
347
346
|
configs.pop(name);
|
|
348
347
|
_save_configs(configs);
|
|
349
|
-
|
|
348
|
+
kv_delete(_TOOLS_CACHE_KEY);
|
|
350
349
|
return {"status": "deleted", "name": name};
|
|
351
350
|
}
|
|
352
351
|
|
|
@@ -397,6 +396,7 @@ impl mcp_register_builtin(name: str, config: dict) -> None {
|
|
|
397
396
|
config["builtin"] = True;
|
|
398
397
|
configs[name] = config;
|
|
399
398
|
_save_configs(configs);
|
|
399
|
+
kv_delete(_TOOLS_CACHE_KEY); # Invalidate cross-pod cache — server set changed
|
|
400
400
|
if name == "jac-mcp" {
|
|
401
401
|
threading.Thread(target=_check_jac_mcp_version, daemon=True).start();
|
|
402
402
|
}
|
|
@@ -405,9 +405,9 @@ impl mcp_register_builtin(name: str, config: dict) -> None {
|
|
|
405
405
|
|
|
406
406
|
"""Return a flat list of all tools from all registered servers (cached for 30s)."""
|
|
407
407
|
impl mcp_get_tools() -> list {
|
|
408
|
-
|
|
409
|
-
if
|
|
410
|
-
return
|
|
408
|
+
cached = kv_get(_TOOLS_CACHE_KEY);
|
|
409
|
+
if cached is not None {
|
|
410
|
+
return cached.get("tools", []);
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
configs = _load_configs();
|
|
@@ -420,8 +420,7 @@ impl mcp_get_tools() -> list {
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
|
|
424
|
-
_tools_cache["ts"] = now;
|
|
423
|
+
kv_set_ttl(_TOOLS_CACHE_KEY, {"tools": all_tools}, ttl=_TOOLS_CACHE_TTL);
|
|
425
424
|
return all_tools;
|
|
426
425
|
}
|
|
427
426
|
|