jac-coder 0.1.0__py3-none-any.whl
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/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- jac_coder-0.1.0.dist-info/top_level.txt +1 -0
jac_coder/nodes.jac
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import os;
|
|
2
|
+
|
|
3
|
+
import from uuid { uuid4 }
|
|
4
|
+
import from datetime { datetime }
|
|
5
|
+
import from jac_coder.config { llm, get_config, get_data_dir }
|
|
6
|
+
|
|
7
|
+
# MainAgent tools — full orchestrator set
|
|
8
|
+
import from jac_coder.tool.think { think }
|
|
9
|
+
import from jac_coder.tool.delegation { spawn_agent }
|
|
10
|
+
import from jac_coder.tool.todo { update_todos }
|
|
11
|
+
import from jac_coder.tool.jac_docs { jac_docs }
|
|
12
|
+
import from jac_coder.tool.question { ask_question }
|
|
13
|
+
import from jac_coder.tool.scaffold { scaffold_project }
|
|
14
|
+
import from jac_coder.tool.web { web_fetch, web_search }
|
|
15
|
+
import from jac_coder.tool.guarded { run_command }
|
|
16
|
+
import from jac_coder.tool.jac_tools { jac_check, jac_run }
|
|
17
|
+
import from jac_coder.tool.search { grep_search, find_files }
|
|
18
|
+
import from jac_coder.tool.filesystem { read_file, list_files }
|
|
19
|
+
import from jac_coder.tool.git { git_status, git_diff, git_commit, git_log }
|
|
20
|
+
import from jac_coder.tool.checked { write_code, edit_code }
|
|
21
|
+
import from jac_coder.tool.jac_analyzer { analyze_project, find_symbol }
|
|
22
|
+
import from jac_coder.tool.validate { validate_project }
|
|
23
|
+
import from jac_coder.tool.preview { capture_preview, evaluate_preview }
|
|
24
|
+
import from jac_coder.tool.mcp { mcp_call }
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_subagent_rules() -> dict {
|
|
28
|
+
data_dir = get_data_dir();
|
|
29
|
+
rules: dict = {};
|
|
30
|
+
rules_dir = os.path.join(data_dir, "rules");
|
|
31
|
+
# Load all rule files from rules/ directory
|
|
32
|
+
for (key, filename) in [
|
|
33
|
+
("core_jac_rules", "core_jac.md"),
|
|
34
|
+
("fullstack_rules", "fullstack.md"),
|
|
35
|
+
("workflow_rules", "workflow.md")
|
|
36
|
+
] {
|
|
37
|
+
filepath = os.path.join(rules_dir, filename);
|
|
38
|
+
try {
|
|
39
|
+
with open(filepath, "r") as f {
|
|
40
|
+
rules[key] = f.read();
|
|
41
|
+
}
|
|
42
|
+
} except (FileNotFoundError, OSError) {
|
|
43
|
+
rules[key] = "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
# Build TOC of available docs for jac_docs() tool
|
|
47
|
+
toc_lines: list = ["Call jac_docs(query) to look up Jac syntax. Available topics:"];
|
|
48
|
+
for subdir in ["reference", "examples"] {
|
|
49
|
+
search_dir = os.path.join(data_dir, subdir);
|
|
50
|
+
if not os.path.isdir(search_dir) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for fname in sorted(os.listdir(search_dir)) {
|
|
54
|
+
if not fname.endswith(".md") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
filepath = os.path.join(search_dir, fname);
|
|
58
|
+
try {
|
|
59
|
+
with open(filepath, "r") as f {
|
|
60
|
+
content = f.read();
|
|
61
|
+
}
|
|
62
|
+
headings = [
|
|
63
|
+
line.replace("## ", "").strip()
|
|
64
|
+
for line in content.split("\n")
|
|
65
|
+
if line.startswith("## ")
|
|
66
|
+
];
|
|
67
|
+
label = subdir + "/" + fname.replace(".md", "");
|
|
68
|
+
toc_lines.append(f" {label}: {', '.join(headings)}");
|
|
69
|
+
} except (FileNotFoundError, OSError) { }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
rules["available_docs"] = "\n".join(toc_lines);
|
|
73
|
+
return rules;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Response objects
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
obj AgentResponse {
|
|
80
|
+
has content: str;
|
|
81
|
+
has agent_mode: str;
|
|
82
|
+
has tools_used: list[str];
|
|
83
|
+
has files_modified: list[str];
|
|
84
|
+
has has_errors: bool;
|
|
85
|
+
has next_steps: list[str];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
sem AgentResponse.content = "Main response text to display to user";
|
|
89
|
+
sem AgentResponse.agent_mode = "Always 'main' for MainAgent responses";
|
|
90
|
+
sem AgentResponse.tools_used = "List of tool names that were called during execution";
|
|
91
|
+
sem AgentResponse.files_modified = "List of file paths that were written or edited";
|
|
92
|
+
sem AgentResponse.has_errors = "TRUE if any tool execution failed, FALSE otherwise";
|
|
93
|
+
sem AgentResponse.next_steps = "Suggested follow-up actions for the user (empty list if none)";
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
obj SubAgentResult {
|
|
97
|
+
has content: str;
|
|
98
|
+
has files_modified: list[str];
|
|
99
|
+
has errors: list[str];
|
|
100
|
+
has tools_used: list[str];
|
|
101
|
+
has iterations_used: int;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
sem SubAgentResult.content = "The SubAgent's response to the task";
|
|
105
|
+
sem SubAgentResult.files_modified = "Files written or edited during execution";
|
|
106
|
+
sem SubAgentResult.errors = "Errors encountered (jac_check failures, tool errors)";
|
|
107
|
+
sem SubAgentResult.tools_used = "Tools called during execution";
|
|
108
|
+
sem SubAgentResult.iterations_used = "Number of ReAct iterations consumed";
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
obj ProjectScan {
|
|
112
|
+
has architecture: str;
|
|
113
|
+
has file_map: dict[str, str];
|
|
114
|
+
has conventions: list[str];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
obj SessionLearnings {
|
|
119
|
+
has new_files: dict[str, str];
|
|
120
|
+
has new_conventions: list[str];
|
|
121
|
+
has new_decisions: list[str];
|
|
122
|
+
has known_issues: list[str];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# McpRegistry — persists MCP server configs in the graph
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
node McpRegistry {
|
|
130
|
+
has servers: dict[str, dict] = {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# ProjectMemory — persists codebase knowledge across sessions
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
node ProjectMemory {
|
|
138
|
+
has project_dir: str = "",
|
|
139
|
+
architecture: str = "",
|
|
140
|
+
file_map: dict[str, str] = {},
|
|
141
|
+
conventions: list[str] = [],
|
|
142
|
+
past_decisions: list[str] = [],
|
|
143
|
+
known_issues: list[str] = [],
|
|
144
|
+
graph_topology: str = "",
|
|
145
|
+
node_details: str = "",
|
|
146
|
+
walker_details: str = "",
|
|
147
|
+
import_map: str = "",
|
|
148
|
+
scan_attempted: bool = False,
|
|
149
|
+
last_updated: str = "";
|
|
150
|
+
|
|
151
|
+
def profile_project(file_tree: str, key_files: str) -> ProjectScan by llm(
|
|
152
|
+
temperature=0.1
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
def collect_learnings(
|
|
156
|
+
files_modified: list[str], session_summary: str
|
|
157
|
+
) -> SessionLearnings by llm(temperature=0.1);
|
|
158
|
+
|
|
159
|
+
def summarize() -> str;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Session — persistent chat state
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
node Session {
|
|
167
|
+
has id: str = "",
|
|
168
|
+
title: str = "New Session",
|
|
169
|
+
directory: str = "",
|
|
170
|
+
agent: str = "main",
|
|
171
|
+
status: str = "active",
|
|
172
|
+
created_at: str = "",
|
|
173
|
+
updated_at: str = "",
|
|
174
|
+
chat_history: list[dict] = [],
|
|
175
|
+
last_agent: str = "",
|
|
176
|
+
active_files: list[str] = [],
|
|
177
|
+
pending_errors: list[str] = [],
|
|
178
|
+
project_summary: str = "";
|
|
179
|
+
|
|
180
|
+
def postinit;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# MainAgent — the orchestrator node
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
node MainAgent {
|
|
188
|
+
def respond(message: str, chat_history: list[dict]) -> str by llm(
|
|
189
|
+
tools=[
|
|
190
|
+
# Think — explicit reasoning before acting or delegating
|
|
191
|
+
think,
|
|
192
|
+
# Understand — read, search, analyze the codebase
|
|
193
|
+
read_file,
|
|
194
|
+
grep_search,
|
|
195
|
+
find_files,
|
|
196
|
+
list_files,
|
|
197
|
+
analyze_project,
|
|
198
|
+
find_symbol,
|
|
199
|
+
jac_docs,
|
|
200
|
+
# Act — handle simple tasks directly
|
|
201
|
+
edit_code,
|
|
202
|
+
write_code,
|
|
203
|
+
run_command,
|
|
204
|
+
jac_run,
|
|
205
|
+
validate_project,
|
|
206
|
+
scaffold_project,
|
|
207
|
+
# Git — first-class version control
|
|
208
|
+
git_status,
|
|
209
|
+
git_diff,
|
|
210
|
+
git_log,
|
|
211
|
+
git_commit,
|
|
212
|
+
# Web — search and fetch external resources
|
|
213
|
+
web_fetch,
|
|
214
|
+
web_search,
|
|
215
|
+
# Visual — screenshot and evaluate live preview UI
|
|
216
|
+
capture_preview,
|
|
217
|
+
evaluate_preview,
|
|
218
|
+
# Delegate — spawn SubAgent walkers for complex tasks
|
|
219
|
+
spawn_agent,
|
|
220
|
+
# Interact — ask user questions, track todos
|
|
221
|
+
ask_question,
|
|
222
|
+
update_todos,
|
|
223
|
+
# MCP — call tools from connected MCP servers
|
|
224
|
+
mcp_call
|
|
225
|
+
],
|
|
226
|
+
incl_info=_subagent_rules,
|
|
227
|
+
max_react_iterations=40,
|
|
228
|
+
temperature=0.2,
|
|
229
|
+
stream=True,
|
|
230
|
+
logging=True
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# SubAgent walkers — spawned on MainAgent node for task delegation
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
glob _subagent_rules: dict = load_subagent_rules();
|
|
239
|
+
|
|
240
|
+
walker WorkerAgent {
|
|
241
|
+
has task: str;
|
|
242
|
+
has result_content: str = "";
|
|
243
|
+
has result_tools: list[str] = [];
|
|
244
|
+
has result_files: list[str] = [];
|
|
245
|
+
|
|
246
|
+
def do_work(task_str: str) -> str by llm(
|
|
247
|
+
tools=[
|
|
248
|
+
jac_docs,
|
|
249
|
+
analyze_project,
|
|
250
|
+
find_symbol,
|
|
251
|
+
read_file,
|
|
252
|
+
list_files,
|
|
253
|
+
grep_search,
|
|
254
|
+
find_files,
|
|
255
|
+
write_code,
|
|
256
|
+
edit_code,
|
|
257
|
+
run_command,
|
|
258
|
+
jac_run,
|
|
259
|
+
validate_project,
|
|
260
|
+
scaffold_project,
|
|
261
|
+
git_status,
|
|
262
|
+
git_diff,
|
|
263
|
+
git_commit,
|
|
264
|
+
git_log,
|
|
265
|
+
web_fetch,
|
|
266
|
+
web_search,
|
|
267
|
+
capture_preview,
|
|
268
|
+
evaluate_preview,
|
|
269
|
+
mcp_call
|
|
270
|
+
],
|
|
271
|
+
incl_info=_subagent_rules,
|
|
272
|
+
max_react_iterations=30,
|
|
273
|
+
temperature=0.2,
|
|
274
|
+
stream=True,
|
|
275
|
+
logging=True
|
|
276
|
+
);
|
|
277
|
+
can execute with MainAgent entry;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
walker ExplorerAgent {
|
|
281
|
+
has task: str;
|
|
282
|
+
has result_content: str = "";
|
|
283
|
+
has result_tools: list[str] = [];
|
|
284
|
+
has result_files: list[str] = [];
|
|
285
|
+
|
|
286
|
+
def do_work(task_str: str) -> str by llm(
|
|
287
|
+
tools=[
|
|
288
|
+
jac_docs,
|
|
289
|
+
analyze_project,
|
|
290
|
+
find_symbol,
|
|
291
|
+
read_file,
|
|
292
|
+
list_files,
|
|
293
|
+
grep_search,
|
|
294
|
+
find_files,
|
|
295
|
+
git_log,
|
|
296
|
+
web_search,
|
|
297
|
+
web_fetch
|
|
298
|
+
],
|
|
299
|
+
incl_info=_subagent_rules,
|
|
300
|
+
max_react_iterations=30,
|
|
301
|
+
temperature=0.2,
|
|
302
|
+
stream=True,
|
|
303
|
+
logging=True
|
|
304
|
+
);
|
|
305
|
+
can execute with MainAgent entry;
|
|
306
|
+
}
|
jac_coder/permission.jac
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fnmatch;
|
|
2
|
+
|
|
3
|
+
obj PermissionRule {
|
|
4
|
+
has tool: str = "*",
|
|
5
|
+
pattern: str = "*",
|
|
6
|
+
action: str = "ask"; # "allow" | "deny" | "ask"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
obj PermissionEngine {
|
|
10
|
+
has rules: list = [],
|
|
11
|
+
always_allowed: dict = {};
|
|
12
|
+
|
|
13
|
+
def init_defaults() -> None;
|
|
14
|
+
def check(tool_id: str, resource: str = "") -> str;
|
|
15
|
+
def remember_allow(tool_id: str, pattern: str = "*") -> None;
|
|
16
|
+
def enable_web_mode() -> None;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
glob permission_engine: PermissionEngine = PermissionEngine();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Entry point for `jac-coder-server` command.
|
|
2
|
+
|
|
3
|
+
Mirrors the jaseci cli_boot.jac pattern — jaclang's import hook allows this
|
|
4
|
+
.jac file to be referenced directly from pyproject.toml [project.scripts]:
|
|
5
|
+
|
|
6
|
+
jac-coder-server = "jac_coder.serve_entry:main"
|
|
7
|
+
|
|
8
|
+
Finds server.jac inside the installed jac_coder package and runs it with
|
|
9
|
+
`jac run`. Works both in dev (editable install) and after pip install.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os;
|
|
13
|
+
import sys;
|
|
14
|
+
import subprocess;
|
|
15
|
+
import from pathlib { Path }
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
"""Start the JacCoder stdio server."""
|
|
19
|
+
def main -> None {
|
|
20
|
+
# __file__ is jac_coder/serve_entry.jac — server.jac lives next to it.
|
|
21
|
+
pkg_dir = Path(__file__).parent;
|
|
22
|
+
server_jac = pkg_dir / "server.jac";
|
|
23
|
+
|
|
24
|
+
if not server_jac.exists() {
|
|
25
|
+
print(f"Error: server.jac not found at {server_jac}", file=sys.stderr);
|
|
26
|
+
sys.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sys.exit(subprocess.call(["jac", "run", str(server_jac)] + sys.argv[1:]));
|
|
30
|
+
}
|
jac_coder/server.jac
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""JacCoder stdio server.
|
|
2
|
+
|
|
3
|
+
This is the bridge between the VS Code extension (TypeScript) and the
|
|
4
|
+
JacCoder AI agent (Jac). The extension spawns this file as a subprocess
|
|
5
|
+
and they talk to each other through stdin/stdout using JSON messages.
|
|
6
|
+
|
|
7
|
+
How it works:
|
|
8
|
+
Extension --> sends a JSON request --> stdin --> this server
|
|
9
|
+
This server --> does the work --> stdout --> extension
|
|
10
|
+
|
|
11
|
+
Message format (JSON-RPC 2.0):
|
|
12
|
+
Request (extension asks something, has an "id"):
|
|
13
|
+
health → check if server is alive
|
|
14
|
+
session.create → start a new chat session
|
|
15
|
+
session.list → get all sessions
|
|
16
|
+
session.get → get one session by id
|
|
17
|
+
session.close → close a session
|
|
18
|
+
chat → send a message to the AI agent
|
|
19
|
+
mcp.add → register an MCP tool server
|
|
20
|
+
mcp.remove → remove an MCP tool server
|
|
21
|
+
mcp.list → list all MCP tool servers
|
|
22
|
+
|
|
23
|
+
Notification (fire-and-forget, no "id"):
|
|
24
|
+
chat.cancel → stop the currently running agent (sent by extension)
|
|
25
|
+
server.ready → server started successfully (sent by us, once)
|
|
26
|
+
chat.event → live updates while agent is thinking/running tools
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import sys;
|
|
30
|
+
import os;
|
|
31
|
+
import json;
|
|
32
|
+
import asyncio;
|
|
33
|
+
import logging;
|
|
34
|
+
import threading;
|
|
35
|
+
import from typing { Any }
|
|
36
|
+
|
|
37
|
+
import from jac_coder.api {
|
|
38
|
+
initialize,
|
|
39
|
+
chat as _api_chat,
|
|
40
|
+
create_session as _api_create_session,
|
|
41
|
+
list_sessions as _api_list_sessions,
|
|
42
|
+
get_session as _api_get_session,
|
|
43
|
+
close_session as _api_close_session,
|
|
44
|
+
api_mcp_add as _api_mcp_add,
|
|
45
|
+
api_mcp_remove as _api_mcp_remove,
|
|
46
|
+
api_mcp_list as _api_mcp_list
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Debug logs — goes to stderr, not stdout, so JSON messages stay clean.
|
|
51
|
+
glob logger = logging.getLogger("jaccoder.stdio");
|
|
52
|
+
|
|
53
|
+
# Save a private copy of stdout before we redirect it to stderr.
|
|
54
|
+
# All JSON writes use this — so accidental print() calls never reach the extension.
|
|
55
|
+
glob _real_stdout: Any = os.fdopen(os.dup(1), 'w', 1);
|
|
56
|
+
|
|
57
|
+
# Prevents two threads writing JSON at the same time and mixing the output.
|
|
58
|
+
glob _stdout_lock: Any = threading.Lock();
|
|
59
|
+
|
|
60
|
+
# Tracks cancelled sessions — set to True when user hits cancel.
|
|
61
|
+
glob _stop_flags: dict = {};
|
|
62
|
+
|
|
63
|
+
# Holds the running thread per session — used to force-stop it on cancel.
|
|
64
|
+
glob _chat_threads: dict = {};
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# JSON-RPC 2.0 message builders
|
|
69
|
+
# These just build the correct dict shape — nothing else.
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
"""Build a success response for a request."""
|
|
73
|
+
def _ok(req_id: Any, result: Any) -> dict {
|
|
74
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
"""Build an error response for a request."""
|
|
78
|
+
def _err(req_id: Any, code: int, message: str) -> dict {
|
|
79
|
+
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
"""Build a server-push notification (no id — no reply expected)."""
|
|
83
|
+
def _notify(method: str, params: dict) -> dict {
|
|
84
|
+
return {"jsonrpc": "2.0", "method": method, "params": params};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
"""Build a cancelled response — sent when the user aborts a chat turn."""
|
|
88
|
+
def _cancelled(req_id: Any) -> dict {
|
|
89
|
+
return _ok(req_id, {"response": "", "agent": "cancelled", "cancelled": True});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Writes one JSON message to the extension — one at a time, thread-safe.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
"""Write one JSON message to stdout (thread-safe)."""
|
|
98
|
+
def _write(msg: dict) -> None {
|
|
99
|
+
global _stdout_lock, _real_stdout;
|
|
100
|
+
line = json.dumps(msg, ensure_ascii=False);
|
|
101
|
+
with _stdout_lock {
|
|
102
|
+
_real_stdout.write(line + "\n");
|
|
103
|
+
_real_stdout.flush();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Chat handler
|
|
110
|
+
#
|
|
111
|
+
# Each chat turn runs in its own thread because the LLM call is blocking
|
|
112
|
+
# (can take seconds or minutes). We can't block the main asyncio loop or
|
|
113
|
+
# we'd never be able to receive a chat.cancel notification while waiting.
|
|
114
|
+
#
|
|
115
|
+
# Flow:
|
|
116
|
+
# 1. Thread starts, calls the AI agent (_api_chat)
|
|
117
|
+
# 2. Agent fires on_event() for every tool it runs → we stream those back
|
|
118
|
+
# 3. When agent finishes → we send the final response
|
|
119
|
+
# 4. If user cancelled → we send a cancelled frame instead
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
"""Run one chat turn in a background thread and stream results back."""
|
|
123
|
+
def _handle_chat(req_id: Any, params: dict) -> None {
|
|
124
|
+
session_id: str = params.get("session_id", "");
|
|
125
|
+
message: str = params.get("message", "");
|
|
126
|
+
agent_context: str = params.get("agent_context", "") or "";
|
|
127
|
+
|
|
128
|
+
_stop_flags[session_id] = False;
|
|
129
|
+
|
|
130
|
+
# Called by the agent each time it does something (reads a file, calls a tool, etc.)
|
|
131
|
+
# We forward these as chat.event notifications so the UI can show live progress.
|
|
132
|
+
def on_event(event_type: str, data: dict) -> None {
|
|
133
|
+
if _stop_flags.get(session_id) {
|
|
134
|
+
raise KeyboardInterrupt("cancelled"); # unwinds the agent immediately
|
|
135
|
+
}
|
|
136
|
+
_write(_notify("chat.event", {"type": event_type, **data}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
result = _api_chat(
|
|
141
|
+
session_id=session_id,
|
|
142
|
+
message=message,
|
|
143
|
+
on_event=on_event,
|
|
144
|
+
agent_context=agent_context
|
|
145
|
+
);
|
|
146
|
+
if _stop_flags.get(session_id) {
|
|
147
|
+
_write(_cancelled(req_id));
|
|
148
|
+
} else {
|
|
149
|
+
_write(_ok(req_id, result));
|
|
150
|
+
}
|
|
151
|
+
} except (KeyboardInterrupt, InterruptedError) {
|
|
152
|
+
_write(_cancelled(req_id));
|
|
153
|
+
} except Exception as e {
|
|
154
|
+
logger.error("chat error: %s", e);
|
|
155
|
+
_write(_err(req_id, -32000, str(e)));
|
|
156
|
+
} finally {
|
|
157
|
+
_stop_flags.pop(session_id, None);
|
|
158
|
+
_chat_threads.pop(session_id, None);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Request dispatcher
|
|
165
|
+
#
|
|
166
|
+
# Routes each incoming request to the right API call and sends back the result.
|
|
167
|
+
# Runs inside the asyncio event loop so Jac graph operations (root, spawn, -->)
|
|
168
|
+
# are always available.
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
"""Route one JSON-RPC request to the correct API function."""
|
|
172
|
+
async def _dispatch(req_id: Any, method: str, params: dict) -> None {
|
|
173
|
+
try {
|
|
174
|
+
if method == "health" {
|
|
175
|
+
_write(_ok(req_id, {"status": "ok", "service": "jac-coder"}));
|
|
176
|
+
|
|
177
|
+
} elif method == "session.create" {
|
|
178
|
+
_write(_ok(req_id, _api_create_session(
|
|
179
|
+
directory=params.get("directory", ""),
|
|
180
|
+
title=params.get("title", "New Session")
|
|
181
|
+
)));
|
|
182
|
+
|
|
183
|
+
} elif method == "session.list" {
|
|
184
|
+
_write(_ok(req_id, {"sessions": _api_list_sessions()}));
|
|
185
|
+
|
|
186
|
+
} elif method == "session.get" {
|
|
187
|
+
_write(_ok(req_id, _api_get_session(session_id=params["session_id"])));
|
|
188
|
+
|
|
189
|
+
} elif method == "session.close" {
|
|
190
|
+
_write(_ok(req_id, _api_close_session(session_id=params["session_id"])));
|
|
191
|
+
|
|
192
|
+
} elif method == "chat" {
|
|
193
|
+
# Chat runs in a thread — LLM calls are blocking and can take a long time.
|
|
194
|
+
# The thread writes events and the final response directly via _write().
|
|
195
|
+
sid: str = params.get("session_id", "");
|
|
196
|
+
chat_thread = threading.Thread(
|
|
197
|
+
target=_handle_chat,
|
|
198
|
+
args=(req_id, params),
|
|
199
|
+
daemon=True,
|
|
200
|
+
name=f"chat-{sid[:8]}"
|
|
201
|
+
);
|
|
202
|
+
_chat_threads[sid] = chat_thread;
|
|
203
|
+
chat_thread.start();
|
|
204
|
+
|
|
205
|
+
} elif method == "mcp.add" {
|
|
206
|
+
_write(_ok(req_id, _api_mcp_add(
|
|
207
|
+
name=params["name"],
|
|
208
|
+
config=params["config"]
|
|
209
|
+
)));
|
|
210
|
+
|
|
211
|
+
} elif method == "mcp.remove" {
|
|
212
|
+
_write(_ok(req_id, _api_mcp_remove(name=params["name"])));
|
|
213
|
+
|
|
214
|
+
} elif method == "mcp.list" {
|
|
215
|
+
_write(_ok(req_id, {"servers": _api_mcp_list()}));
|
|
216
|
+
|
|
217
|
+
} else {
|
|
218
|
+
_write(_err(req_id, -32601, f"Method not found: {method}"));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
} except KeyError as exc {
|
|
222
|
+
_write(_err(req_id, -32602, f"Missing required param: {exc}"));
|
|
223
|
+
} except BaseException as exc {
|
|
224
|
+
logger.error("dispatch [%s]: %s", method, exc);
|
|
225
|
+
_write(_err(req_id, -32000, str(exc)));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Main loop
|
|
232
|
+
#
|
|
233
|
+
# Reads one JSON message per line from stdin.
|
|
234
|
+
# - If the message has an "id" → it's a request, dispatch it.
|
|
235
|
+
# - If no "id" → it's a notification (currently only chat.cancel).
|
|
236
|
+
# - Empty line → EOF → extension closed, exit cleanly.
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
"""Read stdin forever and dispatch each incoming message."""
|
|
240
|
+
async def _main() -> None {
|
|
241
|
+
loop = asyncio.get_event_loop();
|
|
242
|
+
|
|
243
|
+
# Tell the extension we are ready to receive requests.
|
|
244
|
+
_write(_notify("server.ready", {"service": "jac-coder"}));
|
|
245
|
+
|
|
246
|
+
while True {
|
|
247
|
+
try {
|
|
248
|
+
# run_in_executor keeps stdin reading off the async loop
|
|
249
|
+
# so the loop stays free to handle other tasks while waiting.
|
|
250
|
+
line = await loop.run_in_executor(None, sys.stdin.readline);
|
|
251
|
+
} except Exception as e {
|
|
252
|
+
logger.error("stdin read error: %s", e);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if not line {
|
|
257
|
+
# Empty string = EOF = extension disconnected. Exit cleanly.
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
line = line.strip();
|
|
262
|
+
if not line {
|
|
263
|
+
continue; # blank line, skip
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
msg: dict = json.loads(line);
|
|
268
|
+
} except Exception {
|
|
269
|
+
_write(_err(None, -32700, "Invalid JSON"));
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
method: str = msg.get("method", "");
|
|
274
|
+
params: dict = msg.get("params") or {};
|
|
275
|
+
req_id: Any = msg.get("id");
|
|
276
|
+
|
|
277
|
+
if "id" not in msg {
|
|
278
|
+
# Notification (no reply needed) — only chat.cancel is handled.
|
|
279
|
+
if method == "chat.cancel" {
|
|
280
|
+
sid: str = params.get("session_id", "");
|
|
281
|
+
if sid {
|
|
282
|
+
_stop_flags[sid] = True;
|
|
283
|
+
# Force-stop the thread even if it's stuck in a blocking LLM call.
|
|
284
|
+
running_thread = _chat_threads.get(sid);
|
|
285
|
+
if running_thread and running_thread.is_alive() {
|
|
286
|
+
import ctypes;
|
|
287
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
288
|
+
ctypes.c_ulong(running_thread.ident),
|
|
289
|
+
ctypes.py_object(KeyboardInterrupt)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# It's a request — schedule it on the event loop and keep reading.
|
|
298
|
+
asyncio.ensure_future(_dispatch(req_id, method, params));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# Entry point — runs when: jac run server.jac
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
with entry {
|
|
308
|
+
# Log warnings and above to stderr (safe — won't touch the JSON channel).
|
|
309
|
+
logging.basicConfig(
|
|
310
|
+
level=logging.WARNING,
|
|
311
|
+
format="[jaccoder] %(levelname)s %(name)s: %(message)s",
|
|
312
|
+
stream=sys.stderr
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
# Redirect stdout → stderr so accidental print() calls never corrupt the JSON pipe.
|
|
316
|
+
os.dup2(2, 1);
|
|
317
|
+
sys.stdout = sys.stderr;
|
|
318
|
+
|
|
319
|
+
# Boot the Jac agent (loads config, connects LLM, sets up the graph).
|
|
320
|
+
initialize("web");
|
|
321
|
+
|
|
322
|
+
# Start the async event loop — runs forever until stdin closes.
|
|
323
|
+
asyncio.run(_main());
|
|
324
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import from jac_coder.tool.shell { bash_exec }
|
|
2
|
+
import from jac_coder.tool.think { think }
|
|
3
|
+
import from jac_coder.tool.delegation { spawn_agent }
|
|
4
|
+
import from jac_coder.tool.jac_docs { jac_docs }
|
|
5
|
+
import from jac_coder.tool.question { ask_question }
|
|
6
|
+
import from jac_coder.tool.web { web_fetch, web_search }
|
|
7
|
+
import from jac_coder.tool.scaffold { scaffold_project }
|
|
8
|
+
import from jac_coder.tool.search { grep_search, find_files }
|
|
9
|
+
import from jac_coder.tool.todo { update_todos, get_todos }
|
|
10
|
+
import from jac_coder.tool.jac_tools { jac_check, jac_run }
|
|
11
|
+
import from jac_coder.tool.validate { validate_project }
|
|
12
|
+
import from jac_coder.tool.git { git_status, git_diff, git_commit, git_log }
|
|
13
|
+
import from jac_coder.tool.checked { write_code, edit_code }
|
|
14
|
+
import from jac_coder.tool.guarded { write_file_guarded, edit_file_guarded, run_command }
|
|
15
|
+
import from jac_coder.tool.filesystem { read_file, write_file, edit_file, list_files }
|
|
16
|
+
import from jac_coder.tool.jac_analyzer { analyze_project, find_symbol }
|
|
17
|
+
import from jac_coder.tool.preview { capture_preview, evaluate_preview }
|