jac-coder 0.2.1__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 (116) hide show
  1. {jac_coder-0.2.1 → jac_coder-0.2.2}/PKG-INFO +1 -1
  2. {jac_coder-0.2.1 → jac_coder-0.2.2}/README.md +2 -0
  3. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/api.impl.jac +59 -49
  4. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/core/nodes.jac +15 -1
  5. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/core/walkers.impl.jac +51 -33
  6. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/infra/mcp_manager.impl.jac +45 -19
  7. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/infra/mcp_manager.jac +7 -0
  8. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/cost_tracker.impl.jac +6 -0
  9. jac_coder-0.2.2/jac_coder/runtime/permission.impl.jac +171 -0
  10. jac_coder-0.2.2/jac_coder/runtime/permission.jac +55 -0
  11. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/prompt.jac +16 -10
  12. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/skills.impl.jac +28 -4
  13. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/server.jac +44 -11
  14. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-components/SKILL.md +1 -0
  15. jac_coder-0.2.2/jac_coder/skills/jac-cl-styling/SKILL.md +49 -0
  16. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-core-cheatsheet/SKILL.md +2 -1
  17. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-fullstack-patterns/SKILL.md +1 -1
  18. jac_coder-0.2.2/jac_coder/skills/jac-npm-packages/SKILL.md +94 -0
  19. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-scaffold/SKILL.md +5 -6
  20. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/run/guarded.impl.jac +32 -2
  21. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/run/guarded.jac +4 -1
  22. jac_coder-0.2.2/jac_coder/tool/run/shell.impl.jac +206 -0
  23. jac_coder-0.2.2/jac_coder/tool/run/shell.jac +23 -0
  24. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder.egg-info/PKG-INFO +1 -1
  25. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder.egg-info/SOURCES.txt +2 -0
  26. {jac_coder-0.2.1 → jac_coder-0.2.2}/pyproject.toml +1 -1
  27. jac_coder-0.2.1/jac_coder/runtime/permission.impl.jac +0 -62
  28. jac_coder-0.2.1/jac_coder/runtime/permission.jac +0 -19
  29. jac_coder-0.2.1/jac_coder/tool/run/shell.impl.jac +0 -152
  30. jac_coder-0.2.1/jac_coder/tool/run/shell.jac +0 -13
  31. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/__init__.jac +0 -0
  32. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/__init__.py +0 -0
  33. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/api.jac +0 -0
  34. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/cli_entry.py +0 -0
  35. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/core/__init__.jac +0 -0
  36. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/core/nodes.impl.jac +0 -0
  37. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/core/walkers.jac +0 -0
  38. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/infra/__init__.jac +0 -0
  39. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/infra/config.impl.jac +0 -0
  40. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/infra/config.jac +0 -0
  41. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/infra/kv.jac +0 -0
  42. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/lib/__init__.jac +0 -0
  43. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/lib/coder.impl.jac +0 -0
  44. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/lib/coder.jac +0 -0
  45. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/__init__.jac +0 -0
  46. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/context.impl.jac +0 -0
  47. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/context.jac +0 -0
  48. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/cost_tracker.jac +0 -0
  49. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/events.jac +0 -0
  50. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/memory.impl.jac +0 -0
  51. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/memory.jac +0 -0
  52. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/prompt.impl.jac +0 -0
  53. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/runtime/skills.jac +0 -0
  54. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/serve_entry.jac +0 -0
  55. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/ROADMAP.md +0 -0
  56. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-by-llm/SKILL.md +0 -0
  57. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-auth/SKILL.md +0 -0
  58. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-organization/SKILL.md +0 -0
  59. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-cl-routing/SKILL.md +0 -0
  60. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-has-fields/SKILL.md +0 -0
  61. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-impl-files/SKILL.md +0 -0
  62. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-node-edge-patterns/SKILL.md +0 -0
  63. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-sv-auth/SKILL.md +0 -0
  64. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-sv-endpoints/SKILL.md +0 -0
  65. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-sv-persistence/SKILL.md +0 -0
  66. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-types/SKILL.md +0 -0
  67. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/skills/jac-walker-patterns/SKILL.md +0 -0
  68. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/__init__.jac +0 -0
  69. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/git.impl.jac +0 -0
  70. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/git.jac +0 -0
  71. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/mcp.impl.jac +0 -0
  72. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/mcp.jac +0 -0
  73. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/__init__.jac +0 -0
  74. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/delegation.impl.jac +0 -0
  75. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/delegation.jac +0 -0
  76. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/question.impl.jac +0 -0
  77. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/question.jac +0 -0
  78. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/task.impl.jac +0 -0
  79. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/task.jac +0 -0
  80. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/think.impl.jac +0 -0
  81. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/think.jac +0 -0
  82. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/todo.impl.jac +0 -0
  83. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/todo.jac +0 -0
  84. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/validate.impl.jac +0 -0
  85. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/meta/validate.jac +0 -0
  86. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/net/__init__.jac +0 -0
  87. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/net/preview.impl.jac +0 -0
  88. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/net/preview.jac +0 -0
  89. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/net/web.impl.jac +0 -0
  90. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/net/web.jac +0 -0
  91. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/__init__.jac +0 -0
  92. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/filesystem.impl.jac +0 -0
  93. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/filesystem.jac +0 -0
  94. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
  95. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/jac_analyzer.jac +0 -0
  96. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/load_jac_skill.impl.jac +0 -0
  97. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/load_jac_skill.jac +0 -0
  98. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/search.impl.jac +0 -0
  99. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/read/search.jac +0 -0
  100. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/run/__init__.jac +0 -0
  101. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
  102. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/run/jac_tools.jac +0 -0
  103. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/write/__init__.jac +0 -0
  104. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/write/checked.impl.jac +0 -0
  105. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/tool/write/checked.jac +0 -0
  106. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/util/__init__.jac +0 -0
  107. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/util/colors.jac +0 -0
  108. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/util/sandbox.impl.jac +0 -0
  109. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/util/sandbox.jac +0 -0
  110. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/util/tool_output.impl.jac +0 -0
  111. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder/util/tool_output.jac +0 -0
  112. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder.egg-info/dependency_links.txt +0 -0
  113. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder.egg-info/entry_points.txt +0 -0
  114. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder.egg-info/requires.txt +0 -0
  115. {jac_coder-0.2.1 → jac_coder-0.2.2}/jac_coder.egg-info/top_level.txt +0 -0
  116. {jac_coder-0.2.1 → 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.1
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
@@ -156,3 +156,5 @@ jac-code/
156
156
  ├── vscode-jac-coder/ # VS Code extension (TypeScript)
157
157
  └── docs/ # ARCHITECTURE, ROADMAP, PROGRESS, LIBRARY_MODE
158
158
  ```
159
+
160
+ <img width="2661" height="1091" alt="image" src="https://github.com/user-attachments/assets/a314087f-ae94-4c8b-8899-1e4ff3eb465c" />
@@ -244,13 +244,6 @@ impl chat(
244
244
  # Memory initialization
245
245
  _ensure_memory(session, work_dir);
246
246
 
247
- # Add user message
248
- user_msg_record: dict = {"role": "user", "content": message};
249
- if images and len(images) > 0 {
250
- user_msg_record["has_images"] = True;
251
- user_msg_record["image_count"] = len(images);
252
- }
253
- session.chat_history.append(user_msg_record);
254
247
  session.updated_at = datetime.now().isoformat();
255
248
 
256
249
  # Get MainAgent — always resolve fresh (cached refs go stale in jac start)
@@ -260,15 +253,33 @@ impl chat(
260
253
  config = get_config();
261
254
  _init_spawn_budget(config.max_react_iterations * 3);
262
255
 
263
- # Build context
264
- ctx_history = build_context(
265
- session.chat_history,
266
- session.active_files,
267
- session.pending_errors,
268
- project_summary=session.project_summary or ""
269
- );
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
+ }
270
282
 
271
- # Inject mode-aware workflow modules (dynamic prompt assembly)
272
283
  import from jac_coder.runtime.prompt { select_prompt_modules as _select_modules }
273
284
  workflow_modules = _select_modules(
274
285
  last_mode_hint=session.last_mode_hint,
@@ -277,20 +288,28 @@ impl chat(
277
288
  chat_history=session.chat_history
278
289
  );
279
290
  if workflow_modules {
280
- ctx_history.insert(0, {"role": "system", "content": workflow_modules});
291
+ prefix_parts.append(workflow_modules);
281
292
  }
282
293
 
283
- # Inject agent_context if provided (e.g. from JacBuilder)
284
294
  if agent_context {
285
- ctx_history.insert(0, {"role": "system", "content": agent_context});
295
+ prefix_parts.append(agent_context);
286
296
  }
287
297
 
288
- # Inject available MCP tools into context (rebuilt every turn)
289
- mcp_msg = _get_mcp_context_msg();
290
- if mcp_msg {
291
- 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
+ );
292
305
  }
293
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
+
294
313
  # Early exit if already aborted before LLM starts
295
314
  if is_abort_requested() {
296
315
  tool_end();
@@ -306,24 +325,6 @@ impl chat(
306
325
  };
307
326
  }
308
327
 
309
- # Plan mode — inject instruction to write a plan only, no file writes.
310
- # The UI will show "Execute Plan" when done; execution reruns with edit_mode="auto".
311
- if edit_mode == "plan" {
312
- plan_msg: dict = {
313
- "role": "system",
314
- "content": (
315
- "You are in PLAN MODE. Your task is to produce a detailed, step-by-step plan "
316
- "for the user's request. Do NOT write, edit, or run any files. "
317
- "Describe every file you would create or modify, what changes you would make, "
318
- "and why. The user will review your plan and click 'Execute Plan' to apply it."
319
- )
320
- };
321
- ctx_with_plan: list[dict] = [plan_msg];
322
- ctx_with_plan.extend(ctx_history);
323
- ctx_history = ctx_with_plan;
324
- }
325
-
326
- # Call MainAgent — returns StreamEvent generator with logging enabled
327
328
  # If user attached images, convert to byllm Image object and pass as user_image
328
329
  user_image = None;
329
330
  if images and len(images) > 0 {
@@ -339,7 +340,10 @@ impl chat(
339
340
  sys.stderr.write(f"[api] Failed to create Image from user attachment: {img_err}\n");
340
341
  }
341
342
  }
342
- 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);
343
347
  stream_result = _consume_llm_stream(event_stream);
344
348
 
345
349
  response_text = stream_result["content"];
@@ -379,16 +383,22 @@ impl chat(
379
383
  }
380
384
  }
381
385
 
382
- # 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.
383
388
  if not is_abort_requested() {
384
- record: dict = {"role": "assistant", "content": response_text, "agent": "main"};
385
- if tools_used {
386
- record["tools_used"] = tools_used;
387
- }
388
- if files_modified {
389
- 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
+ }
390
401
  }
391
- session.chat_history.append(record);
392
402
  }
393
403
  session.last_agent = "main";
394
404
  session.updated_at = datetime.now().isoformat();
@@ -82,6 +82,16 @@ obj SessionLearnings {
82
82
  # ---------------------------------------------------------------------------
83
83
  node McpRegistry {
84
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]] = {};
85
95
  }
86
96
 
87
97
 
@@ -145,7 +155,10 @@ node Session {
145
155
  # MainAgent — the orchestrator node
146
156
  # ---------------------------------------------------------------------------
147
157
  node MainAgent {
148
- 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(
149
162
  tools=[
150
163
  # Think — explicit reasoning before acting or delegating
151
164
  think,
@@ -184,6 +197,7 @@ node MainAgent {
184
197
  # MCP — call tools from connected MCP servers
185
198
  mcp_call
186
199
  ],
200
+ conversation=self._conv,
187
201
  on_iteration=_iteration_hook,
188
202
  max_react_iterations=80,
189
203
  temperature=0.2,
@@ -173,8 +173,7 @@ impl interact.enter_session with Session entry {
173
173
  }
174
174
  }
175
175
 
176
- # Append user message and update
177
- here.chat_history.append({"role": "user", "content": self.message});
176
+ # byLLM appends the user message from the `respond(message=...)` arg.
178
177
  here.updated_at = datetime.now().isoformat();
179
178
  self.chat_history = here.chat_history;
180
179
 
@@ -212,28 +211,27 @@ impl _persist_response(
212
211
  files_modified: list[str] = [],
213
212
  tool_records: list[dict] = []
214
213
  ) -> None {
214
+ # byLLM already appended the user/tool/assistant turns via _conv binding;
215
+ # we only patch JacCoder UI metadata onto the trailing assistant entry.
215
216
  session = _find_session(session_id);
216
217
  if not session {
217
218
  return;
218
219
  }
219
220
 
220
- for rec in tool_records {
221
- session.chat_history.append({
222
- "role": "tool",
223
- "name": rec.get("tool", ""),
224
- "args": rec.get("args", {}),
225
- "content": rec.get("result", "")
226
- });
221
+ for idx in range(len(session.chat_history) - 1, -1, -1) {
222
+ message_entry = session.chat_history[idx];
223
+ if message_entry.get("role") == "assistant" and "agent" not in message_entry {
224
+ message_entry["agent"] = agent_mode;
225
+ if tools_used {
226
+ message_entry["tools_used"] = tools_used;
227
+ }
228
+ if files_modified {
229
+ message_entry["files_modified"] = files_modified;
230
+ }
231
+ break;
232
+ }
227
233
  }
228
234
 
229
- record: dict = {"role": "assistant", "content": response, "agent": agent_mode};
230
- if tools_used {
231
- record["tools_used"] = tools_used;
232
- }
233
- if files_modified {
234
- record["files_modified"] = files_modified;
235
- }
236
- session.chat_history.append(record);
237
235
  session.last_agent = agent_mode;
238
236
  session.updated_at = datetime.now().isoformat();
239
237
  }
@@ -314,45 +312,65 @@ impl _respond_and_persist(agent: MainAgent, ctx: interact) -> None {
314
312
  pending_errors = session.pending_errors if session else [];
315
313
  project_summary = session.project_summary if session else "";
316
314
 
317
- # Build compacted context
318
- ctx_history = build_context(
319
- ctx.chat_history, active_files, pending_errors, project_summary=project_summary
320
- );
315
+ # Per-turn context gets folded into the user message; we can't add
316
+ # role=system dicts to session.chat_history without them persisting stale.
317
+ prefix_parts: list[str] = [];
318
+
319
+ if project_summary {
320
+ is_progress_file = project_summary.lstrip().startswith("#");
321
+ label = (
322
+ "Project progress (.jaccoder/progress.md)"
323
+ if is_progress_file
324
+ else "Project context"
325
+ );
326
+ prefix_parts.append(
327
+ 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{project_summary}"
328
+ );
329
+ }
330
+ if active_files {
331
+ prefix_parts.append("Active files: " + ", ".join(active_files));
332
+ }
333
+ if pending_errors {
334
+ prefix_parts.append("Pending errors:\n" + "\n".join(pending_errors));
335
+ }
321
336
 
322
- # Inject the available-skills listing (names + descriptions only).
323
- # The LLM picks which skills it needs and pulls bodies via load_jac_skill(name).
324
- # Inserted FIRST so workflow_modules ends up above it in the final prompt,
325
- # keeping the listing closer to the user message where attention is strongest.
326
337
  import from jac_coder.runtime.skills { inject_skills_listing }
327
338
  skill_listing = inject_skills_listing();
328
339
  if skill_listing {
329
- ctx_history.insert(0, {"role": "system", "content": skill_listing});
340
+ prefix_parts.append(skill_listing);
330
341
  }
331
342
 
332
- # Inject mode-aware workflow modules (dynamic prompt assembly)
333
343
  import from jac_coder.runtime.prompt { select_prompt_modules }
334
344
  workflow_modules = select_prompt_modules(
335
345
  last_mode_hint=session.last_mode_hint if session else "",
336
346
  message=ctx.message,
337
347
  directory=session.directory if session else "",
338
- chat_history=ctx.chat_history
348
+ chat_history=session.chat_history if session else []
339
349
  );
340
350
  if workflow_modules {
341
- ctx_history.insert(0, {"role": "system", "content": workflow_modules});
351
+ prefix_parts.append(workflow_modules);
342
352
  }
343
353
 
344
- # If agent_context was provided (e.g. from JacBuilder), inject it
345
354
  if ctx.agent_context {
346
- ctx_history.insert(0, {"role": "system", "content": ctx.agent_context});
355
+ prefix_parts.append(ctx.agent_context);
347
356
  }
348
357
 
358
+ composed_message = (
359
+ "\n\n---\n\n".join(prefix_parts) + "\n\n---\n\n" + ctx.message
360
+ if prefix_parts
361
+ else ctx.message
362
+ );
363
+
349
364
  # Initialize shared budget for SubAgents
350
365
  import from jac_coder.tool.meta.delegation { init_budget }
351
366
  config = get_config();
352
367
  init_budget(config.max_react_iterations * 3);
353
368
 
354
- # Call MainAgent — returns StreamEvent generator with logging enabled
355
- event_stream = agent.respond(message=ctx.message, chat_history=ctx_history);
369
+ if session {
370
+ agent._conv = session.chat_history;
371
+ }
372
+
373
+ event_stream = agent.respond(message=composed_message);
356
374
  stream_result = _consume_llm_stream(event_stream);
357
375
 
358
376
  response_text = stream_result["content"];
@@ -30,12 +30,20 @@ glob _sessions: dict = {}; # name -> ClientSession (open inside _mgr_loop)
30
30
  glob _exit_stacks: dict = {}; # name -> AsyncExitStack (keeps transports alive)
31
31
  glob _TOOLS_CACHE_KEY: str = "jc:mcp:tools";
32
32
  glob _TOOLS_CACHE_TTL: int = 30; # seconds before tool list is re-fetched
33
- glob _builtin_names: set = set(); # names registered via mcp_register_builtin
34
- glob _disconnected_names: set = set(); # names intentionally disconnected by user
33
+
34
+ impl _service_mode_active() -> bool {
35
+ return bool(
36
+ os.environ.get("JACCODER_WEB_MODE", "")
37
+ or os.environ.get("JACCODER_IDE_MODE", "")
38
+ );
39
+ }
35
40
 
36
41
 
37
42
  """Background thread: check PyPI for latest jac-mcp version (3s timeout, fails silently)."""
38
- def _check_jac_mcp_version() -> None {
43
+ impl _check_jac_mcp_version() -> None {
44
+ if _service_mode_active() {
45
+ return;
46
+ }
39
47
  try {
40
48
  current: str = version("jac-mcp");
41
49
  req = Request(
@@ -286,31 +294,33 @@ impl mcp_add_server(name: str, config: dict) -> dict {
286
294
  }
287
295
 
288
296
 
289
- """Close the connection and remove the server from the registry."""
297
+ """Close the connection and mark the server disconnected in the graph."""
290
298
  impl mcp_disconnect_server(name: str) -> dict {
291
299
  # Disconnect only — close the active connection but keep the config.
292
300
  # This applies to ALL servers (builtin and user-added) so they stay
293
301
  # visible in the list and can be reconnected.
294
- global _disconnected_names;
302
+ # Intent is persisted in McpRegistry.disconnected_servers so it survives
303
+ # pod restarts and is visible across all pods.
295
304
  configs = _load_configs();
296
305
  if name not in configs {
297
306
  return {"error": f"Server '{name}' not found"};
298
307
  }
299
- _disconnected_names.add(name);
308
+ reg = _get_registry();
309
+ if name not in reg.disconnected_servers {
310
+ reg.disconnected_servers = list(reg.disconnected_servers) + [name];
311
+ }
300
312
  _submit(_close_connection(name));
301
313
  kv_delete(_TOOLS_CACHE_KEY);
302
- return {"status": "disconnected", "name": name};
314
+ return {"status": "disconnected", "name": name};
303
315
  }
304
316
 
305
317
 
306
318
  """Reconnect a previously-disconnected MCP server."""
307
319
  impl mcp_reconnect_server(name: str) -> dict {
308
- global _disconnected_names;
309
320
  configs = _load_configs();
310
321
  if name not in configs {
311
322
  return {"error": f"Server '{name}' not found"};
312
323
  }
313
- _disconnected_names.discard(name);
314
324
  # Close any stale connection first, then reconnect via list_tools.
315
325
  # Iterate configs to get a typed config value (subscript on untyped dict
316
326
  # produces Unknown in the Jac type checker — the for pattern is used
@@ -321,9 +331,14 @@ impl mcp_reconnect_server(name: str) -> dict {
321
331
  if n == name {
322
332
  try {
323
333
  tools = _submit(_async_list_tools(n, config));
334
+ # Only remove from disconnected_servers on success — keeps state
335
+ # consistent if connection fails (server stays disconnected).
336
+ reg = _get_registry();
337
+ if name in reg.disconnected_servers {
338
+ reg.disconnected_servers = [s for s in reg.disconnected_servers if s != name];
339
+ }
324
340
  return {"status": "connected", "name": name};
325
341
  } except Exception as e {
326
- _disconnected_names.add(name);
327
342
  return {"error": f"Failed to reconnect '{name}': {e}"};
328
343
  }
329
344
  }
@@ -334,15 +349,18 @@ impl mcp_reconnect_server(name: str) -> dict {
334
349
 
335
350
  impl mcp_delete_server(name: str) -> dict {
336
351
  # Permanently remove a user-added server from the registry.
337
- # Built-in servers cannot be deleted.
338
- if name in _builtin_names {
339
- return {"error": f"Built-in server '{name}' cannot be deleted"};
340
- }
352
+ # Built-in servers cannot be deleted — the builtin flag is stored as
353
+ # config["builtin"]=True inside McpRegistry.servers (graph-persisted),
341
354
  configs = _load_configs();
342
355
  if name not in configs {
343
356
  return {"error": f"Server '{name}' not found"};
344
357
  }
345
- _submit(_close_connection(name));
358
+ for (n, cfg) in configs.items() {
359
+ if n == name and cfg.get("builtin", False) {
360
+ return {"error": f"Built-in server '{name}' cannot be deleted"};
361
+ }
362
+ }
363
+ _submit(_close_connection(name));
346
364
  configs.pop(name);
347
365
  _save_configs(configs);
348
366
  kv_delete(_TOOLS_CACHE_KEY);
@@ -353,6 +371,8 @@ impl mcp_delete_server(name: str) -> dict {
353
371
  """Return all registered servers with their connection status and tool counts."""
354
372
  impl mcp_list_servers() -> list {
355
373
  configs = _load_configs();
374
+ # Read disconnected intent from the graph — persisted, cross-pod consistent.
375
+ disconnected = _get_registry().disconnected_servers;
356
376
  result: list = [];
357
377
  for (name, config) in configs.items() {
358
378
  entry: dict = {
@@ -363,7 +383,7 @@ impl mcp_list_servers() -> list {
363
383
  "latest_version": _jac_mcp_update_info.get("latest", "") if name == "jac-mcp" else ""
364
384
  };
365
385
  # Skip auto-connect for servers the user intentionally disconnected
366
- if name in _disconnected_names {
386
+ if name in disconnected {
367
387
  entry["status"] = "disconnected";
368
388
  entry["tool_count"] = 0;
369
389
  entry["tools"] = [];
@@ -388,16 +408,16 @@ impl mcp_list_servers() -> list {
388
408
 
389
409
  """Save a built-in server to the registry without a connectivity check."""
390
410
  impl mcp_register_builtin(name: str, config: dict) -> None {
391
- global _builtin_names;
392
- _builtin_names.add(name);
393
411
  configs = _load_configs();
394
412
  # Always update builtin config — the binary path can change between installs
395
413
  # (e.g. pipx venv vs workspace venv). Stale paths cause connection errors.
414
+ # builtin=True is stored in the config dict inside McpRegistry.servers so
415
+ # it is graph-persisted and cross-pod consistent — no RAM set needed.
396
416
  config["builtin"] = True;
397
417
  configs[name] = config;
398
418
  _save_configs(configs);
399
419
  kv_delete(_TOOLS_CACHE_KEY); # Invalidate cross-pod cache — server set changed
400
- if name == "jac-mcp" {
420
+ if name == "jac-mcp" and not _service_mode_active() {
401
421
  threading.Thread(target=_check_jac_mcp_version, daemon=True).start();
402
422
  }
403
423
  }
@@ -411,8 +431,14 @@ impl mcp_get_tools() -> list {
411
431
  }
412
432
 
413
433
  configs = _load_configs();
434
+ # Read disconnected intent from the graph so tools from user-disconnected
435
+ # servers are never surfaced to the agent (bug fix: previously ignored).
436
+ disconnected = _get_registry().disconnected_servers;
414
437
  all_tools: list = [];
415
438
  for (name, config) in configs.items() {
439
+ if name in disconnected {
440
+ continue; # Respect user's disconnect intent — skip this server
441
+ }
416
442
  try {
417
443
  all_tools.extend(_submit(_async_list_tools(name, config)));
418
444
  } except Exception as e {
@@ -43,3 +43,10 @@ def mcp_call_tool(server_name: str, tool_name: str, arguments: dict) -> str;
43
43
  Saves now and tries it on first real tool call.
44
44
  """
45
45
  def mcp_register_builtin(name: str, config: dict) -> None;
46
+
47
+ """Return True when this process is serving multiple end-users (web/IDE).
48
+ Used to gate per-process best-effort tasks that only make sense in CLI."""
49
+ def _service_mode_active() -> bool;
50
+
51
+ """Background thread: check PyPI for latest jac-mcp version. No-op in service mode."""
52
+ def _check_jac_mcp_version() -> None;
@@ -206,6 +206,12 @@ impl reset() -> None {
206
206
 
207
207
 
208
208
  impl save_to_file(filepath: str = "") -> str {
209
+ # In web/IDE mode many users share the same server, so writing one user's
210
+ # cost to a local file would overwrite another user's data. Skip silently.
211
+ # In CLI mode the file is safe and useful.
212
+ if os.environ.get("JACCODER_WEB_MODE", "") or os.environ.get("JACCODER_IDE_MODE", "") {
213
+ return "";
214
+ }
209
215
  if not filepath {
210
216
  filepath = "jaccoder_cost.json";
211
217
  }