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,362 @@
1
+ # Fullstack Jac Pitfalls — .cl.jac Frontend + Backend Integration
2
+
3
+ > These rules apply when building fullstack Jaseci apps with .cl.jac frontend components.
4
+ > Everything in core_jac.md ALSO applies. This file covers the additional fullstack gotchas.
5
+
6
+ ---
7
+
8
+ ## File Types
9
+
10
+ - `.jac` — Backend code (nodes, endpoints, walkers). Uses Python runtime.
11
+ - `.cl.jac` — Frontend code (React components). Compiles to JavaScript.
12
+
13
+ ---
14
+
15
+ ## BEFORE WRITING ANY CODE
16
+
17
+ **Read `jac.toml` FIRST.** Only use npm packages listed in `[dependencies.npm]`. Do NOT import unlisted libraries. NEVER assume any UI library is available. Use plain Tailwind CSS + inline SVG for styling and icons.
18
+
19
+ ---
20
+
21
+ ## Backend Rules (.jac)
22
+
23
+ ### 1. ALL backend code goes in `main.jac`
24
+
25
+ Do NOT split backend into multiple files. Nodes, endpoints, walkers — all in `main.jac`.
26
+
27
+ ### 2. Endpoint types: `def:pub` vs `def:priv` vs `walker:pub`
28
+
29
+ ```jac
30
+ # Public — anyone can call
31
+ def:pub get_items() -> list {
32
+ return [{"id": str(i.id), "title": i.title} for i in [root-->][?:Item]];
33
+ }
34
+
35
+ # Private — requires login, per-user isolated root
36
+ def:priv get_my_items() -> list {
37
+ return [{"id": str(i.id), "title": i.title} for i in [root-->][?:Item]];
38
+ }
39
+
40
+ # Walker — for complex graph traversal
41
+ walker :pub get_details {
42
+ has item_id: str;
43
+ can find with Root entry {
44
+ for i in [-->][?:Item] {
45
+ if str(i.id) == self.item_id { visit i; return; }
46
+ }
47
+ report {"error": "not found"};
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### 3. Return dicts/lists from endpoints — NOT nodes directly
53
+
54
+ ```jac
55
+ # WRONG — returns node objects
56
+ def:pub get_items() -> list { return [root-->][?:Item]; }
57
+
58
+ # RIGHT — serialize to dicts
59
+ def:pub get_items() -> list {
60
+ return [{"id": str(i.id), "title": i.title} for i in [root-->][?:Item]];
61
+ }
62
+ ```
63
+
64
+ ### 4. Node CRUD pattern
65
+
66
+ ```jac
67
+ node Item {
68
+ has title: str;
69
+ has done: bool = False;
70
+ }
71
+
72
+ # Create + connect to root (auto-persists in SQLite)
73
+ root ++> Item(title="Buy milk");
74
+
75
+ # Query
76
+ items = [root-->][?:Item];
77
+ found = [root-->][?:Item](?title == "Buy milk")[0];
78
+
79
+ # Delete
80
+ del found;
81
+ # or: root del--> found;
82
+ ```
83
+
84
+ ### 5. Frontend entry point in main.jac
85
+
86
+ ```jac
87
+ cl import from .components.Layout { Layout }
88
+ cl {
89
+ def:pub app() -> JsxElement { return <Layout />; }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Frontend Rules (.cl.jac)
96
+
97
+ ### 6. Component props — ALWAYS use `props: dict`
98
+
99
+ ```jac
100
+ # WRONG — individual params on JsxElement components
101
+ def:pub Header(title: str, theme: str) -> JsxElement { ... }
102
+
103
+ # RIGHT — always use props: dict, destructure inside
104
+ def:pub Header(props: dict) -> JsxElement {
105
+ title = props.title or "";
106
+ theme = props.theme or "light";
107
+ return <header>{title}</header>;
108
+ }
109
+
110
+ # EXCEPTION: app(), layout(), page() take no props — that's fine
111
+ def:pub app() -> JsxElement { return <Layout />; }
112
+ ```
113
+
114
+ Components returning `JsxElement` MUST accept `props: dict` as the single parameter (or no params). NEVER use individual typed params like `(title: str, count: int)`.
115
+
116
+ Also use `className`, NOT `class` for HTML attributes in JSX:
117
+ ```jac
118
+ # WRONG
119
+ <div class="container">
120
+
121
+ # RIGHT
122
+ <div className="container">
123
+ ```
124
+
125
+ ### 7. Jac syntax, NOT JavaScript
126
+
127
+ ```
128
+ WRONG (JS) → RIGHT (Jac)
129
+ ──────────────────────────────────────────────────
130
+ const x = 5; → x = 5;
131
+ let items = []; → has items: list = [];
132
+ x ? a : b → a if x else b
133
+ () => { ... } → def handler() -> None { ... }
134
+ `hello ${name}` → f"hello {name}"
135
+ x === y → x == y
136
+ console.log(x) → print(x)
137
+ x.length → len(x)
138
+ parseInt(x) → int(x)
139
+ x.toString() → str(x)
140
+ items.map(fn) → {[<Item /> for item in items]}
141
+ items.filter(fn) → [i for i in items if cond]
142
+ items.push(x) → items = items + [x];
143
+ useEffect(() => {}, []) → can with entry { ... }
144
+ useState(0) → has count: int = 0;
145
+ !condition → not condition
146
+ new Date() → Reflect.construct(Date, [])
147
+ window.open(url) → globalThis.open(url, "_blank")
148
+ ```
149
+
150
+ ### 7. State — `has` auto-generates useState
151
+
152
+ ```jac
153
+ has count: int = 0; # auto-creates count + setCount
154
+ has name: str = "";
155
+ has items: list = [];
156
+
157
+ count = count + 1; # calls setCount internally
158
+ items = items + [newItem]; # new reference = re-render
159
+ ```
160
+
161
+ **NEVER define `setX` yourself. NEVER use `.append()` for state** — it mutates in place (no re-render). Always `items = items + [x]`.
162
+
163
+ ### 8. Effects — `can with entry/exit`
164
+
165
+ ```jac
166
+ can with entry { ... } # mount (useEffect(fn, []))
167
+ async can with entry { ... } # async mount
168
+ can with [dep1, dep2] entry { ... } # dependency watch
169
+ can with exit { ... } # cleanup/unmount
170
+ ```
171
+
172
+ **NEVER use `useEffect(lambda...)` — that is OLD syntax.**
173
+
174
+ ### 9. Event handlers — NEVER inline in JSX
175
+
176
+ ```jac
177
+ # WRONG — lambda in JSX
178
+ <button onClick={lambda -> None { count = count + 1; }}>
179
+
180
+ # WRONG — inline def
181
+ <button onClick={def(e: any) -> None { count = count + 1; }}>
182
+
183
+ # RIGHT — named function ABOVE return, passed by name
184
+ def handle_click() -> None {
185
+ count = count + 1;
186
+ }
187
+ return <button onClick={handle_click}>Click</button>;
188
+ ```
189
+
190
+ ALL event handlers must be named `def` functions defined BEFORE `return`.
191
+
192
+ ### 10. Functions MUST be defined BEFORE `return`
193
+
194
+ ```jac
195
+ # WRONG — helper after return (unreachable)
196
+ return <div>{render_item(data)}</div>;
197
+ def render_item(d: dict) -> JsxElement { ... }
198
+
199
+ # RIGHT — define above
200
+ def render_item(d: dict) -> JsxElement { ... }
201
+ return <div>{render_item(data)}</div>;
202
+ ```
203
+
204
+ ### 11. No comments inside JSX
205
+
206
+ ```jac
207
+ # WRONG — all of these crash
208
+ return <div>
209
+ {# comment}
210
+ <!-- HTML comment -->
211
+ {/* JS comment */}
212
+ </div>;
213
+
214
+ # RIGHT — comments ABOVE JSX
215
+ # Render the list
216
+ return <div>...</div>;
217
+ ```
218
+
219
+ ### 12. `sv import` — positional args ONLY
220
+
221
+ In .cl.jac, kwargs compile to a SINGLE dict arg. Server gets wrong data.
222
+
223
+ ```jac
224
+ # WRONG — kwargs
225
+ resp = await calc(a=2, b=4, op="add");
226
+ # Server receives: {"a": {"a":2, "b":4, "op":"add"}}
227
+
228
+ # RIGHT — positional, order matches def:pub signature
229
+ resp = await calc(2, 4, "add");
230
+ ```
231
+
232
+ ### 13. JS constructors need `Reflect.construct`
233
+
234
+ In .cl.jac, `ClassName()` without `new` returns wrong type or throws. Jac has no `new` keyword.
235
+
236
+ ```jac
237
+ # WRONG
238
+ year = Date().getFullYear(); # CRASH: string has no method getFullYear
239
+ ws = WebSocket("ws://localhost"); # CRASH: must be called with new
240
+
241
+ # RIGHT
242
+ year = Reflect.construct(Date, []).getFullYear();
243
+ ws = Reflect.construct(WebSocket, ["ws://localhost"]);
244
+
245
+ # Safe statics (no Reflect needed):
246
+ Date.now(); JSON.parse(); Math.random();
247
+ ```
248
+
249
+ Classes that ALWAYS need Reflect.construct: Date, WebSocket, TextDecoder, TextEncoder, URL, URLSearchParams, FormData, AbortController, RegExp, Error, Worker, Headers, Request, Response.
250
+
251
+ ### 14. Browser global name conflicts
252
+
253
+ NEVER define functions named: `open`, `close`, `print`, `fetch`, `focus`, `blur`, `scroll`, `alert`, `confirm`, `prompt`, `stop`, `find`
254
+
255
+ Use `handleOpen`, `handleClose`, `handleFetch`, etc.
256
+
257
+ ### 15. Display numbers/booleans with `str()`
258
+
259
+ ```jac
260
+ # WRONG — may render nothing
261
+ <span>{count}</span>
262
+
263
+ # RIGHT
264
+ <span>{str(count)}</span>
265
+ ```
266
+
267
+ ### 16. ALWAYS guard everything defensively
268
+
269
+ Undefined access CRASHES the whole app in .cl.jac.
270
+
271
+ ```jac
272
+ # State — always init with correct types, NEVER None
273
+ has items: list = [];
274
+ has user: dict = {};
275
+ has loading: bool = True;
276
+
277
+ # Dict access — use "key" in dict, not .get()
278
+ name = item["name"] if "name" in item else "";
279
+
280
+ # Async results — always wrap in try/except
281
+ async can with entry {
282
+ try {
283
+ result = await get_items();
284
+ items = result or [];
285
+ } except Exception as e {
286
+ error = str(e);
287
+ }
288
+ loading = False;
289
+ }
290
+
291
+ # Props — always default
292
+ items = props.items or [];
293
+ title = props.title or "";
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Import Rules (CRITICAL — memorize the pattern)
299
+
300
+ ### In .jac files (backend)
301
+
302
+ ```jac
303
+ import from datetime { datetime } # Python module
304
+ cl import from .components.Layout { Layout } # Client component (NO quotes, cl prefix)
305
+ ```
306
+
307
+ ### In .cl.jac files (frontend)
308
+
309
+ ```jac
310
+ # Client-to-client — WITH quotes, DOTS not slashes
311
+ import from ".Header" { Header } # same directory
312
+ import from "..hooks.useTodos" { useTodos } # parent directory
313
+ import from "...lib.utils" { cn } # 2 levels up
314
+
315
+ # Server calls — sv import, NO quotes
316
+ sv import from ..main { get_todos, add_todo }
317
+
318
+ # NPM packages — WITH quotes
319
+ import from "clsx" { cn }
320
+
321
+ # Runtime — cl import, WITH quotes
322
+ cl import from "@jac/runtime" { jacLogin, Outlet }
323
+
324
+ # CSS — WITH quotes
325
+ import "..styles.global.css";
326
+ ```
327
+
328
+ ### Dot level cheat sheet
329
+
330
+ ```
331
+ . = same directory
332
+ .. = 1 level up (parent)
333
+ ... = 2 levels up
334
+ ```
335
+
336
+ ### NEVER do
337
+
338
+ - `cl sv import` — never combine prefixes
339
+ - `"..main"` with quotes on sv import — sv import has NO quotes
340
+ - Slashes in import paths — always dots
341
+ - File extensions in imports — never `.cl`, `.jac`, `.cl.jac`
342
+
343
+ ---
344
+
345
+ ## Project Structure
346
+
347
+ ```
348
+ project-root/
349
+ ├── jac.toml # config + deps
350
+ ├── main.jac # ALL backend: nodes + endpoints + cl { app() }
351
+ ├── components/ # .cl.jac components (one per file)
352
+ │ ├── Layout.cl.jac # Root layout — imports child components
353
+ │ ├── Header.cl.jac # Nav/header
354
+ │ └── ItemList.cl.jac # Data display (calls hook)
355
+ ├── hooks/ # .cl.jac data hooks (sv import + state)
356
+ │ └── useItems.cl.jac
357
+ ├── pages/ # .jac file-based routing (optional)
358
+ ├── lib/ # .cl.jac shared utilities
359
+ └── styles/ # CSS files
360
+ ```
361
+
362
+ Layout.cl.jac is the root layout. Data logic lives in hooks/, called by child components — NOT by Layout.
@@ -0,0 +1,88 @@
1
+ # Coding Workflow & Validation Discipline
2
+
3
+ > Follow this workflow EVERY TIME you write Jac code. No shortcuts.
4
+
5
+ ---
6
+
7
+ ## Before Writing Code
8
+
9
+ 1. **Read `jac.toml`** — check npm deps, project config, entry point.
10
+ 2. **Call `jac_docs(query)`** — look up syntax for what you're building. Do NOT guess.
11
+ 3. **If existing project**: call `analyze_project(directory)` to understand structure.
12
+ 4. **Call `jac_docs` again** before EACH new file type (`.jac` vs `.cl.jac` have different syntax).
13
+
14
+ ---
15
+
16
+ ## Build Order (fullstack apps)
17
+
18
+ 1. `main.jac` backend first — nodes + endpoints
19
+ 2. Hooks — `hooks/useX.cl.jac` (sv import from backend)
20
+ 3. Leaf components — `Header.cl.jac`, `ItemCard.cl.jac` (no data logic)
21
+ 4. Container components — `ItemList.cl.jac` (calls hooks, renders leaf components)
22
+ 5. Layout — `Layout.cl.jac` LAST (imports child components)
23
+ 6. App entry — `cl { def:pub app() }` in `main.jac`
24
+
25
+ **Build dependencies bottom-up.** Never import a file that doesn't exist yet.
26
+
27
+ ---
28
+
29
+ ## While Writing Code
30
+
31
+ - Use `write_code` / `edit_code` — they auto-run `jac_check` per file for syntax errors.
32
+ - Do NOT call `jac_check` or `jac check` manually.
33
+ - Do NOT use `run_command` for jac check.
34
+ - If `write_code`/`edit_code` report errors → call `jac_docs(query)` to look up correct syntax → fix → retry.
35
+ - Call `jac_docs` at least once every 3-4 tool calls. If unsure about ANY syntax, look it up immediately.
36
+ - Use ABSOLUTE file paths for all operations.
37
+
38
+ ---
39
+
40
+ ## After Writing ALL Files
41
+
42
+ ### Step 1: Cross-file import check
43
+
44
+ Before validating, manually verify:
45
+ - Every `import from "..."` / `sv import from ...` references a file that exists
46
+ - sv import function names match actual `def:pub` names in `main.jac`
47
+ - Component imports use correct dot-levels (`.` same dir, `..` one up, `...` two up)
48
+ - No duplicate component names or endpoint names across files
49
+
50
+ ### Step 2: Validate
51
+
52
+ Call `validate_project(directory)` ONCE after all files are written.
53
+ - It batch type-checks all `.jac` files
54
+ - It auto-fixes common patterns (root→root(), (?:)→[?:])
55
+ - It returns all type errors with hints
56
+
57
+ ### Step 3: Fix and re-validate
58
+
59
+ - Read the report from `validate_project`
60
+ - Fix all remaining errors
61
+ - Call `validate_project()` again to confirm clean
62
+
63
+ ### Step 4: Preview log check (fullstack apps)
64
+
65
+ - Read the LAST 50 lines of `.jac-preview.log`
66
+ - Look for: `[Client]`, `ERROR`, `Exception`, `undefined`, `module not found`
67
+ - Common runtime errors:
68
+ - "module not found" → file doesn't exist OR stale HMR cache
69
+ - "undefined" → missing prop, uninitialized state, wrong import
70
+ - "Function X failed" → sv import name doesn't match backend def:pub
71
+ - "422" → request body schema mismatch
72
+
73
+ ---
74
+
75
+ ## When Stuck
76
+
77
+ 1. Call `jac_docs(query)` — look up the correct syntax
78
+ 2. Call `analyze_project` or `find_symbol` — check existing patterns
79
+ 3. If same error 2-3 times → `ask_question` to ask the user
80
+ 4. If editing same file 3+ times → step back, check assumptions
81
+
82
+ ---
83
+
84
+ ## HMR "module not found" Fix
85
+
86
+ 1. File exists but compiled JS is stale
87
+ 2. Do trivial edit on the TARGET file (not the importing file) — add a blank line
88
+ 3. HMR recompiles, import resolves
jac_coder/events.jac ADDED
@@ -0,0 +1,110 @@
1
+ import time;
2
+ import threading;
3
+
4
+ glob _tl = threading.local();
5
+
6
+
7
+ def _callbacks() -> list {
8
+ if not hasattr(_tl, "callbacks") {
9
+ _tl.callbacks = [];
10
+ }
11
+ return _tl.callbacks;
12
+ }
13
+
14
+
15
+ def register_event_callback(cb: Any) -> None {
16
+ _callbacks().append(cb);
17
+ }
18
+
19
+ def clear_event_callbacks() -> None {
20
+ _tl.callbacks = [];
21
+ }
22
+
23
+ def next_step() -> int {
24
+ if not hasattr(_tl, "step_counter") {
25
+ _tl.step_counter = 0;
26
+ }
27
+ if not hasattr(_tl, "turn_tools") {
28
+ _tl.turn_tools = 0;
29
+ }
30
+ _tl.step_counter += 1;
31
+ _tl.turn_tools += 1;
32
+ return _tl.step_counter;
33
+ }
34
+
35
+ def reset_steps() -> None {
36
+ _tl.step_counter = 0;
37
+ _tl.active_tool = {};
38
+ }
39
+
40
+ def get_active_tool() -> dict {
41
+ return getattr(_tl, "active_tool", {});
42
+ }
43
+
44
+ def set_active_tool(info: dict) -> None {
45
+ _tl.active_tool = info;
46
+ }
47
+
48
+ def set_agent_prefix(prefix: str) -> None {
49
+ _tl.agent_prefix = prefix;
50
+ }
51
+
52
+ def get_agent_prefix() -> str {
53
+ return getattr(_tl, "agent_prefix", "");
54
+ }
55
+
56
+ def start_turn() -> None {
57
+ _tl.turn_start = time.time();
58
+ _tl.turn_tools = 0;
59
+ _tl.turn_errors = 0;
60
+ _tl.turn_files = [];
61
+ }
62
+
63
+ def track_file(path: str) -> None {
64
+ if not hasattr(_tl, "turn_files") {
65
+ _tl.turn_files = [];
66
+ }
67
+ if path and path not in _tl.turn_files {
68
+ _tl.turn_files.append(path);
69
+ }
70
+ }
71
+
72
+ def track_error() -> None {
73
+ if not hasattr(_tl, "turn_errors") {
74
+ _tl.turn_errors = 0;
75
+ }
76
+ _tl.turn_errors += 1;
77
+ }
78
+
79
+ def emit_turn_summary() -> dict {
80
+ turn_start = getattr(_tl, "turn_start", 0.0);
81
+ turn_tools = getattr(_tl, "turn_tools", 0);
82
+ turn_errors = getattr(_tl, "turn_errors", 0);
83
+ turn_files = getattr(_tl, "turn_files", []);
84
+ duration_s = round(time.time() - turn_start, 1) if turn_start > 0 else 0.0;
85
+ summary = {
86
+ "tools_count": turn_tools,
87
+ "files_modified": list(turn_files),
88
+ "errors": turn_errors,
89
+ "duration_s": duration_s
90
+ };
91
+ emit_event("turn_summary", summary);
92
+ return summary;
93
+ }
94
+
95
+ def emit_event(event_type: str, data: dict) -> None {
96
+ cbs = _callbacks();
97
+ if not cbs {
98
+ return;
99
+ }
100
+ data["timestamp"] = time.time();
101
+ data["event_type"] = event_type;
102
+ for cb in list(cbs) {
103
+ try {
104
+ cb(event_type, data);
105
+ } except Exception as e {
106
+ import sys;
107
+ sys.stderr.write(f"[events] callback error: {e}\n");
108
+ }
109
+ }
110
+ }