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
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# init
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
impl initialize(mode: str = "web") -> None {
|
|
5
|
+
if mode == "web" {
|
|
6
|
+
os.environ["JACCODER_WEB_MODE"] = "1";
|
|
7
|
+
permission_engine.init_defaults();
|
|
8
|
+
permission_engine.enable_web_mode();
|
|
9
|
+
} else {
|
|
10
|
+
permission_engine.init_defaults();
|
|
11
|
+
}
|
|
12
|
+
# Register jac-mcp using the current venv's python so it's always available.
|
|
13
|
+
import sys as _sys;
|
|
14
|
+
try {
|
|
15
|
+
jac_cmd = [_sys.executable, "-m", "jaclang", "mcp"];
|
|
16
|
+
mcp_register_builtin("jac-mcp", {"type": "stdio", "command": jac_cmd});
|
|
17
|
+
} except Exception as e {
|
|
18
|
+
_sys.stderr.write(f"[mcp] jac-mcp not available: {e}. Install with: pip install jac-mcp\n");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Session lifecycle
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
impl create_session(directory: str, title: str = "", agent: str = "main") -> dict {
|
|
27
|
+
session_title = title if title else f"Session-{datetime.now().strftime('%H%M%S')}";
|
|
28
|
+
try {
|
|
29
|
+
before_ids = set([s.id for s in [root()-->][?:Session]]);
|
|
30
|
+
} except Exception {
|
|
31
|
+
before_ids = set();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
root() spawn new_session(title=session_title, directory=directory);
|
|
35
|
+
|
|
36
|
+
session_id = "";
|
|
37
|
+
session_obj = None;
|
|
38
|
+
for s in [root()-->][?:Session] {
|
|
39
|
+
if s.id not in before_ids {
|
|
40
|
+
session_id = s.id;
|
|
41
|
+
session_obj = s;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if not session_id {
|
|
46
|
+
return {"error": "Failed to create session"};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Store in registry so background threads can find it
|
|
50
|
+
_session_registry[session_id] = session_obj;
|
|
51
|
+
|
|
52
|
+
return {"session_id": session_id, "title": session_title, "status": "created"};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
impl list_sessions() -> list {
|
|
57
|
+
result: list = [];
|
|
58
|
+
try {
|
|
59
|
+
sessions = [root()-->][?:Session][?status=="active"];
|
|
60
|
+
} except Exception {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
for s in sessions {
|
|
64
|
+
try {
|
|
65
|
+
user_msgs = len([m for m in s.chat_history if m.get("role") == "user"]);
|
|
66
|
+
result.append(
|
|
67
|
+
{
|
|
68
|
+
"id": s.id,
|
|
69
|
+
"title": s.title,
|
|
70
|
+
"message_count": user_msgs,
|
|
71
|
+
"updated_at": s.updated_at
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
} except Exception {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
result.sort(key=lambda x : x["updated_at"], reverse=True);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
impl get_session(session_id: str) -> dict {
|
|
84
|
+
matches = [root()-->][?:Session][?id==session_id];
|
|
85
|
+
if not matches {
|
|
86
|
+
return {"error": "Session not found"};
|
|
87
|
+
}
|
|
88
|
+
s = matches[0];
|
|
89
|
+
return {
|
|
90
|
+
"id": s.id,
|
|
91
|
+
"title": s.title,
|
|
92
|
+
"status": s.status,
|
|
93
|
+
"directory": s.directory,
|
|
94
|
+
"chat_history": s.chat_history,
|
|
95
|
+
"created_at": s.created_at,
|
|
96
|
+
"updated_at": s.updated_at
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
impl close_session(session_id: str) -> dict {
|
|
102
|
+
matches = [root()-->][?:Session][?id==session_id];
|
|
103
|
+
if not matches {
|
|
104
|
+
return {"error": "Session not found"};
|
|
105
|
+
}
|
|
106
|
+
matches[0].status = "closed";
|
|
107
|
+
matches[0].updated_at = datetime.now().isoformat();
|
|
108
|
+
_session_registry.pop(session_id, None);
|
|
109
|
+
return {"status": "closed", "id": matches[0].id};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# chat — the main entry point
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
impl chat(
|
|
117
|
+
session_id: str,
|
|
118
|
+
message: str,
|
|
119
|
+
directory: str = "",
|
|
120
|
+
on_event: Any = None,
|
|
121
|
+
mode: str = "full",
|
|
122
|
+
agent_context: str = ""
|
|
123
|
+
) -> dict {
|
|
124
|
+
# Find session — try registry first (works across threads), then graph
|
|
125
|
+
session = _session_registry.get(session_id);
|
|
126
|
+
if not session {
|
|
127
|
+
matches = [root()-->][?:Session][?id==session_id];
|
|
128
|
+
if matches {
|
|
129
|
+
session = matches[0];
|
|
130
|
+
_session_registry[session_id] = session;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if not session {
|
|
134
|
+
return {"error": "Session not found", "agent": "error"};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Override directory if provided
|
|
138
|
+
if directory {
|
|
139
|
+
session.directory = directory;
|
|
140
|
+
}
|
|
141
|
+
work_dir = session.directory;
|
|
142
|
+
|
|
143
|
+
# Setup
|
|
144
|
+
reset_steps();
|
|
145
|
+
start_turn();
|
|
146
|
+
if work_dir {
|
|
147
|
+
set_sandbox_root(work_dir);
|
|
148
|
+
}
|
|
149
|
+
if on_event {
|
|
150
|
+
register_event_callback(on_event);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
# Memory initialization
|
|
155
|
+
_ensure_memory(session, work_dir);
|
|
156
|
+
|
|
157
|
+
# Add user message
|
|
158
|
+
session.chat_history.append({"role": "user", "content": message});
|
|
159
|
+
session.updated_at = datetime.now().isoformat();
|
|
160
|
+
|
|
161
|
+
# Get MainAgent — always resolve fresh (cached refs go stale in jac serve)
|
|
162
|
+
main_agent = ensure_main_agent();
|
|
163
|
+
|
|
164
|
+
# Initialize shared budget for SubAgents
|
|
165
|
+
config = get_config();
|
|
166
|
+
_init_spawn_budget(config.max_react_iterations * 3);
|
|
167
|
+
|
|
168
|
+
# Build context
|
|
169
|
+
ctx_history = build_context(
|
|
170
|
+
session.chat_history,
|
|
171
|
+
session.active_files,
|
|
172
|
+
session.pending_errors,
|
|
173
|
+
project_summary=session.project_summary or ""
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
# Inject agent_context if provided (e.g. from JacBuilder)
|
|
177
|
+
if agent_context {
|
|
178
|
+
ctx_history.insert(0, {"role": "system", "content": agent_context});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Inject available MCP tools into context so agent knows what's callable
|
|
182
|
+
try {
|
|
183
|
+
mcp_tools = mcp_get_tools();
|
|
184
|
+
} except Exception {
|
|
185
|
+
mcp_tools = [];
|
|
186
|
+
}
|
|
187
|
+
if mcp_tools {
|
|
188
|
+
lines: list = [
|
|
189
|
+
"Available MCP tools — call via mcp_call(server_name, tool_name, arguments_json):",
|
|
190
|
+
"Check each tool's inputSchema for required arguments before calling."
|
|
191
|
+
];
|
|
192
|
+
for t in mcp_tools {
|
|
193
|
+
schema_hint = "";
|
|
194
|
+
input_schema = t.get("inputSchema", {});
|
|
195
|
+
if input_schema {
|
|
196
|
+
required = input_schema.get("required", []);
|
|
197
|
+
if required {
|
|
198
|
+
schema_hint = f" required: {', '.join(required)}";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
lines.append(f" - server={t['server']} tool={t['name']} {t['description']}{schema_hint}");
|
|
202
|
+
}
|
|
203
|
+
mcp_sys_msg: dict = {"role": "system", "content": "\n".join(lines)};
|
|
204
|
+
ctx_with_mcp: list[dict] = [mcp_sys_msg];
|
|
205
|
+
ctx_with_mcp.extend(ctx_history);
|
|
206
|
+
ctx_history = ctx_with_mcp;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Call MainAgent — returns StreamEvent generator with logging enabled
|
|
210
|
+
event_stream = main_agent.respond(message=message, chat_history=ctx_history);
|
|
211
|
+
stream_result = _consume_llm_stream(event_stream);
|
|
212
|
+
|
|
213
|
+
response_text = stream_result["content"];
|
|
214
|
+
tools_used = stream_result["tools_used"];
|
|
215
|
+
|
|
216
|
+
# Finalize turn — get accumulated file/error data from tool internals
|
|
217
|
+
tool_end();
|
|
218
|
+
summary = emit_turn_summary();
|
|
219
|
+
|
|
220
|
+
files_modified = summary.get("files_modified", []);
|
|
221
|
+
has_errors = summary.get("errors", 0) > 0;
|
|
222
|
+
|
|
223
|
+
# Update session state
|
|
224
|
+
if files_modified {
|
|
225
|
+
for f in files_modified {
|
|
226
|
+
if f not in session.active_files {
|
|
227
|
+
session.active_files.append(f);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if len(session.active_files) > 10 {
|
|
231
|
+
session.active_files = session.active_files[-10:];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if has_errors {
|
|
235
|
+
files_str = ", ".join(files_modified) if files_modified else "unknown";
|
|
236
|
+
session.pending_errors = [f"Errors in previous turn (files: {files_str})"];
|
|
237
|
+
} else {
|
|
238
|
+
session.pending_errors = [];
|
|
239
|
+
}
|
|
240
|
+
if files_modified and session.directory {
|
|
241
|
+
memory = find_or_create_memory(session.directory);
|
|
242
|
+
if memory {
|
|
243
|
+
update_memory_from_session(
|
|
244
|
+
memory, files_modified, response_text[:500]
|
|
245
|
+
);
|
|
246
|
+
session.project_summary = memory.summarize();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Persist response
|
|
251
|
+
record: dict = {"role": "assistant", "content": response_text, "agent": "main"};
|
|
252
|
+
if tools_used {
|
|
253
|
+
record["tools_used"] = tools_used;
|
|
254
|
+
}
|
|
255
|
+
if files_modified {
|
|
256
|
+
record["files_modified"] = files_modified;
|
|
257
|
+
}
|
|
258
|
+
session.chat_history.append(record);
|
|
259
|
+
session.last_agent = "main";
|
|
260
|
+
session.updated_at = datetime.now().isoformat();
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"response": response_text,
|
|
264
|
+
"agent": "main",
|
|
265
|
+
"tools_used": tools_used,
|
|
266
|
+
"files_modified": files_modified,
|
|
267
|
+
"has_errors": has_errors,
|
|
268
|
+
"next_steps": [],
|
|
269
|
+
"turn_summary": summary
|
|
270
|
+
};
|
|
271
|
+
} except Exception as e {
|
|
272
|
+
sys.stderr.write(f"[api] chat error: {e}\n");
|
|
273
|
+
return {"error": str(e), "agent": "error"};
|
|
274
|
+
} finally {
|
|
275
|
+
if on_event {
|
|
276
|
+
clear_event_callbacks();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Internal helpers
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
def _ensure_memory(session: Session, work_dir: str) -> None {
|
|
286
|
+
if not work_dir or session.project_summary {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
memory = find_or_create_memory(work_dir);
|
|
290
|
+
if not memory {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
has_project = (
|
|
294
|
+
os.path.exists(os.path.join(work_dir, "jac.toml"))
|
|
295
|
+
or os.path.exists(os.path.join(work_dir, "main.jac"))
|
|
296
|
+
);
|
|
297
|
+
if not memory.architecture and not memory.scan_attempted and has_project {
|
|
298
|
+
_init_memory(memory, work_dir);
|
|
299
|
+
}
|
|
300
|
+
summary = memory.summarize();
|
|
301
|
+
if summary {
|
|
302
|
+
session.project_summary = summary;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _post_process(session: Session, response_obj: Any) -> None {
|
|
308
|
+
# Track files
|
|
309
|
+
if response_obj.files_modified {
|
|
310
|
+
for f in response_obj.files_modified {
|
|
311
|
+
if f not in session.active_files {
|
|
312
|
+
session.active_files.append(f);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if len(session.active_files) > 10 {
|
|
316
|
+
session.active_files = session.active_files[-10:];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Track errors
|
|
321
|
+
if response_obj.has_errors {
|
|
322
|
+
files_str = ", ".join(response_obj.files_modified)
|
|
323
|
+
if response_obj.files_modified
|
|
324
|
+
else "unknown";
|
|
325
|
+
session.pending_errors = [f"Errors in previous turn (files: {files_str})"];
|
|
326
|
+
} else {
|
|
327
|
+
session.pending_errors = [];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Update memory
|
|
331
|
+
if response_obj.files_modified and session.directory {
|
|
332
|
+
memory = find_or_create_memory(session.directory);
|
|
333
|
+
if memory {
|
|
334
|
+
update_memory_from_session(
|
|
335
|
+
memory, response_obj.files_modified, response_obj.content[:500]
|
|
336
|
+
);
|
|
337
|
+
session.project_summary = memory.summarize();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Persist response
|
|
342
|
+
record: dict = {
|
|
343
|
+
"role": "assistant",
|
|
344
|
+
"content": response_obj.content,
|
|
345
|
+
"agent": "main"
|
|
346
|
+
};
|
|
347
|
+
if response_obj.tools_used {
|
|
348
|
+
record["tools_used"] = response_obj.tools_used;
|
|
349
|
+
}
|
|
350
|
+
if response_obj.files_modified {
|
|
351
|
+
record["files_modified"] = response_obj.files_modified;
|
|
352
|
+
}
|
|
353
|
+
session.chat_history.append(record);
|
|
354
|
+
session.last_agent = "main";
|
|
355
|
+
session.updated_at = datetime.now().isoformat();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# Model management API
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
impl api_set_model(model: str) -> dict {
|
|
363
|
+
return _set_model(model);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
impl api_get_model() -> dict {
|
|
367
|
+
return _get_model();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# MCP management API
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
impl api_mcp_add(name: str, config: dict) -> dict {
|
|
375
|
+
raw = mcp_add_server(name, config);
|
|
376
|
+
result: dict = {**raw};
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
impl api_mcp_remove(name: str) -> dict {
|
|
381
|
+
raw = mcp_remove_server(name);
|
|
382
|
+
result: dict = {**raw};
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
impl api_mcp_list() -> list {
|
|
387
|
+
raw = mcp_list_servers();
|
|
388
|
+
result: list = [*raw];
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _init_spawn_budget(total: int) -> None {
|
|
394
|
+
try {
|
|
395
|
+
import from jac_coder.tool.delegation { init_budget }
|
|
396
|
+
;
|
|
397
|
+
init_budget(total);
|
|
398
|
+
} except Exception { }
|
|
399
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
impl get_home_dir() -> str {
|
|
2
|
+
home = os.path.join(os.path.expanduser("~"), ".jaccoder");
|
|
3
|
+
os.makedirs(home, exist_ok=True);
|
|
4
|
+
return home;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
glob _STR_FIELDS: list = ["default_model"];
|
|
9
|
+
glob _FLOAT_FIELDS: list = ["temperature"];
|
|
10
|
+
glob _INT_FIELDS: list = ["max_tokens", "max_react_iterations"];
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
impl _merge_from_file(config: JacCoderConfig, path: str) -> None {
|
|
14
|
+
try {
|
|
15
|
+
with open(path, "r") as f {
|
|
16
|
+
data = json.load(f);
|
|
17
|
+
}
|
|
18
|
+
} except (json.JSONDecodeError, FileNotFoundError, OSError) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for key in _STR_FIELDS {
|
|
22
|
+
if key in data {
|
|
23
|
+
setattr(config, key, str(data[key]));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for key in _FLOAT_FIELDS {
|
|
27
|
+
if key in data {
|
|
28
|
+
try {
|
|
29
|
+
setattr(config, key, float(data[key]));
|
|
30
|
+
} except (ValueError, TypeError) { }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for key in _INT_FIELDS {
|
|
34
|
+
if key in data {
|
|
35
|
+
try {
|
|
36
|
+
setattr(config, key, int(data[key]));
|
|
37
|
+
} except (ValueError, TypeError) { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
impl load_config(project_dir: str = "") -> JacCoderConfig {
|
|
44
|
+
global _config;
|
|
45
|
+
config = JacCoderConfig();
|
|
46
|
+
|
|
47
|
+
if project_dir {
|
|
48
|
+
config.project_dir = os.path.realpath(project_dir);
|
|
49
|
+
} else {
|
|
50
|
+
config.project_dir = os.getcwd();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
global_path = os.path.join(get_home_dir(), "config.json");
|
|
54
|
+
if os.path.exists(global_path) {
|
|
55
|
+
_merge_from_file(config, global_path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
project_path = os.path.join(config.project_dir, "jaccoder.json");
|
|
59
|
+
if os.path.exists(project_path) {
|
|
60
|
+
_merge_from_file(config, project_path);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
env_overrides: list = [
|
|
64
|
+
("MODEL", "default_model", str),
|
|
65
|
+
("TEMPERATURE", "temperature", float),
|
|
66
|
+
("MAX_TOKENS", "max_tokens", int),
|
|
67
|
+
("MAX_REACT_ITERATIONS", "max_react_iterations", int)
|
|
68
|
+
];
|
|
69
|
+
for (env_key, attr, cast) in env_overrides {
|
|
70
|
+
val = os.environ.get(env_key, "");
|
|
71
|
+
if val {
|
|
72
|
+
try {
|
|
73
|
+
setattr(config, attr, cast(val));
|
|
74
|
+
} except (ValueError, TypeError) { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_config = config;
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
impl get_config() -> JacCoderConfig {
|
|
84
|
+
global _config;
|
|
85
|
+
return _config;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Model switching — simple swap, keys come from env/.env
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
"""Switch the active LLM model. Mutates the existing Model object
|
|
93
|
+
so all modules that imported `llm` see the change immediately."""
|
|
94
|
+
impl set_model(model: str) -> dict {
|
|
95
|
+
global model_name, _config;
|
|
96
|
+
model = model.strip();
|
|
97
|
+
if not model {
|
|
98
|
+
return {"error": "Model name cannot be empty"};
|
|
99
|
+
}
|
|
100
|
+
model_name = model;
|
|
101
|
+
llm.model_name = model;
|
|
102
|
+
_config.default_model = model;
|
|
103
|
+
return {"status": "ok", "model": model};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
"""Return current model name."""
|
|
107
|
+
impl get_model() -> dict {
|
|
108
|
+
return {"model": model_name};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
glob _data_dir_cache: str = "";
|
|
113
|
+
|
|
114
|
+
impl get_data_dir() -> str {
|
|
115
|
+
global _data_dir_cache;
|
|
116
|
+
if _data_dir_cache {
|
|
117
|
+
return _data_dir_cache;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Strategy 1: JACCODER_ROOT env var
|
|
121
|
+
jc_root = os.environ.get("JACCODER_ROOT", "");
|
|
122
|
+
if jc_root {
|
|
123
|
+
candidate = os.path.join(jc_root, "jac_coder", "data");
|
|
124
|
+
if os.path.isdir(os.path.join(candidate, "rules")) {
|
|
125
|
+
_data_dir_cache = candidate;
|
|
126
|
+
return _data_dir_cache;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Strategy 2: walk up from __file__ until we find jac_coder/data/rules
|
|
131
|
+
path = os.path.abspath(__file__);
|
|
132
|
+
for _i in range(10) {
|
|
133
|
+
path = os.path.dirname(path);
|
|
134
|
+
if path == os.path.dirname(path) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
candidate = os.path.join(path, "data");
|
|
138
|
+
if os.path.isdir(os.path.join(candidate, "rules")) {
|
|
139
|
+
_data_dir_cache = candidate;
|
|
140
|
+
return _data_dir_cache;
|
|
141
|
+
}
|
|
142
|
+
candidate = os.path.join(path, "jac_coder", "data");
|
|
143
|
+
if os.path.isdir(os.path.join(candidate, "rules")) {
|
|
144
|
+
_data_dir_cache = candidate;
|
|
145
|
+
return _data_dir_cache;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Strategy 3: CWD based
|
|
150
|
+
for base in [os.getcwd(), os.path.dirname(os.getcwd())] {
|
|
151
|
+
candidate = os.path.join(base, "jac_coder", "data");
|
|
152
|
+
if os.path.isdir(os.path.join(candidate, "rules")) {
|
|
153
|
+
_data_dir_cache = candidate;
|
|
154
|
+
return _data_dir_cache;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Fallback: best guess from __file__
|
|
159
|
+
_data_dir_cache = os.path.join(
|
|
160
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data"
|
|
161
|
+
);
|
|
162
|
+
return _data_dir_cache;
|
|
163
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
def _build_prefix(
|
|
2
|
+
active_files: list[str], pending_errors: list[str], project_summary: str = ""
|
|
3
|
+
) -> list[dict] {
|
|
4
|
+
parts = [];
|
|
5
|
+
if project_summary {
|
|
6
|
+
parts.append("Project context:\n" + project_summary);
|
|
7
|
+
}
|
|
8
|
+
if active_files {
|
|
9
|
+
parts.append("Active files: " + ", ".join(active_files));
|
|
10
|
+
}
|
|
11
|
+
if pending_errors {
|
|
12
|
+
parts.append("Pending errors:\n" + "\n".join(pending_errors));
|
|
13
|
+
}
|
|
14
|
+
if parts {
|
|
15
|
+
return [{"role": "system", "content": "\n\n".join(parts)}];
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _extract_key_actions(messages: list[dict]) -> str {
|
|
22
|
+
files = [];
|
|
23
|
+
for msg in messages {
|
|
24
|
+
if msg.get("role") == "assistant" {
|
|
25
|
+
for f in msg.get("files_modified", []) {
|
|
26
|
+
if f not in files {
|
|
27
|
+
files.append(f);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if files {
|
|
33
|
+
return ", ".join(files[:5]);
|
|
34
|
+
}
|
|
35
|
+
return "exploration and planning";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _select_important(older: list[dict]) -> list[dict] {
|
|
40
|
+
important = [];
|
|
41
|
+
i = 0;
|
|
42
|
+
while i < len(older) {
|
|
43
|
+
msg = older[i];
|
|
44
|
+
if msg.get("role") == "assistant" {
|
|
45
|
+
content = msg.get("content", "");
|
|
46
|
+
files = msg.get("files_modified", []);
|
|
47
|
+
is_important = (
|
|
48
|
+
len(files) > 0
|
|
49
|
+
or "ACTION REQUIRED" in content
|
|
50
|
+
or "VALIDATION ERRORS" in content
|
|
51
|
+
or "RUNTIME ERRORS" in content
|
|
52
|
+
);
|
|
53
|
+
if is_important {
|
|
54
|
+
if i > 0 and older[i - 1] not in important {
|
|
55
|
+
important.append(older[i - 1]);
|
|
56
|
+
}
|
|
57
|
+
important.append(msg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
return important;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
impl estimate_tokens(messages: list[dict]) -> int {
|
|
67
|
+
total = 0;
|
|
68
|
+
for msg in messages {
|
|
69
|
+
total += len(msg.get("content", ""));
|
|
70
|
+
}
|
|
71
|
+
return total // 4;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
impl needs_compaction(messages: list[dict], budget: int = 20000) -> bool {
|
|
76
|
+
return estimate_tokens(messages) > int(budget * 0.8);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
impl build_context(
|
|
81
|
+
chat_history: list[dict],
|
|
82
|
+
active_files: list[str] = [],
|
|
83
|
+
pending_errors: list[str] = [],
|
|
84
|
+
config: ContextConfig | None = None,
|
|
85
|
+
project_summary: str = ""
|
|
86
|
+
) -> list[dict] {
|
|
87
|
+
cfg = config if config else ContextConfig();
|
|
88
|
+
prefix = _build_prefix(active_files, pending_errors, project_summary);
|
|
89
|
+
|
|
90
|
+
if not needs_compaction(chat_history, cfg.token_budget) {
|
|
91
|
+
return prefix + list(chat_history);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
recent_msg_count = cfg.recent_window * 2;
|
|
95
|
+
if len(chat_history) <= recent_msg_count {
|
|
96
|
+
return prefix + list(chat_history);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
recent = chat_history[-recent_msg_count:];
|
|
100
|
+
older = chat_history[:-recent_msg_count];
|
|
101
|
+
|
|
102
|
+
important = _select_important(older);
|
|
103
|
+
|
|
104
|
+
non_important_count = (len(older) - len(important)) // 2;
|
|
105
|
+
result = list(prefix);
|
|
106
|
+
if non_important_count > 0 {
|
|
107
|
+
actions = _extract_key_actions(older);
|
|
108
|
+
summary = (
|
|
109
|
+
f"[Context compacted: {non_important_count} earlier exchanges condensed. "
|
|
110
|
+
f"Key actions: {actions}]"
|
|
111
|
+
);
|
|
112
|
+
result.append({"role": "system", "content": summary});
|
|
113
|
+
}
|
|
114
|
+
result.extend(important);
|
|
115
|
+
result.extend(recent);
|
|
116
|
+
return result;
|
|
117
|
+
}
|