jac-coder 0.2.0__tar.gz → 0.2.2__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.
Files changed (132) hide show
  1. {jac_coder-0.2.0 → jac_coder-0.2.2}/PKG-INFO +1 -1
  2. jac_coder-0.2.2/README.md +160 -0
  3. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/api.impl.jac +76 -77
  4. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/api.jac +1 -6
  5. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/core/nodes.impl.jac +12 -1
  6. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/core/nodes.jac +26 -18
  7. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/core/walkers.impl.jac +51 -45
  8. jac_coder-0.2.2/jac_coder/infra/kv.jac +111 -0
  9. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/infra/mcp_manager.impl.jac +56 -31
  10. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/infra/mcp_manager.jac +7 -0
  11. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/lib/coder.impl.jac +91 -11
  12. jac_coder-0.2.2/jac_coder/runtime/cost_tracker.impl.jac +229 -0
  13. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/cost_tracker.jac +12 -12
  14. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/events.jac +69 -5
  15. jac_coder-0.2.2/jac_coder/runtime/permission.impl.jac +171 -0
  16. jac_coder-0.2.2/jac_coder/runtime/permission.jac +55 -0
  17. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/prompt.jac +35 -14
  18. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/skills.impl.jac +33 -9
  19. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/skills.jac +7 -2
  20. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/server.jac +44 -11
  21. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/ROADMAP.md +1 -1
  22. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-components/SKILL.md +4 -1
  23. jac_coder-0.2.2/jac_coder/skills/jac-cl-styling/SKILL.md +49 -0
  24. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-core-cheatsheet/SKILL.md +3 -1
  25. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-fullstack-patterns/SKILL.md +4 -2
  26. jac_coder-0.2.2/jac_coder/skills/jac-npm-packages/SKILL.md +94 -0
  27. jac_coder-0.2.2/jac_coder/skills/jac-scaffold/SKILL.md +70 -0
  28. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/__init__.jac +0 -1
  29. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/git.impl.jac +15 -14
  30. jac_coder-0.2.2/jac_coder/tool/git.jac +34 -0
  31. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/delegation.impl.jac +6 -1
  32. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/delegation.jac +1 -0
  33. jac_coder-0.2.2/jac_coder/tool/meta/think.impl.jac +19 -0
  34. jac_coder-0.2.2/jac_coder/tool/meta/think.jac +9 -0
  35. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/filesystem.impl.jac +15 -11
  36. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/run/guarded.impl.jac +32 -2
  37. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/run/guarded.jac +4 -1
  38. jac_coder-0.2.2/jac_coder/tool/run/shell.impl.jac +206 -0
  39. jac_coder-0.2.2/jac_coder/tool/run/shell.jac +23 -0
  40. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/write/checked.impl.jac +3 -5
  41. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder.egg-info/PKG-INFO +1 -1
  42. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder.egg-info/SOURCES.txt +4 -11
  43. {jac_coder-0.2.0 → jac_coder-0.2.2}/pyproject.toml +1 -1
  44. jac_coder-0.2.0/README.md +0 -141
  45. jac_coder-0.2.0/jac_coder/runtime/cost_tracker.impl.jac +0 -134
  46. jac_coder-0.2.0/jac_coder/runtime/permission.impl.jac +0 -62
  47. jac_coder-0.2.0/jac_coder/runtime/permission.jac +0 -19
  48. jac_coder-0.2.0/jac_coder/skills/jac-scaffold/SKILL.md +0 -50
  49. jac_coder-0.2.0/jac_coder/tool/git.jac +0 -18
  50. jac_coder-0.2.0/jac_coder/tool/meta/think.impl.jac +0 -4
  51. jac_coder-0.2.0/jac_coder/tool/meta/think.jac +0 -5
  52. jac_coder-0.2.0/jac_coder/tool/run/shell.impl.jac +0 -152
  53. jac_coder-0.2.0/jac_coder/tool/run/shell.jac +0 -13
  54. jac_coder-0.2.0/jac_coder/tool/write/scaffold.impl.jac +0 -236
  55. jac_coder-0.2.0/jac_coder/tool/write/scaffold.jac +0 -12
  56. jac_coder-0.2.0/tests/test_context.py +0 -53
  57. jac_coder-0.2.0/tests/test_events.py +0 -40
  58. jac_coder-0.2.0/tests/test_graph.py +0 -33
  59. jac_coder-0.2.0/tests/test_interact.py +0 -40
  60. jac_coder-0.2.0/tests/test_jaccoder.py +0 -72
  61. jac_coder-0.2.0/tests/test_memory.py +0 -53
  62. jac_coder-0.2.0/tests/test_selfcorrect.py +0 -72
  63. jac_coder-0.2.0/tests/test_tools.py +0 -45
  64. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/__init__.jac +0 -0
  65. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/__init__.py +0 -0
  66. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/cli_entry.py +0 -0
  67. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/core/__init__.jac +0 -0
  68. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/core/walkers.jac +0 -0
  69. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/infra/__init__.jac +0 -0
  70. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/infra/config.impl.jac +0 -0
  71. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/infra/config.jac +0 -0
  72. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/lib/__init__.jac +0 -0
  73. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/lib/coder.jac +0 -0
  74. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/__init__.jac +0 -0
  75. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/context.impl.jac +0 -0
  76. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/context.jac +0 -0
  77. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/memory.impl.jac +0 -0
  78. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/memory.jac +0 -0
  79. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/runtime/prompt.impl.jac +0 -0
  80. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/serve_entry.jac +0 -0
  81. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-by-llm/SKILL.md +0 -0
  82. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-auth/SKILL.md +0 -0
  83. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-organization/SKILL.md +0 -0
  84. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-routing/SKILL.md +0 -0
  85. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-has-fields/SKILL.md +0 -0
  86. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-impl-files/SKILL.md +0 -0
  87. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-node-edge-patterns/SKILL.md +0 -0
  88. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-sv-auth/SKILL.md +0 -0
  89. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-sv-endpoints/SKILL.md +0 -0
  90. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-sv-persistence/SKILL.md +0 -0
  91. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-types/SKILL.md +0 -0
  92. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/skills/jac-walker-patterns/SKILL.md +0 -0
  93. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/mcp.impl.jac +0 -0
  94. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/mcp.jac +0 -0
  95. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/__init__.jac +0 -0
  96. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/question.impl.jac +0 -0
  97. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/question.jac +0 -0
  98. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/task.impl.jac +0 -0
  99. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/task.jac +0 -0
  100. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/todo.impl.jac +0 -0
  101. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/todo.jac +0 -0
  102. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/validate.impl.jac +0 -0
  103. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/meta/validate.jac +0 -0
  104. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/net/__init__.jac +0 -0
  105. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/net/preview.impl.jac +0 -0
  106. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/net/preview.jac +0 -0
  107. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/net/web.impl.jac +0 -0
  108. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/net/web.jac +0 -0
  109. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/__init__.jac +0 -0
  110. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/filesystem.jac +0 -0
  111. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
  112. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/jac_analyzer.jac +0 -0
  113. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/load_jac_skill.impl.jac +0 -0
  114. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/load_jac_skill.jac +0 -0
  115. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/search.impl.jac +0 -0
  116. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/read/search.jac +0 -0
  117. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/run/__init__.jac +0 -0
  118. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
  119. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/run/jac_tools.jac +0 -0
  120. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/write/__init__.jac +0 -0
  121. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/tool/write/checked.jac +0 -0
  122. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/util/__init__.jac +0 -0
  123. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/util/colors.jac +0 -0
  124. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/util/sandbox.impl.jac +0 -0
  125. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/util/sandbox.jac +0 -0
  126. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/util/tool_output.impl.jac +0 -0
  127. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder/util/tool_output.jac +0 -0
  128. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder.egg-info/dependency_links.txt +0 -0
  129. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder.egg-info/entry_points.txt +0 -0
  130. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder.egg-info/requires.txt +0 -0
  131. {jac_coder-0.2.0 → jac_coder-0.2.2}/jac_coder.egg-info/top_level.txt +0 -0
  132. {jac_coder-0.2.0 → jac_coder-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-coder
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: AI coding agent backend for Jac, powered by jac-byllm
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,160 @@
1
+ # JacCoder
2
+
3
+ ![JacCoder CLI](assets/cli.png)
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
+ ![JacCoder Graph Architecture](assets/graph.png)
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
+ ```
159
+
160
+ <img width="2661" height="1091" alt="image" src="https://github.com/user-attachments/assets/a314087f-ae94-4c8b-8899-1e4ff3eb465c" />
@@ -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 — try registry first (works across threads), then graph
189
- session = _session_registry.get(session_id);
190
- if not session {
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 }
@@ -251,13 +244,6 @@ impl chat(
251
244
  # Memory initialization
252
245
  _ensure_memory(session, work_dir);
253
246
 
254
- # Add user message
255
- user_msg_record: dict = {"role": "user", "content": message};
256
- if images and len(images) > 0 {
257
- user_msg_record["has_images"] = True;
258
- user_msg_record["image_count"] = len(images);
259
- }
260
- session.chat_history.append(user_msg_record);
261
247
  session.updated_at = datetime.now().isoformat();
262
248
 
263
249
  # Get MainAgent — always resolve fresh (cached refs go stale in jac start)
@@ -267,15 +253,33 @@ impl chat(
267
253
  config = get_config();
268
254
  _init_spawn_budget(config.max_react_iterations * 3);
269
255
 
270
- # Build context
271
- ctx_history = build_context(
272
- session.chat_history,
273
- session.active_files,
274
- session.pending_errors,
275
- project_summary=session.project_summary or ""
276
- );
256
+ # Per-turn context gets folded into the user message; we can't add
257
+ # role=system dicts to session.chat_history without them persisting stale.
258
+ prefix_parts: list[str] = [];
259
+
260
+ if session.project_summary {
261
+ is_progress_file = session.project_summary.lstrip().startswith("#");
262
+ label = (
263
+ "Project progress (.jaccoder/progress.md)"
264
+ if is_progress_file
265
+ else "Project context"
266
+ );
267
+ prefix_parts.append(
268
+ f"[INTERNAL — do NOT present this to the user unless they ask about the project. This is your background knowledge for making informed decisions.]\n{label}:\n{session.project_summary}"
269
+ );
270
+ }
271
+ if session.active_files {
272
+ prefix_parts.append("Active files: " + ", ".join(session.active_files));
273
+ }
274
+ if session.pending_errors {
275
+ prefix_parts.append("Pending errors:\n" + "\n".join(session.pending_errors));
276
+ }
277
+
278
+ mcp_msg = _get_mcp_context_msg();
279
+ if mcp_msg {
280
+ prefix_parts.append(str(mcp_msg.get("content", "")));
281
+ }
277
282
 
278
- # Inject mode-aware workflow modules (dynamic prompt assembly)
279
283
  import from jac_coder.runtime.prompt { select_prompt_modules as _select_modules }
280
284
  workflow_modules = _select_modules(
281
285
  last_mode_hint=session.last_mode_hint,
@@ -284,20 +288,28 @@ impl chat(
284
288
  chat_history=session.chat_history
285
289
  );
286
290
  if workflow_modules {
287
- ctx_history.insert(0, {"role": "system", "content": workflow_modules});
291
+ prefix_parts.append(workflow_modules);
288
292
  }
289
293
 
290
- # Inject agent_context if provided (e.g. from JacBuilder)
291
294
  if agent_context {
292
- ctx_history.insert(0, {"role": "system", "content": agent_context});
295
+ prefix_parts.append(agent_context);
293
296
  }
294
297
 
295
- # Inject available MCP tools into context (cached to avoid rebuilding every turn)
296
- mcp_msg = _get_mcp_context_msg();
297
- if mcp_msg {
298
- ctx_history.insert(0, mcp_msg);
298
+ if edit_mode == "plan" {
299
+ prefix_parts.append(
300
+ "You are in PLAN MODE. Your task is to produce a detailed, step-by-step plan "
301
+ "for the user's request. Do NOT write, edit, or run any files. "
302
+ "Describe every file you would create or modify, what changes you would make, "
303
+ "and why. The user will review your plan and click 'Execute Plan' to apply it."
304
+ );
299
305
  }
300
306
 
307
+ composed_message = (
308
+ "\n\n---\n\n".join(prefix_parts) + "\n\n---\n\n" + message
309
+ if prefix_parts
310
+ else message
311
+ );
312
+
301
313
  # Early exit if already aborted before LLM starts
302
314
  if is_abort_requested() {
303
315
  tool_end();
@@ -313,24 +325,6 @@ impl chat(
313
325
  };
314
326
  }
315
327
 
316
- # Plan mode — inject instruction to write a plan only, no file writes.
317
- # The UI will show "Execute Plan" when done; execution reruns with edit_mode="auto".
318
- if edit_mode == "plan" {
319
- plan_msg: dict = {
320
- "role": "system",
321
- "content": (
322
- "You are in PLAN MODE. Your task is to produce a detailed, step-by-step plan "
323
- "for the user's request. Do NOT write, edit, or run any files. "
324
- "Describe every file you would create or modify, what changes you would make, "
325
- "and why. The user will review your plan and click 'Execute Plan' to apply it."
326
- )
327
- };
328
- ctx_with_plan: list[dict] = [plan_msg];
329
- ctx_with_plan.extend(ctx_history);
330
- ctx_history = ctx_with_plan;
331
- }
332
-
333
- # Call MainAgent — returns StreamEvent generator with logging enabled
334
328
  # If user attached images, convert to byllm Image object and pass as user_image
335
329
  user_image = None;
336
330
  if images and len(images) > 0 {
@@ -346,7 +340,10 @@ impl chat(
346
340
  sys.stderr.write(f"[api] Failed to create Image from user attachment: {img_err}\n");
347
341
  }
348
342
  }
349
- event_stream = main_agent.respond(message=message, chat_history=ctx_history, user_image=user_image);
343
+
344
+ main_agent._conv = session.chat_history;
345
+
346
+ event_stream = main_agent.respond(message=composed_message, user_image=user_image);
350
347
  stream_result = _consume_llm_stream(event_stream);
351
348
 
352
349
  response_text = stream_result["content"];
@@ -386,16 +383,22 @@ impl chat(
386
383
  }
387
384
  }
388
385
 
389
- # Persist response skip if aborted (abort note already in chat_history)
386
+ # byLLM already appended the assistant/tool turns; patch UI metadata
387
+ # onto the trailing assistant entry. Skip if aborted.
390
388
  if not is_abort_requested() {
391
- record: dict = {"role": "assistant", "content": response_text, "agent": "main"};
392
- if tools_used {
393
- record["tools_used"] = tools_used;
394
- }
395
- if files_modified {
396
- record["files_modified"] = files_modified;
389
+ for idx in range(len(session.chat_history) - 1, -1, -1) {
390
+ message_entry = session.chat_history[idx];
391
+ if message_entry.get("role") == "assistant" and "agent" not in message_entry {
392
+ message_entry["agent"] = "main";
393
+ if tools_used {
394
+ message_entry["tools_used"] = tools_used;
395
+ }
396
+ if files_modified {
397
+ message_entry["files_modified"] = files_modified;
398
+ }
399
+ break;
400
+ }
397
401
  }
398
- session.chat_history.append(record);
399
402
  }
400
403
  session.last_agent = "main";
401
404
  session.updated_at = datetime.now().isoformat();
@@ -557,7 +560,7 @@ impl api_mcp_list() -> list {
557
560
 
558
561
 
559
562
  impl abort_session(session_id: str) -> None {
560
- session = _session_registry.get(session_id);
563
+ session = _find_session(session_id);
561
564
  if session {
562
565
  session.chat_history.append({
563
566
  "role": "assistant",
@@ -567,9 +570,14 @@ impl abort_session(session_id: str) -> None {
567
570
  }
568
571
 
569
572
 
570
- glob _mcp_ctx_cache: dict = {}; # {"msg": dict, "tool_names": set}
573
+ """Build MCP tools context message for the LLM system prompt.
571
574
 
572
- """Build MCP tools context message, cached by tool names."""
575
+ Rebuilt every turn no module-level cache. The previous in-process cache
576
+ (`_mcp_ctx_cache`) was keyed on `frozenset(tool_names)`, so descriptions or
577
+ inputSchemas could change on another pod without invalidating the cache,
578
+ causing stale system prompts. Rebuild cost is ~50us of string formatting,
579
+ dominated by the LLM call that follows.
580
+ """
573
581
  def _get_mcp_context_msg() -> dict | None {
574
582
  try {
575
583
  mcp_tools = mcp_get_tools();
@@ -579,12 +587,6 @@ def _get_mcp_context_msg() -> dict | None {
579
587
  if not mcp_tools {
580
588
  return None;
581
589
  }
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
590
  lines: list = [
589
591
  "Available MCP tools — call via mcp_call(server_name, tool_name, arguments_json):",
590
592
  "Check each tool's inputSchema for required arguments before calling."
@@ -600,10 +602,7 @@ def _get_mcp_context_msg() -> dict | None {
600
602
  }
601
603
  lines.append(f" - server={t['server']} tool={t['name']} {t['description']}{schema_hint}");
602
604
  }
603
- msg: dict = {"role": "system", "content": "\n".join(lines)};
604
- _mcp_ctx_cache["msg"] = msg;
605
- _mcp_ctx_cache["tool_names"] = current_names;
606
- return msg;
605
+ return {"role": "system", "content": "\n".join(lines)};
607
606
  }
608
607
 
609
608
 
@@ -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 investigation: spawn_agent(task, mode="worker"|"explorer").
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 }
@@ -83,6 +82,16 @@ obj SessionLearnings {
83
82
  # ---------------------------------------------------------------------------
84
83
  node McpRegistry {
85
84
  has servers: dict[str, dict] = {};
85
+ has disconnected_servers: list[str] = [];
86
+ }
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # UserPermissionRules — persists per-user "always allow" decisions in the graph
91
+ # so they are visible across pods (B3 / horizontal scaling).
92
+ node UserPermissionRules {
93
+ has user_id: str = "",
94
+ always_allowed: dict[str, list[str]] = {};
86
95
  }
87
96
 
88
97
 
@@ -146,7 +155,10 @@ node Session {
146
155
  # MainAgent — the orchestrator node
147
156
  # ---------------------------------------------------------------------------
148
157
  node MainAgent {
149
- def respond(message: str, chat_history: list[dict], user_image: Image | None = None) -> str by llm(
158
+ # Bound to Session.chat_history by the walker before each call.
159
+ has _conv: list[dict] = [];
160
+
161
+ def respond(message: str, user_image: Image | None = None) -> str by llm(
150
162
  tools=[
151
163
  # Think — explicit reasoning before acting or delegating
152
164
  think,
@@ -163,7 +175,6 @@ node MainAgent {
163
175
  write_code,
164
176
  run_command,
165
177
  jac_run,
166
- scaffold_project,
167
178
  # Git — first-class version control
168
179
  git_status,
169
180
  git_diff,
@@ -186,6 +197,7 @@ node MainAgent {
186
197
  # MCP — call tools from connected MCP servers
187
198
  mcp_call
188
199
  ],
200
+ conversation=self._conv,
189
201
  on_iteration=_iteration_hook,
190
202
  max_react_iterations=80,
191
203
  temperature=0.2,
@@ -200,8 +212,9 @@ node MainAgent {
200
212
  # SubAgent walkers — spawned on MainAgent node for task delegation
201
213
  # ---------------------------------------------------------------------------
202
214
 
203
- """on_iteration callback — checks abort flag between ReAct iterations."""
215
+ """on_iteration callback — checks abort flag and tracks real iteration count."""
204
216
  def _iteration_hook(ctx: IterationContext) -> IterationAction {
217
+ set_iter_count(ctx.iteration);
205
218
  if is_abort_requested() {
206
219
  return IterationAction.ABORT;
207
220
  }
@@ -217,6 +230,9 @@ walker WorkerAgent {
217
230
 
218
231
  def do_work(task_str: str) -> str by llm(
219
232
  tools=[
233
+ # Reason
234
+ think,
235
+ # Understand
220
236
  load_jac_skill,
221
237
  analyze_project,
222
238
  find_symbol,
@@ -224,26 +240,18 @@ walker WorkerAgent {
224
240
  list_files,
225
241
  grep_search,
226
242
  find_files,
243
+ # Act
227
244
  write_code,
228
245
  edit_code,
229
246
  run_command,
230
247
  jac_run,
231
- scaffold_project,
248
+ # Git — status only, workers don't commit
232
249
  git_status,
233
- git_diff,
234
- git_commit,
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
250
+ # Validate
251
+ browser_validate
244
252
  ],
245
253
  on_iteration=_iteration_hook,
246
- max_react_iterations=60,
254
+ max_react_iterations=30,
247
255
  temperature=0.2,
248
256
  max_tokens=8192,
249
257
  stream=True,