jac-coder 0.2.5__tar.gz → 0.2.6__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 (101) hide show
  1. {jac_coder-0.2.5 → jac_coder-0.2.6}/PKG-INFO +1 -1
  2. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/api.impl.jac +14 -1
  3. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/api.jac +2 -1
  4. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/cli.jac +56 -8
  5. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/core/nodes.impl.jac +1 -1
  6. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/config.jac +33 -0
  7. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/cost_tracker.impl.jac +41 -8
  8. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/cost_tracker.jac +11 -3
  9. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/file_logger.jac +1 -1
  10. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/prompt.jac +20 -18
  11. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/server.jac +24 -3
  12. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/clarify_intent.impl.jac +1 -1
  13. jac_coder-0.2.6/jac_coder/tool/net/preview.impl.jac +597 -0
  14. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/net/preview.jac +16 -11
  15. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/guarded.impl.jac +37 -3
  16. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/guarded.jac +7 -1
  17. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/write/checked.impl.jac +31 -1
  18. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/write/checked.jac +2 -2
  19. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder.egg-info/PKG-INFO +1 -1
  20. {jac_coder-0.2.5 → jac_coder-0.2.6}/pyproject.toml +1 -1
  21. jac_coder-0.2.5/jac_coder/tool/net/preview.impl.jac +0 -626
  22. {jac_coder-0.2.5 → jac_coder-0.2.6}/README.md +0 -0
  23. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/__init__.jac +0 -0
  24. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/__init__.py +0 -0
  25. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/cli.impl.jac +0 -0
  26. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/cli_entry.py +0 -0
  27. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/core/__init__.jac +0 -0
  28. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/core/nodes.jac +0 -0
  29. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/core/walkers.impl.jac +0 -0
  30. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/core/walkers.jac +0 -0
  31. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/__init__.jac +0 -0
  32. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/config.impl.jac +0 -0
  33. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/events.jac +0 -0
  34. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/kv.jac +0 -0
  35. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/mcp_manager.impl.jac +0 -0
  36. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/infra/mcp_manager.jac +0 -0
  37. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/lib/__init__.jac +0 -0
  38. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/lib/coder.impl.jac +0 -0
  39. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/lib/coder.jac +0 -0
  40. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/__init__.jac +0 -0
  41. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/compaction.jac +0 -0
  42. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/context.impl.jac +0 -0
  43. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/context.jac +0 -0
  44. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/events.jac +0 -0
  45. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/memory.impl.jac +0 -0
  46. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/memory.jac +0 -0
  47. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/permission.impl.jac +0 -0
  48. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/permission.jac +0 -0
  49. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/prompt.impl.jac +0 -0
  50. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/skills.impl.jac +0 -0
  51. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/runtime/skills.jac +0 -0
  52. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/serve_entry.jac +0 -0
  53. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/__init__.jac +0 -0
  54. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/git.impl.jac +0 -0
  55. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/git.jac +0 -0
  56. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/mcp.impl.jac +0 -0
  57. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/mcp.jac +0 -0
  58. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/__init__.jac +0 -0
  59. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/clarify_intent.jac +0 -0
  60. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/delegation.impl.jac +0 -0
  61. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/delegation.jac +0 -0
  62. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/task.impl.jac +0 -0
  63. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/task.jac +0 -0
  64. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/think.impl.jac +0 -0
  65. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/think.jac +0 -0
  66. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/todo.impl.jac +0 -0
  67. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/todo.jac +0 -0
  68. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/validate.impl.jac +0 -0
  69. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/meta/validate.jac +0 -0
  70. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/net/__init__.jac +0 -0
  71. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/net/web.impl.jac +0 -0
  72. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/net/web.jac +0 -0
  73. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/__init__.jac +0 -0
  74. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/filesystem.impl.jac +0 -0
  75. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/filesystem.jac +0 -0
  76. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
  77. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/jac_analyzer.jac +0 -0
  78. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/load_jac_skill.impl.jac +0 -0
  79. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/load_jac_skill.jac +0 -0
  80. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/search.impl.jac +0 -0
  81. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/read/search.jac +0 -0
  82. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/__init__.jac +0 -0
  83. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
  84. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/jac_tools.jac +0 -0
  85. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/shell.impl.jac +0 -0
  86. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/run/shell.jac +0 -0
  87. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/tool/write/__init__.jac +0 -0
  88. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/__init__.jac +0 -0
  89. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/banner.jac +0 -0
  90. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/colors.jac +0 -0
  91. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/md_render.jac +0 -0
  92. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/sandbox.impl.jac +0 -0
  93. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/sandbox.jac +0 -0
  94. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/tool_output.impl.jac +0 -0
  95. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder/util/tool_output.jac +0 -0
  96. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder.egg-info/SOURCES.txt +0 -0
  97. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder.egg-info/dependency_links.txt +0 -0
  98. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder.egg-info/entry_points.txt +0 -0
  99. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder.egg-info/requires.txt +0 -0
  100. {jac_coder-0.2.5 → jac_coder-0.2.6}/jac_coder.egg-info/top_level.txt +0 -0
  101. {jac_coder-0.2.5 → jac_coder-0.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-coder
3
- Version: 0.2.5
3
+ Version: 0.2.6
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
@@ -624,14 +624,27 @@ impl clarification_response(
624
624
  if not session_id {
625
625
  return {"ok": False, "error": "session_id required"};
626
626
  }
627
- _clarification_decisions[session_id] = {
627
+ decision = {
628
628
  "request_id": request_id,
629
629
  "selected_id": selected_id,
630
630
  "custom_text": custom_text,
631
631
  "skipped": skipped
632
632
  };
633
+ # Cross-pod delivery: write to Redis so the waiting pod finds it on its next 250ms poll.
634
+ # Key scoped by request_id — a late response for a timed-out question can't
635
+ # pollute the next clarification on the same session.
636
+ # TTL 130s covers the 120s gate timeout + buffer; waiting pod deletes key on pick-up.
637
+ if os.environ.get("JACCODER_WEB_MODE", "") {
638
+ import from jac_coder.infra.kv { get_kv }
639
+ try {
640
+ get_kv().set_with_ttl(_CLRFY_KEY_PREFIX + session_id + ":" + request_id, decision, 130);
641
+ } except Exception { ; }
642
+ }
643
+ # Same-pod delivery: only store locally if the waiter gate exists on this pod.
644
+ # Avoids a permanent dict entry on cross-pod responder pods (no one ever pops it there).
633
645
  gate = _clarification_events.get(session_id);
634
646
  if gate {
647
+ _clarification_decisions[session_id] = decision;
635
648
  gate.set();
636
649
  }
637
650
  return {"ok": True};
@@ -43,7 +43,8 @@ import from jac_coder.tool.run.guarded {
43
43
  _clear_chat_session,
44
44
  _chat_contexts,
45
45
  _clarification_events,
46
- _clarification_decisions
46
+ _clarification_decisions,
47
+ _CLRFY_KEY_PREFIX
47
48
  }
48
49
  import from jac_coder.runtime.events {
49
50
  register_event_callback,
@@ -32,7 +32,13 @@ import from jac_coder.util.colors {
32
32
  }
33
33
 
34
34
 
35
- import from jac_coder.runtime.cost_tracker { record_cost, is_cost_tracking_enabled, reset_request_cost }
35
+ import from jac_coder.runtime.cost_tracker {
36
+ record_cost,
37
+ is_cost_tracking_enabled,
38
+ reset_request_cost,
39
+ mark_call_started as _ct_mark_call_started,
40
+ mark_call_finished as _ct_mark_call_finished
41
+ }
36
42
 
37
43
  # Session-wide accumulated cost totals for the CLI /cost command.
38
44
  # Accumulates across all turns in a single CLI session; reset by /cost reset.
@@ -87,6 +93,21 @@ with entry {
87
93
  import from litellm.integrations.custom_logger { CustomLogger as _CL }
88
94
 
89
95
  class _CostLogger(_CL) {
96
+ def log_pre_api_call(
97
+ self: _CostLogger,
98
+ model: object,
99
+ messages: object,
100
+ kwargs: dict
101
+ ) {
102
+ if is_cost_tracking_enabled() {
103
+ try {
104
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
105
+ _ct_mark_call_started(_cid);
106
+ } except Exception {
107
+ _ = 0;
108
+ }
109
+ }
110
+ }
90
111
  def log_success_event(
91
112
  self: _CostLogger,
92
113
  kwargs: dict,
@@ -94,16 +115,41 @@ with entry {
94
115
  start_time: object,
95
116
  end_time: object
96
117
  ) {
97
- if is_cost_tracking_enabled() {
98
- try {
118
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
119
+ try {
120
+ if is_cost_tracking_enabled() {
99
121
  slp = kwargs.get("standard_logging_object") or {};
100
122
  cost = float(slp.get("response_cost") or 0.0);
101
123
  if cost > 0 {
102
- record_cost(cost);
124
+ record_cost(cost, _cid);
125
+ }
126
+ }
127
+ } except Exception {
128
+ _ = 0;
129
+ } finally {
130
+ try { _ct_mark_call_finished(_cid); } except Exception { ; }
131
+ }
132
+ }
133
+ async def async_log_success_event(
134
+ self: _CostLogger,
135
+ kwargs: dict,
136
+ response_obj: object,
137
+ start_time: object,
138
+ end_time: object
139
+ ) {
140
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
141
+ try {
142
+ if is_cost_tracking_enabled() {
143
+ slp = kwargs.get("standard_logging_object") or {};
144
+ cost = float(slp.get("response_cost") or 0.0);
145
+ if cost > 0 {
146
+ record_cost(cost, _cid);
103
147
  }
104
- } except Exception {
105
- _ = 0;
106
148
  }
149
+ } except Exception {
150
+ _ = 0;
151
+ } finally {
152
+ try { _ct_mark_call_finished(_cid); } except Exception { ; }
107
153
  }
108
154
  }
109
155
 
@@ -114,11 +160,13 @@ with entry {
114
160
  start_time: object,
115
161
  end_time: object
116
162
  ) {
117
- _ = 0;
163
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
164
+ try { _ct_mark_call_finished(_cid); } except Exception { ; }
118
165
  }
119
166
  }
120
167
 
121
- litellm.callbacks.append(_CostLogger());
168
+ _cli_cost_logger = _CostLogger();
169
+ litellm.callbacks.append(_cli_cost_logger);
122
170
  } except Exception {
123
171
  _ = 0;
124
172
  }
@@ -152,7 +152,7 @@ Build responsive, mobile-first UIs: fluid flex/grid layouts, Tailwind responsive
152
152
  ## Workflow
153
153
  1. Read .jaccoder/progress.md if it exists, update it as you go.
154
154
  2. analyze_project(dir) for existing projects.
155
- 3. Build bottom-up: services → hooks → components layout.
155
+ 3. Minimal main.jac rendering first, then leaf-first: services → hooks → components, wiring each into the layout as it's built. Never import a file you haven't created yet.
156
156
  4. **If `components/ui/` exists AND jac.toml has `[jac-shadcn]` (jac-shadcn project):**
157
157
  - Load `jac-shadcn-components` skill BEFORE writing any UI.
158
158
  - Load `jac-cl-styling` for conditional class and cn() patterns.
@@ -1,6 +1,7 @@
1
1
  import os;
2
2
  import json;
3
3
  import threading;
4
+ import from contextlib { suppress }
4
5
  import from pathlib { Path }
5
6
  import from dotenv { load_dotenv }
6
7
  import from byllm.lib { Model }
@@ -47,6 +48,33 @@ glob llm = SessionAwareModel(
47
48
  call_params={"max_tool_result_length": 20000}
48
49
  );
49
50
 
51
+ """Warn (once per model name) when the active model lacks vision support."""
52
+ glob _vision_warned_models: set[str] = `set();
53
+
54
+ def _warn_if_no_vision(model_name_to_check: str) -> None {
55
+ if not model_name_to_check or model_name_to_check in _vision_warned_models {
56
+ return;
57
+ }
58
+ _vision_warned_models.add(model_name_to_check);
59
+ with suppress(Exception) {
60
+ import litellm;
61
+ import sys;
62
+ # Only warn for models litellm knows about — unknown models are
63
+ # trusted (supports_vision returns False for unknowns too).
64
+ known = model_name_to_check in litellm.model_cost;
65
+ if known and not litellm.supports_vision(model_name_to_check) {
66
+ sys.stderr.write(
67
+ f"[jac-coder] Note: model '{model_name_to_check}' does not support vision. "
68
+ "Browser tools will return text only — the agent cannot verify "
69
+ "visual outcomes (theme colors, layout, rendering). For full "
70
+ "visual verification, use a vision-capable model "
71
+ "(e.g. gpt-5.x, claude-sonnet-4.x, gemini-2.x).\n"
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+
50
78
  """Set a thread-local model override. Fully concurrent — no locks, no global mutation."""
51
79
  def set_session_llm(model_name_override: str, api_key_override: str = "") {
52
80
  import logging;
@@ -55,6 +83,7 @@ def set_session_llm(model_name_override: str, api_key_override: str = "") {
55
83
  logging.getLogger("jac_coder.infra.config").info(
56
84
  f"Session LLM set: thread={threading.current_thread().name} model={model_name_override} has_key={bool(api_key_override)}"
57
85
  );
86
+ _warn_if_no_vision(model_name_override);
58
87
  }
59
88
 
60
89
  """Clear the thread-local model override."""
@@ -92,4 +121,8 @@ with entry {
92
121
  llm.call_params = {"max_tool_result_length": 3000};
93
122
  }
94
123
  llm.postinit();
124
+ # Surface a vision-capability warning at startup if the default
125
+ # model can't handle the image content blocks that browser tools
126
+ # now return. Session-level overrides re-check via set_session_llm.
127
+ _warn_if_no_vision(model_name);
95
128
  }
@@ -43,14 +43,20 @@ impl record_usage(usage_data: dict, agent: str = "main") -> None {
43
43
  }
44
44
 
45
45
 
46
- impl record_cost(cost: float) -> None {
47
- # Accumulate USD cost from a litellm CustomLogger callback into the
48
- # per-request ContextVar state. The ContextVar propagates across threads
49
- # via litellm's `executor.submit(ctx.run, success_handler)`.
46
+ impl record_cost(cost: float, call_id: str = "") -> None {
50
47
  if not _enabled {
51
48
  return;
52
49
  }
53
- state = _request_state_cv.get();
50
+ state = None;
51
+ if call_id {
52
+ with _active_calls_lock {
53
+ _entry = _active_calls.get(call_id);
54
+ state = _entry[1] if _entry is not None else None;
55
+ }
56
+ }
57
+ if state is None {
58
+ state = _request_state_cv.get();
59
+ }
54
60
  if state is not None {
55
61
  with _lock {
56
62
  state["cost"] = float(state.get("cost", 0.0)) + float(cost);
@@ -93,7 +99,7 @@ impl reset_request_cost() -> None {
93
99
  }
94
100
 
95
101
 
96
- impl mark_call_started() -> None {
102
+ impl mark_call_started(call_id: str = "") -> None {
97
103
  state = _request_state_cv.get();
98
104
  if state is None {
99
105
  return;
@@ -107,11 +113,38 @@ impl mark_call_started() -> None {
107
113
  ev.clear();
108
114
  }
109
115
  }
116
+ if call_id {
117
+ with _active_calls_lock {
118
+ # Prune stale entries when map is at capacity to bound memory.
119
+ if len(_active_calls) >= _ACTIVE_CALLS_MAX {
120
+ _now = time.monotonic();
121
+ _stale = [];
122
+ for (_k, _ent) in _active_calls.items() {
123
+ if _now - _ent[0] > _ACTIVE_CALLS_TTL_SECONDS {
124
+ _stale.append(_k);
125
+ }
126
+ }
127
+ for _k in _stale {
128
+ _active_calls.pop(_k, None);
129
+ }
130
+ }
131
+ _active_calls[call_id] = (time.monotonic(), state);
132
+ }
133
+ }
110
134
  }
111
135
 
112
136
 
113
- impl mark_call_finished() -> None {
114
- state = _request_state_cv.get();
137
+ impl mark_call_finished(call_id: str = "") -> None {
138
+ state = None;
139
+ if call_id {
140
+ with _active_calls_lock {
141
+ _entry = _active_calls.pop(call_id, None);
142
+ state = _entry[1] if _entry is not None else None;
143
+ }
144
+ }
145
+ if state is None {
146
+ state = _request_state_cv.get();
147
+ }
115
148
  if state is None {
116
149
  return;
117
150
  }
@@ -7,6 +7,7 @@ import os;
7
7
  import json;
8
8
  import threading;
9
9
  import contextvars;
10
+ import time;
10
11
 
11
12
  glob _enabled: bool = os.environ.get("JACCODER_TRACK_COST", "") in ("1", "true", "yes");
12
13
  glob _lock = threading.Lock();
@@ -19,6 +20,13 @@ glob _request_state_cv: contextvars.ContextVar = contextvars.ContextVar(
19
20
  "jaccoder_request_state", default=None
20
21
  );
21
22
 
23
+ # Each entry is (insert_timestamp: float, state: dict) so stale entries
24
+ # can be reaped when the map approaches the size cap.
25
+ glob _active_calls: dict = {};
26
+ glob _active_calls_lock = threading.Lock();
27
+ glob _ACTIVE_CALLS_MAX: int = 2048;
28
+ glob _ACTIVE_CALLS_TTL_SECONDS: float = 300.0;
29
+
22
30
 
23
31
  def is_cost_tracking_enabled() -> bool {
24
32
  return _enabled;
@@ -35,11 +43,11 @@ def disable_cost_tracking() -> None {
35
43
  }
36
44
 
37
45
  def record_usage(usage_data: dict, agent: str = "main") -> None;
38
- def record_cost(cost: float) -> None;
46
+ def record_cost(cost: float, call_id: str = "") -> None;
39
47
  def get_request_cost() -> float;
40
48
  def reset_request_cost() -> None;
41
- def mark_call_started() -> None;
42
- def mark_call_finished() -> None;
49
+ def mark_call_started(call_id: str = "") -> None;
50
+ def mark_call_finished(call_id: str = "") -> None;
43
51
  def wait_for_pending_costs(timeout: float = 2.0) -> bool;
44
52
  def save_to_file(filepath: str = "") -> str;
45
53
  def get_per_call_data() -> list;
@@ -198,7 +198,7 @@ def file_log_event(session_id: str, event_type: str, data: dict) -> None {
198
198
  return;
199
199
  }
200
200
 
201
- _log_file_handle.write(json.dumps(record) + "\n");
201
+ _log_file_handle.write(json.dumps(record, default=vars) + "\n");
202
202
 
203
203
  } except Exception as e {
204
204
  sys.stderr.write(f"[JacCoder] log write error: {e}\n");
@@ -46,26 +46,26 @@ Use absolute paths. Load the `jac-scaffold` skill for the full template list and
46
46
 
47
47
  ## Iteration Budget
48
48
  You have limited iterations. Prioritize a WORKING app over a perfect one:
49
- - Build breadth-first: get ALL files written and the app running before polishing any single file.
49
+ - Build breadth-first: get a working end-to-end skeleton rendering before polishing any single file.
50
50
  - If a component has errors after 2 fix attempts, write a minimal working version and move on.
51
51
  - Target: app starts and renders by iteration ~50. Remaining iterations for testing and fixes.
52
52
  - Never spend 10+ iterations debugging one file while the rest of the app is unbuilt.
53
53
 
54
54
  ## Build Workflow
55
55
  0. **Inspect the workspace** (per Step 0 above) — decide EXTEND vs CREATE before anything else.
56
- 1. Load the relevant skills from `<jac-skills-available>` (fullstack + anything else the task touches) via `load_jac_skill(name)`.
56
+ 1. Load skills from `<jac-skills-available>` via `load_jac_skill(name)`: **always start with `jac-core-cheatsheet`**, then load the task-specific skills (fullstack + anything the task touches).
57
57
  2. Create or update `.jaccoder/progress.md` with the plan.
58
- 3. Backend services (.sv.jac) + main.jac.
58
+ 3. Backend services (.sv.jac), then a minimal main.jac that renders a real layout (not a stub) — your live-preview shell; it grows as you wire in components.
59
59
  4. Tailwind v4 — jac.toml MUST have ALL four of these (missing any one breaks the build silently):
60
60
  [dependencies.npm.dev]: `tailwindcss = "latest"` and `"@tailwindcss/vite" = "latest"`
61
61
  [plugins.client.vite]: `plugins = ["tailwindcss()"]` AND `lib_imports = ["import tailwindcss from '@tailwindcss/vite'"]`
62
62
  5. CSS entry point `styles/global.css`: first line `@import "tailwindcss";`
63
63
  Import in main.jac using `cl import`: `cl import ".styles.global.css";`
64
64
  (Use `cl import`, NOT plain `import` — plain import causes E5001 if placed before `to cl:`)
65
- 6. Frontend hooks + components (.cl.jac) with Tailwind.
65
+ 6. Frontend (.cl.jac) with Tailwind: build LEAF-FIRST — write each hook/component fully, THEN wire it into its parent. Never import a file that doesn't exist yet — it flashes "undefined component" in the live preview.
66
66
  7. Run `jac install`— after all jac.toml changes are final.
67
67
  8. `run_command("jac start --dev main.jac", background=True)`.
68
- 9. browser_validate(url) → PASS/FAIL. Follow FAIL action instructions.
68
+ 9. browser_validate(url) → PASS/FAIL. Follow FAIL action instructions. Validate at milestones, not after every component.
69
69
  10. If PASS → one interactive test (browser_do + browser_state), then browser_close.
70
70
  11. Update progress.md Status: DONE.
71
71
 
@@ -88,19 +88,21 @@ Unstyled OR non-responsive app = bug.""";
88
88
 
89
89
 
90
90
  glob SEM_BROWSER_TESTING: str = """## Browser Testing
91
- Fix problems yourselfnever stop to report them. Follow browser_validate's escalating ACTION instructions exactly:
92
- - 1st FAIL: read source, fix. 2nd: warning. 3rd+: mandatory bisect (skeleton → add components back).
93
- - Auth-gated blank page: tool auto-detects, follow its instructions.
94
-
95
- ## After PASS
96
- PASS = page renders, NOT that it works. You must:
97
- 1. Screenshot to confirm UI looks correct.
98
- 2. Check the layout at a narrow / mobile width no horizontal scroll, overlapping elements, or cut-off content. Fix responsive issues before declaring done.
99
- 3. Test the primary feature (one action + verify state changed via screenshot).
100
- 4. If action failscode bug. Fix, restart, retest. Never retry same action twice.
101
- 5. After 2 failed interactive tests → close browser, report status, move on.
102
-
103
- SKELETON/PLACEHOLDER PASS is not done restore all components.""";
91
+ Browser tools return observations snapshot, annotated screenshot, JS errors, console errors, optional server-log diagnosis. They do not judge PASS/FAIL. You decide by looking at the screenshot.
92
+
93
+ ## Visual verification is the contract
94
+ After ANY edit that affects rendering (CSS, classNames, layout, component structure), the very next validate MUST confirm your change is visible in the screenshot. A successful file write means nothing if the pixels don't reflect it. Never scrape CSS or curl the page — the screenshot is ground truth.
95
+
96
+ ## When the screenshot shows something wrong
97
+ - Blank or near-blank page likely auth-gated or routing issue; try the app's known entry path before declaring failure.
98
+ - Placeholder / unfinished UI not done; restore the real content.
99
+ - JS or console error names a file read it, fix it, re-validate.
100
+ - 5xx with server-log diagnosis the diagnosis names the compile error or missing dep; act on it.
101
+
102
+ ## Anti-loop
103
+ 3 validates on the same URL without convergence → stop. Reduce the page to a known-good shell, add pieces back one at a time, validate after each. The first addition that breaks is the bug.
104
+
105
+ Never retry the same failing browser action twice.""";
104
106
 
105
107
 
106
108
  glob SEM_PROGRESS_TEMPLATE: str = """## Progress Tracking
@@ -541,9 +541,26 @@ with entry {
541
541
  try {
542
542
  import litellm;
543
543
  import from litellm.integrations.custom_logger { CustomLogger as _CL }
544
- import from jac_coder.runtime.cost_tracker { record_cost }
544
+ import from jac_coder.runtime.cost_tracker {
545
+ record_cost,
546
+ mark_call_started as _ct_mark_call_started,
547
+ mark_call_finished as _ct_mark_call_finished
548
+ }
545
549
 
546
550
  class _CostLogger(_CL) {
551
+ def log_pre_api_call(
552
+ self: _CostLogger,
553
+ model: object,
554
+ messages: object,
555
+ kwargs: dict
556
+ ) {
557
+ try {
558
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
559
+ _ct_mark_call_started(_cid);
560
+ } except Exception {
561
+ _ = 0;
562
+ }
563
+ }
547
564
  def log_success_event(
548
565
  self: _CostLogger,
549
566
  kwargs: dict,
@@ -551,15 +568,18 @@ with entry {
551
568
  start_time: object,
552
569
  end_time: object
553
570
  ) {
571
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
554
572
  try {
555
573
  slp = kwargs.get("standard_logging_object") or {};
556
574
  raw = slp.get("response_cost") or 0;
557
575
  cost = float(str(raw));
558
576
  if cost > 0 {
559
- record_cost(cost);
577
+ record_cost(cost, _cid);
560
578
  }
561
579
  } except Exception {
562
580
  _ = 0;
581
+ } finally {
582
+ try { _ct_mark_call_finished(_cid); } except Exception { ; }
563
583
  }
564
584
  }
565
585
 
@@ -570,7 +590,8 @@ with entry {
570
590
  start_time: object,
571
591
  end_time: object
572
592
  ) {
573
- _ = 0;
593
+ _cid = str(kwargs.get("litellm_call_id", "") or "");
594
+ try { _ct_mark_call_finished(_cid); } except Exception { ; }
574
595
  }
575
596
  }
576
597
 
@@ -34,7 +34,7 @@ impl clarify_intent(
34
34
  "allow_custom": allow_custom,
35
35
  "skippable": skippable
36
36
  });
37
- result = str(_await_clarification(session_id, options_payload));
37
+ result = str(_await_clarification(session_id, request_id, options_payload));
38
38
  } else {
39
39
  # 3. CLI branch — colored numbered menu
40
40
  print(f"\n{YELLOW}[Clarification]{RESET} {question}\n");