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.
Files changed (85) hide show
  1. jac_coder/__init__.jac +0 -0
  2. jac_coder/api.jac +82 -0
  3. jac_coder/cli_entry.py +25 -0
  4. jac_coder/config.jac +36 -0
  5. jac_coder/context.jac +17 -0
  6. jac_coder/data/examples/ai_agent.md +90 -0
  7. jac_coder/data/examples/blog_app.md +386 -0
  8. jac_coder/data/examples/core_patterns.md +321 -0
  9. jac_coder/data/examples/todo_app.md +321 -0
  10. jac_coder/data/reference/ai.md +131 -0
  11. jac_coder/data/reference/backend.md +215 -0
  12. jac_coder/data/reference/frontend.md +271 -0
  13. jac_coder/data/reference/osp.md +229 -0
  14. jac_coder/data/reference/pitfalls.md +141 -0
  15. jac_coder/data/reference/syntax.md +159 -0
  16. jac_coder/data/rules/core_jac.md +559 -0
  17. jac_coder/data/rules/fullstack.md +362 -0
  18. jac_coder/data/rules/workflow.md +88 -0
  19. jac_coder/events.jac +110 -0
  20. jac_coder/impl/api.impl.jac +399 -0
  21. jac_coder/impl/config.impl.jac +163 -0
  22. jac_coder/impl/context.impl.jac +117 -0
  23. jac_coder/impl/mcp_manager.impl.jac +380 -0
  24. jac_coder/impl/memory.impl.jac +247 -0
  25. jac_coder/impl/nodes.impl.jac +259 -0
  26. jac_coder/impl/permission.impl.jac +62 -0
  27. jac_coder/impl/walkers.impl.jac +298 -0
  28. jac_coder/mcp_manager.jac +35 -0
  29. jac_coder/memory.jac +15 -0
  30. jac_coder/nodes.jac +306 -0
  31. jac_coder/permission.jac +19 -0
  32. jac_coder/serve_entry.jac +30 -0
  33. jac_coder/server.jac +324 -0
  34. jac_coder/tool/__init__.jac +17 -0
  35. jac_coder/tool/checked.jac +10 -0
  36. jac_coder/tool/delegation.jac +23 -0
  37. jac_coder/tool/filesystem.jac +25 -0
  38. jac_coder/tool/git.jac +18 -0
  39. jac_coder/tool/guarded.jac +23 -0
  40. jac_coder/tool/impl/checked.impl.jac +38 -0
  41. jac_coder/tool/impl/delegation.impl.jac +157 -0
  42. jac_coder/tool/impl/filesystem.impl.jac +781 -0
  43. jac_coder/tool/impl/git.impl.jac +115 -0
  44. jac_coder/tool/impl/guarded.impl.jac +72 -0
  45. jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
  46. jac_coder/tool/impl/jac_docs.impl.jac +136 -0
  47. jac_coder/tool/impl/jac_tools.impl.jac +79 -0
  48. jac_coder/tool/impl/mcp.impl.jac +32 -0
  49. jac_coder/tool/impl/preview.impl.jac +233 -0
  50. jac_coder/tool/impl/question.impl.jac +29 -0
  51. jac_coder/tool/impl/scaffold.impl.jac +231 -0
  52. jac_coder/tool/impl/search.impl.jac +85 -0
  53. jac_coder/tool/impl/shell.impl.jac +89 -0
  54. jac_coder/tool/impl/task.impl.jac +12 -0
  55. jac_coder/tool/impl/think.impl.jac +4 -0
  56. jac_coder/tool/impl/todo.impl.jac +58 -0
  57. jac_coder/tool/impl/validate.impl.jac +236 -0
  58. jac_coder/tool/impl/web.impl.jac +91 -0
  59. jac_coder/tool/jac_analyzer.jac +21 -0
  60. jac_coder/tool/jac_docs.jac +9 -0
  61. jac_coder/tool/jac_tools.jac +11 -0
  62. jac_coder/tool/mcp.jac +17 -0
  63. jac_coder/tool/preview.jac +31 -0
  64. jac_coder/tool/question.jac +7 -0
  65. jac_coder/tool/scaffold.jac +10 -0
  66. jac_coder/tool/search.jac +14 -0
  67. jac_coder/tool/shell.jac +12 -0
  68. jac_coder/tool/task.jac +9 -0
  69. jac_coder/tool/think.jac +5 -0
  70. jac_coder/tool/todo.jac +12 -0
  71. jac_coder/tool/validate.jac +11 -0
  72. jac_coder/tool/vision.jac +17 -0
  73. jac_coder/tool/web.jac +10 -0
  74. jac_coder/util/__init__.jac +18 -0
  75. jac_coder/util/colors.jac +20 -0
  76. jac_coder/util/impl/sandbox.impl.jac +38 -0
  77. jac_coder/util/impl/tool_output.impl.jac +208 -0
  78. jac_coder/util/sandbox.jac +8 -0
  79. jac_coder/util/tool_output.jac +29 -0
  80. jac_coder/walkers.jac +67 -0
  81. jac_coder-0.1.0.dist-info/METADATA +9 -0
  82. jac_coder-0.1.0.dist-info/RECORD +85 -0
  83. jac_coder-0.1.0.dist-info/WHEEL +5 -0
  84. jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
  85. 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
+ }