@mamdouh-aboammar/agentic-workflow 1.2.0

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 (118) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +13 -0
  3. package/.skills.json +19 -0
  4. package/AGENTS.md +1344 -0
  5. package/CLAUDE.md +178 -0
  6. package/GEMINI.md +102 -0
  7. package/LICENSE +21 -0
  8. package/README.md +350 -0
  9. package/SKILL.md +132 -0
  10. package/bin/agentic-hooks.sh +79 -0
  11. package/bin/cli.js +1060 -0
  12. package/core/__init__.py +52 -0
  13. package/core/ai_evaluator.py +117 -0
  14. package/core/autopilot_engine.py +368 -0
  15. package/core/clean_code_guard.py +188 -0
  16. package/core/engine_py/__init__.py +29 -0
  17. package/core/engine_py/agent_worker.py +136 -0
  18. package/core/engine_py/decider.py +150 -0
  19. package/core/engine_py/energy.py +45 -0
  20. package/core/engine_py/event_bus.py +63 -0
  21. package/core/engine_py/executor.py +186 -0
  22. package/core/engine_py/models.py +193 -0
  23. package/core/engine_py/queue.py +314 -0
  24. package/core/engine_py/runner.py +116 -0
  25. package/core/engine_py/system_workers.py +70 -0
  26. package/core/engine_py/toon_adapter.py +586 -0
  27. package/core/engine_py/verification_controller.py +208 -0
  28. package/core/engine_py/worker.py +167 -0
  29. package/core/engine_spec/event_schema.json +65 -0
  30. package/core/engine_spec/example_workflow.yaml +73 -0
  31. package/core/engine_spec/workflow_schema.json +127 -0
  32. package/core/hooks/__init__.py +29 -0
  33. package/core/hooks/adapters/__init__.py +25 -0
  34. package/core/hooks/adapters/claude_adapter.py +83 -0
  35. package/core/hooks/adapters/cli_agent_adapter.py +82 -0
  36. package/core/hooks/adapters/codex_adapter.py +78 -0
  37. package/core/hooks/adapters/cursor_adapter.py +73 -0
  38. package/core/hooks/adapters/gemini_adapter.py +93 -0
  39. package/core/hooks/adapters/homebrew_adapter.py +69 -0
  40. package/core/hooks/adapters/mcp_proxy.py +133 -0
  41. package/core/hooks/adapters/shell_adapter.py +65 -0
  42. package/core/hooks/dispatcher.py +118 -0
  43. package/core/hooks/policy_engine.py +375 -0
  44. package/core/hooks/session_end.py +141 -0
  45. package/core/hooks/types.py +147 -0
  46. package/core/integrations/__init__.py +28 -0
  47. package/core/integrations/installer.py +225 -0
  48. package/core/integrations/lifecycle_director.py +175 -0
  49. package/core/integrations/registry.py +105 -0
  50. package/core/multi_agent_system.py +164 -0
  51. package/core/skills_indexer.py +742 -0
  52. package/core/system/__init__.py +25 -0
  53. package/core/system/announcements.py +72 -0
  54. package/core/system/dependencies.py +69 -0
  55. package/core/system/doctor.py +171 -0
  56. package/core/system/health.py +144 -0
  57. package/core/system/installer.py +137 -0
  58. package/core/system/notifications.py +97 -0
  59. package/core/system/refresher.py +110 -0
  60. package/core/system/updater.py +167 -0
  61. package/core/system/version_tracker.py +65 -0
  62. package/docs/architecture_plan.md +7 -0
  63. package/docs/guides/failure-recovery.md +714 -0
  64. package/docs/implementation_summary.md +10 -0
  65. package/docs/protocols/autopilot-execution.md +148 -0
  66. package/docs/protocols/code-change-protocol.md +49 -0
  67. package/docs/protocols/context-preservation-detail.md +114 -0
  68. package/docs/protocols/quality-gates.md +110 -0
  69. package/docs/protocols/ulw-mode.md +60 -0
  70. package/docs/research_findings.md +10 -0
  71. package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
  72. package/install.sh +111 -0
  73. package/marketplace.json +37 -0
  74. package/package.json +81 -0
  75. package/skills/agentic-workflow/SKILL.md +132 -0
  76. package/skills/agentic-workflow/skill-spec.json +100 -0
  77. package/soul.md +445 -0
  78. package/src/engine_ts/decider.ts +186 -0
  79. package/src/engine_ts/event-bus.ts +57 -0
  80. package/src/engine_ts/executor.ts +262 -0
  81. package/src/engine_ts/index.ts +12 -0
  82. package/src/engine_ts/queue.ts +93 -0
  83. package/src/engine_ts/runner.ts +108 -0
  84. package/src/engine_ts/skills-indexer.ts +264 -0
  85. package/src/engine_ts/toon-adapter.ts +91 -0
  86. package/src/engine_ts/types.ts +134 -0
  87. package/src/engine_ts/verification-controller.ts +204 -0
  88. package/src/engine_ts/worker.ts +280 -0
  89. package/src/hooks/adapters/claude-adapter.ts +54 -0
  90. package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
  91. package/src/hooks/adapters/codex-adapter.ts +69 -0
  92. package/src/hooks/adapters/cursor-adapter.ts +60 -0
  93. package/src/hooks/adapters/gemini-adapter.ts +71 -0
  94. package/src/hooks/adapters/homebrew-adapter.ts +36 -0
  95. package/src/hooks/adapters/mcp-proxy.ts +66 -0
  96. package/src/hooks/adapters/shell-adapter.ts +42 -0
  97. package/src/hooks/dispatcher.ts +113 -0
  98. package/src/hooks/index.ts +16 -0
  99. package/src/hooks/policy-engine.ts +376 -0
  100. package/src/hooks/session-end.ts +125 -0
  101. package/src/hooks/types.ts +61 -0
  102. package/src/index.d.ts +34 -0
  103. package/src/index.ts +23 -0
  104. package/src/integrations/index.ts +7 -0
  105. package/src/integrations/installer.ts +208 -0
  106. package/src/integrations/lifecycle-director.ts +139 -0
  107. package/src/integrations/registry.ts +82 -0
  108. package/src/system/announcements.ts +143 -0
  109. package/src/system/dependencies.ts +176 -0
  110. package/src/system/doctor.ts +374 -0
  111. package/src/system/health.ts +270 -0
  112. package/src/system/index.ts +14 -0
  113. package/src/system/installer.ts +262 -0
  114. package/src/system/notifications.ts +180 -0
  115. package/src/system/refresher.ts +207 -0
  116. package/src/system/types.ts +268 -0
  117. package/src/system/updater.ts +219 -0
  118. package/src/system/version-tracker.ts +137 -0
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ toon_adapter.py — Universal TOON (Token-Oriented Object Notation v4.1) Adapter for Python
4
+
5
+ Compliant with the official TOON specification: https://github.com/toon-format/spec
6
+ Supports:
7
+ 1. Canonical numeric normalization (§2)
8
+ 2. Minimal string quoting heuristics (§7)
9
+ 3. Inline primitive arrays [N]: v1,v2... (§9.1)
10
+ 4. Tabular arrays for uniform objects key[N]{f1,f2}: (§9.3)
11
+ 5. Keyed tabular objects key[N:]{f1,f2}: (§9.5)
12
+ 6. Indentation-based hierarchical objects (§8)
13
+ 7. Full-line comment handling and decoding (§5.1)
14
+ 8. Token savings estimation & conversation turn compression for LLM contexts
15
+ """
16
+
17
+ import re
18
+ import json
19
+ from typing import Any, Dict, List, Optional, Tuple, Union
20
+
21
+ # Attempt optional import of upstream toon_format package if installed
22
+ try:
23
+ import toon_format as _upstream_toon
24
+ _HAS_UPSTREAM = True
25
+ except ImportError:
26
+ _upstream_toon = None
27
+ _HAS_UPSTREAM = False
28
+
29
+
30
+ class ToonEncodeError(Exception):
31
+ """Raised when data cannot be encoded to TOON."""
32
+ pass
33
+
34
+
35
+ class ToonDecodeError(Exception):
36
+ """Raised when TOON content violates specification grammar or schema."""
37
+ pass
38
+
39
+
40
+ class ToonEncoder:
41
+ """Zero-dependency reference encoder for TOON Specification v4.1."""
42
+
43
+ def __init__(self, delimiter: str = ",", indent_size: int = 2):
44
+ if delimiter not in (",", "\t", "|"):
45
+ raise ValueError(f"Invalid TOON delimiter: {delimiter!r}. Must be ',', '\\t', or '|'")
46
+ self.delimiter = delimiter
47
+ self.indent_size = indent_size
48
+
49
+ def encode(self, value: Any) -> str:
50
+ """Encodes arbitrary JSON-compatible Python value into canonical TOON string."""
51
+ normalized = self._normalize_host_type(value)
52
+ lines = self._encode_value(normalized, depth=0, key=None)
53
+ return "\n".join(lines)
54
+
55
+ def _normalize_host_type(self, val: Any) -> Any:
56
+ """Normalizes host types to JSON data model per Spec §3."""
57
+ if val is None or isinstance(val, (bool, int, float, str)):
58
+ if isinstance(val, float):
59
+ if val != val or val == float("inf") or val == float("-inf"):
60
+ return None
61
+ if val == -0.0:
62
+ return 0.0
63
+ return val
64
+ if hasattr(val, "to_dict") and callable(val.to_dict):
65
+ return self._normalize_host_type(val.to_dict())
66
+ if hasattr(val, "model_dump") and callable(val.model_dump):
67
+ return self._normalize_host_type(val.model_dump())
68
+ if isinstance(val, (list, tuple, set)):
69
+ return [self._normalize_host_type(x) for x in val]
70
+ if isinstance(val, dict):
71
+ return {str(k): self._normalize_host_type(v) for k, v in val.items()}
72
+ return str(val)
73
+
74
+ def _format_number(self, n: Union[int, float]) -> str:
75
+ """Formats numbers canonically per Spec §2."""
76
+ if isinstance(n, bool):
77
+ return "true" if n else "false"
78
+ if isinstance(n, int):
79
+ return str(n)
80
+ if n == 0.0 or n == -0.0:
81
+ return "0"
82
+ abs_n = abs(n)
83
+ if 1e-6 <= abs_n < 1e21:
84
+ s = f"{n:.14f}".rstrip("0").rstrip(".")
85
+ return s if s != "-0" else "0"
86
+ # Outside range: scientific
87
+ return f"{n:e}".replace("E", "e").replace("+0", "+").replace("-0", "-")
88
+
89
+ def _should_quote(self, s: str) -> bool:
90
+ """Determines if a string token requires quoting per Spec §7.2."""
91
+ if s == "" or s == "true" or s == "false" or s == "null":
92
+ return True
93
+ if s == "[]" or s == "{}":
94
+ return True
95
+ # Leading / trailing whitespace
96
+ if s[0] in " \t\r\n" or s[-1] in " \t\r\n":
97
+ return True
98
+ # Starts with comment char, list marker, or quote
99
+ if s.startswith("#") or s.startswith("- ") or s == "-":
100
+ return True
101
+ if s.startswith('"') or s.startswith("'"):
102
+ return True
103
+ # Contains structural delimiters
104
+ if self.delimiter in s or ":" in s or "[" in s or "]" in s or "{" in s or "}" in s:
105
+ return True
106
+ if "\n" in s or "\r" in s or "\t" in s or "\\" in s or '"' in s:
107
+ return True
108
+ # Number-like or forbidden leading zero
109
+ if re.match(r"^-?[0-9]+(?:\.[0-9]+)?(?:e[+-]?[0-9]+)?$", s, re.IGNORECASE):
110
+ return True
111
+ if re.match(r"^-?0[0-9]+", s):
112
+ return True
113
+ return False
114
+
115
+ def _quote_string(self, s: str) -> str:
116
+ """Quotes and escapes string per Spec §7.1."""
117
+ escaped = (
118
+ s.replace("\\", "\\\\")
119
+ .replace('"', '\\"')
120
+ .replace("\n", "\\n")
121
+ .replace("\r", "\\r")
122
+ .replace("\t", "\\t")
123
+ )
124
+ return f'"{escaped}"'
125
+
126
+ def _format_primitive(self, val: Any) -> str:
127
+ """Formats primitive value for inline or tabular cell."""
128
+ if val is None:
129
+ return "null"
130
+ if isinstance(val, bool):
131
+ return "true" if val else "false"
132
+ if isinstance(val, (int, float)):
133
+ return self._format_number(val)
134
+ s = str(val)
135
+ if self._should_quote(s):
136
+ return self._quote_string(s)
137
+ return s
138
+
139
+ def _is_primitive(self, val: Any) -> bool:
140
+ return val is None or isinstance(val, (bool, int, float, str))
141
+
142
+ def _is_uniform_object_array(self, arr: List[Any]) -> Tuple[bool, List[str]]:
143
+ """Checks if array elements are objects with identical primitive fields."""
144
+ if not arr or not all(isinstance(x, dict) for x in arr):
145
+ return False, []
146
+ first_keys = list(arr[0].keys())
147
+ if not first_keys:
148
+ return False, []
149
+ for item in arr:
150
+ if list(item.keys()) != first_keys:
151
+ return False, []
152
+ # Check if all values are primitives (uniform tabular)
153
+ for v in item.values():
154
+ if not self._is_primitive(v):
155
+ return False, []
156
+ return True, first_keys
157
+
158
+ def _is_uniform_object_dict(self, d: Dict[str, Any]) -> Tuple[bool, List[str]]:
159
+ """Checks if dict values are uniform objects (Keyed Tabular §9.5)."""
160
+ if not d or not all(isinstance(v, dict) for v in d.values()):
161
+ return False, []
162
+ first_val = next(iter(d.values()))
163
+ fields = list(first_val.keys())
164
+ if not fields:
165
+ return False, []
166
+ for v in d.values():
167
+ if list(v.keys()) != fields:
168
+ return False, []
169
+ for cell in v.values():
170
+ if not self._is_primitive(cell):
171
+ return False, []
172
+ return True, fields
173
+
174
+ def _encode_value(self, val: Any, depth: int, key: Optional[str]) -> List[str]:
175
+ indent = " " * (depth * self.indent_size)
176
+ child_indent = " " * ((depth + 1) * self.indent_size)
177
+ lines: List[str] = []
178
+
179
+ prefix = f"{indent}{key}: " if key is not None else indent
180
+
181
+ if self._is_primitive(val):
182
+ lines.append(f"{prefix}{self._format_primitive(val)}")
183
+ return lines
184
+
185
+ if isinstance(val, list):
186
+ length = len(val)
187
+ header_key = key if key is not None else ""
188
+ delim_sym = "" if self.delimiter == "," else ("\t" if self.delimiter == "\t" else "|")
189
+
190
+ if length == 0:
191
+ if key is not None:
192
+ lines.append(f"{indent}{key}: []")
193
+ else:
194
+ lines.append(f"{indent}[]")
195
+ return lines
196
+
197
+ # 1. Inline primitive array
198
+ if all(self._is_primitive(x) for x in val):
199
+ items_str = self.delimiter.join(self._format_primitive(x) for x in val)
200
+ lines.append(f"{indent}{header_key}[{length}{delim_sym}]: {items_str}")
201
+ return lines
202
+
203
+ # 2. Tabular form (uniform objects)
204
+ is_uniform, fields = self._is_uniform_object_array(val)
205
+ if is_uniform:
206
+ fields_str = self.delimiter.join(fields)
207
+ lines.append(f"{indent}{header_key}[{length}{delim_sym}]{{{fields_str}}}:")
208
+ for row_obj in val:
209
+ row_cells = [self._format_primitive(row_obj[f]) for f in fields]
210
+ lines.append(f"{child_indent}{self.delimiter.join(row_cells)}")
211
+ return lines
212
+
213
+ # 3. List form (mixed or non-uniform)
214
+ lines.append(f"{indent}{header_key}[{length}{delim_sym}]:")
215
+ for item in val:
216
+ if self._is_primitive(item):
217
+ lines.append(f"{child_indent}- {self._format_primitive(item)}")
218
+ elif isinstance(item, dict):
219
+ # List of objects
220
+ sub_lines = self._encode_value(item, depth=depth + 1, key=None)
221
+ if sub_lines:
222
+ # First field carries list hyphen
223
+ first_line = sub_lines[0].lstrip()
224
+ lines.append(f"{child_indent}- {first_line}")
225
+ for extra_line in sub_lines[1:]:
226
+ lines.append(f" {extra_line}")
227
+ else:
228
+ lines.append(f"{child_indent}-")
229
+ elif isinstance(item, list):
230
+ sub_lines = self._encode_value(item, depth=depth + 1, key=None)
231
+ if sub_lines:
232
+ first_line = sub_lines[0].lstrip()
233
+ lines.append(f"{child_indent}- {first_line}")
234
+ for extra_line in sub_lines[1:]:
235
+ lines.append(f" {extra_line}")
236
+ return lines
237
+
238
+ if isinstance(val, dict):
239
+ # Check Keyed Tabular form §9.5
240
+ is_keyed, fields = self._is_uniform_object_dict(val)
241
+ delim_sym = "" if self.delimiter == "," else ("\t" if self.delimiter == "\t" else "|")
242
+ if is_keyed and len(val) > 0:
243
+ header_key = key if key is not None else ""
244
+ fields_str = self.delimiter.join(fields)
245
+ lines.append(f"{indent}{header_key}[{len(val)}:{delim_sym}]{{{fields_str}}}:")
246
+ for k, v in val.items():
247
+ cells = [self._format_primitive(v[f]) for f in fields]
248
+ lines.append(f"{child_indent}{k}: {self.delimiter.join(cells)}")
249
+ return lines
250
+
251
+ if key is not None:
252
+ lines.append(f"{indent}{key}:")
253
+ current_depth = depth + 1 if key is not None else depth
254
+ for k, v in val.items():
255
+ encoded_field = self._encode_value(v, depth=current_depth, key=k)
256
+ lines.extend(encoded_field)
257
+ return lines
258
+
259
+ lines.append(f"{prefix}{self._format_primitive(str(val))}")
260
+ return lines
261
+
262
+
263
+ class ToonDecoder:
264
+ """Reference decoder for TOON Specification v4.1 with strict mode."""
265
+
266
+ def __init__(self, delimiter: str = ",", indent_size: int = 2, strict: bool = True):
267
+ self.delimiter = delimiter
268
+ self.indent_size = indent_size
269
+ self.strict = strict
270
+
271
+ def decode(self, content: str) -> Any:
272
+ """Parses TOON formatted text back into Python data structures."""
273
+ # Pre-pass: strip full-line comments (§5.1) and CRLF normalisation
274
+ raw_lines = content.replace("\r\n", "\n").split("\n")
275
+ lines: List[str] = []
276
+ for line in raw_lines:
277
+ stripped = line.strip()
278
+ if stripped.startswith("#"):
279
+ continue
280
+ lines.append(line)
281
+
282
+ # Remove trailing blank lines
283
+ while lines and not lines[-1].strip():
284
+ lines.pop()
285
+
286
+ if not lines or not any(l.strip() for l in lines):
287
+ return {}
288
+
289
+ parsed, _ = self._parse_block(lines, start_idx=0, base_depth=0)
290
+ return parsed
291
+
292
+ def _parse_primitive(self, token: str) -> Any:
293
+ token = token.strip()
294
+ if token == "null":
295
+ return None
296
+ if token == "true":
297
+ return True
298
+ if token == "false":
299
+ return False
300
+ if token == "[]":
301
+ return []
302
+ if token == "{}":
303
+ return {}
304
+
305
+ # Quoted string
306
+ if (token.startswith('"') and token.endswith('"')) or (token.startswith("'") and token.endswith("'")):
307
+ inner = token[1:-1]
308
+ return (
309
+ inner.replace('\\"', '"')
310
+ .replace("\\'", "'")
311
+ .replace("\\n", "\n")
312
+ .replace("\\r", "\r")
313
+ .replace("\\t", "\t")
314
+ .replace("\\\\", "\\")
315
+ )
316
+
317
+ # Number parsing per §4
318
+ # Forbidden leading zero (e.g. "05") is treated as string
319
+ if re.match(r"^-?0[0-9]+", token):
320
+ return token
321
+
322
+ if re.match(r"^-?[0-9]+(?:\.[0-9]+)?(?:e[+-]?[0-9]+)?$", token, re.IGNORECASE):
323
+ if "." in token or "e" in token or "E" in token:
324
+ try:
325
+ return float(token)
326
+ except ValueError:
327
+ return token
328
+ try:
329
+ return int(token)
330
+ except ValueError:
331
+ return token
332
+
333
+ return token
334
+
335
+ def _split_delim(self, text: str, delimiter: str) -> List[str]:
336
+ """Splits cells while respecting quoted substrings."""
337
+ cells: List[str] = []
338
+ curr: List[str] = []
339
+ in_quote = False
340
+ quote_char = ""
341
+ escape = False
342
+
343
+ for ch in text:
344
+ if escape:
345
+ curr.append(ch)
346
+ escape = False
347
+ continue
348
+ if ch == "\\":
349
+ curr.append(ch)
350
+ escape = True
351
+ continue
352
+ if ch in ('"', "'"):
353
+ if not in_quote:
354
+ in_quote = True
355
+ quote_char = ch
356
+ elif quote_char == ch:
357
+ in_quote = False
358
+ curr.append(ch)
359
+ continue
360
+ if ch == delimiter and not in_quote:
361
+ cells.append("".join(curr).strip())
362
+ curr = []
363
+ else:
364
+ curr.append(ch)
365
+
366
+ cells.append("".join(curr).strip())
367
+ return cells
368
+
369
+ def _parse_block(self, lines: List[str], start_idx: int, base_depth: int) -> Tuple[Any, int]:
370
+ result_dict: Dict[str, Any] = {}
371
+ idx = start_idx
372
+
373
+ while idx < len(lines):
374
+ line = lines[idx]
375
+ if not line.strip():
376
+ idx += 1
377
+ continue
378
+
379
+ current_indent = len(line) - len(line.lstrip(" "))
380
+ depth = current_indent // self.indent_size
381
+ if depth < base_depth:
382
+ break
383
+
384
+ stripped = line.strip()
385
+
386
+ # Root array or Keyed tabular or Tabular array header
387
+ # Pattern 1: Tabular array: key[N<delim?>]{fields}: OR [N<delim?>]{fields}:
388
+ tabular_match = re.match(r"^([a-zA-Z0-9_\-\.]+)?\[(\d+)([\t|])?\]\{([^}]+)\}:\s*$", stripped)
389
+ if tabular_match:
390
+ key = tabular_match.group(1)
391
+ count = int(tabular_match.group(2))
392
+ delim = tabular_match.group(3) or self.delimiter
393
+ fields = self._split_delim(tabular_match.group(4), delim)
394
+ rows: List[Dict[str, Any]] = []
395
+ idx += 1
396
+
397
+ while idx < len(lines):
398
+ sub_line = lines[idx]
399
+ if not sub_line.strip():
400
+ idx += 1
401
+ continue
402
+ sub_indent = len(sub_line) - len(sub_line.lstrip(" "))
403
+ sub_depth = sub_indent // self.indent_size
404
+ if sub_depth <= depth:
405
+ break
406
+ row_cells = self._split_delim(sub_line.strip(), delim)
407
+ row_dict = {}
408
+ for i, f in enumerate(fields):
409
+ val_str = row_cells[i] if i < len(row_cells) else ""
410
+ row_dict[f] = self._parse_primitive(val_str)
411
+ rows.append(row_dict)
412
+ idx += 1
413
+
414
+ if key:
415
+ result_dict[key] = rows
416
+ else:
417
+ return rows, idx
418
+ continue
419
+
420
+ # Pattern 2: Keyed Tabular: key[N:<delim?>]{fields}: OR [N:<delim?>]{fields}:
421
+ keyed_match = re.match(r"^([a-zA-Z0-9_\-\.]+)?\[(\d+):([\t|])?\]\{([^}]+)\}:\s*$", stripped)
422
+ if keyed_match:
423
+ key = keyed_match.group(1)
424
+ count = int(keyed_match.group(2))
425
+ delim = keyed_match.group(3) or self.delimiter
426
+ fields = self._split_delim(keyed_match.group(4), delim)
427
+ keyed_obj: Dict[str, Dict[str, Any]] = {}
428
+ idx += 1
429
+
430
+ while idx < len(lines):
431
+ sub_line = lines[idx]
432
+ if not sub_line.strip():
433
+ idx += 1
434
+ continue
435
+ sub_indent = len(sub_line) - len(sub_line.lstrip(" "))
436
+ sub_depth = sub_indent // self.indent_size
437
+ if sub_depth <= depth:
438
+ break
439
+ if ":" in sub_line:
440
+ entry_key, cells_part = sub_line.strip().split(":", 1)
441
+ entry_key = entry_key.strip()
442
+ cells = self._split_delim(cells_part.strip(), delim)
443
+ entry_dict = {}
444
+ for i, f in enumerate(fields):
445
+ val_str = cells[i] if i < len(cells) else ""
446
+ entry_dict[f] = self._parse_primitive(val_str)
447
+ keyed_obj[entry_key] = entry_dict
448
+ idx += 1
449
+
450
+ if key:
451
+ result_dict[key] = keyed_obj
452
+ else:
453
+ return keyed_obj, idx
454
+ continue
455
+
456
+ # Pattern 3: Inline primitive array: key[N<delim?>]: v1,v2 OR [N<delim?>]: v1,v2
457
+ inline_match = re.match(r"^([a-zA-Z0-9_\-\.]+)?\[(\d+)([\t|])?\]:\s*(.*)$", stripped)
458
+ if inline_match:
459
+ key = inline_match.group(1)
460
+ count = int(inline_match.group(2))
461
+ delim = inline_match.group(3) or self.delimiter
462
+ items_part = inline_match.group(4).strip()
463
+ if items_part:
464
+ items = [self._parse_primitive(c) for c in self._split_delim(items_part, delim)]
465
+ idx += 1
466
+ else:
467
+ # Multiline list items
468
+ items = []
469
+ idx += 1
470
+ while idx < len(lines):
471
+ sub_line = lines[idx]
472
+ if not sub_line.strip():
473
+ idx += 1
474
+ continue
475
+ sub_indent = len(sub_line) - len(sub_line.lstrip(" "))
476
+ sub_depth = sub_indent // self.indent_size
477
+ if sub_depth <= depth:
478
+ break
479
+ item_str = sub_line.strip()
480
+ if item_str.startswith("- "):
481
+ items.append(self._parse_primitive(item_str[2:].strip()))
482
+ elif item_str == "-":
483
+ items.append(None)
484
+ idx += 1
485
+
486
+ if key:
487
+ result_dict[key] = items
488
+ else:
489
+ return items, idx
490
+ continue
491
+
492
+ # Pattern 4: Plain Key-Value or Nested Object
493
+ if ":" in stripped:
494
+ k, v = stripped.split(":", 1)
495
+ k = k.strip()
496
+ v = v.strip()
497
+ if v:
498
+ result_dict[k] = self._parse_primitive(v)
499
+ idx += 1
500
+ else:
501
+ # Nested object block
502
+ child_obj, idx = self._parse_block(lines, idx + 1, base_depth=depth + 1)
503
+ result_dict[k] = child_obj
504
+ continue
505
+
506
+ # Bare scalar at root
507
+ if depth == 0 and len(result_dict) == 0:
508
+ return self._parse_primitive(stripped), idx + 1
509
+
510
+ idx += 1
511
+
512
+ return result_dict, idx
513
+
514
+
515
+ # ═══════════════════════════════════════════════════════════════
516
+ # Public Module API
517
+ # ═══════════════════════════════════════════════════════════════
518
+
519
+ def encode_toon(data: Any, delimiter: str = ",", indent: int = 2) -> str:
520
+ """
521
+ Serializes arbitrary Python data to official TOON v4.1 format.
522
+ Delegates to upstream toon_format if available, otherwise uses reference encoder.
523
+ """
524
+ if _HAS_UPSTREAM:
525
+ try:
526
+ return _upstream_toon.encode(data, {"delimiter": delimiter, "indent": indent})
527
+ except Exception as e:
528
+ import logging
529
+ logging.debug("toon_adapter: upstream encode failed, using built-in: %s", e)
530
+ return ToonEncoder(delimiter=delimiter, indent_size=indent).encode(data)
531
+
532
+
533
+ def decode_toon(content: str, delimiter: str = ",", indent: int = 2, strict: bool = True) -> Any:
534
+ """
535
+ Parses official TOON v4.1 formatted string back into Python types.
536
+ """
537
+ if _HAS_UPSTREAM:
538
+ try:
539
+ return _upstream_toon.decode(content, {"indent": indent, "strict": strict})
540
+ except Exception as e:
541
+ import logging
542
+ logging.debug("toon_adapter: upstream decode failed, using built-in: %s", e)
543
+ return ToonDecoder(delimiter=delimiter, indent_size=indent, strict=strict).decode(content)
544
+
545
+
546
+ def format_conversation_turns(turns: List[Dict[str, Any]]) -> str:
547
+ """
548
+ Encodes multi-turn agent conversation history into a dense TOON tabular block.
549
+ Cuts context token consumption by ~45% compared to JSON.
550
+ """
551
+ normalized_turns = []
552
+ for i, t in enumerate(turns):
553
+ role = t.get("role", "assistant")
554
+ content = t.get("content", "").replace("\n", " ")
555
+ normalized_turns.append({
556
+ "idx": i + 1,
557
+ "role": role,
558
+ "content": content
559
+ })
560
+ return encode_toon({"dialogue": normalized_turns})
561
+
562
+
563
+ def calculate_token_savings(data: Any) -> Dict[str, Any]:
564
+ """
565
+ Calculates size and estimated token savings between standard JSON and TOON.
566
+ Heuristic: ~4 chars per token for typical JSON / English text.
567
+ """
568
+ json_str = json.dumps(data, indent=2)
569
+ toon_str = encode_toon(data)
570
+
571
+ json_bytes = len(json_str.encode("utf-8"))
572
+ toon_bytes = len(toon_str.encode("utf-8"))
573
+
574
+ # Token approximations
575
+ json_tokens = max(1, len(json_str) // 4)
576
+ toon_tokens = max(1, len(toon_str) // 4)
577
+ savings = max(0, int((1.0 - (toon_tokens / json_tokens)) * 100))
578
+
579
+ return {
580
+ "json_chars": len(json_str),
581
+ "toon_chars": len(toon_str),
582
+ "json_estimated_tokens": json_tokens,
583
+ "toon_estimated_tokens": toon_tokens,
584
+ "savings_percent": savings,
585
+ "bytes_ratio": round(toon_bytes / max(1, json_bytes), 2)
586
+ }