@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.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +13 -0
- package/.skills.json +19 -0
- package/AGENTS.md +1344 -0
- package/CLAUDE.md +178 -0
- package/GEMINI.md +102 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/SKILL.md +132 -0
- package/bin/agentic-hooks.sh +79 -0
- package/bin/cli.js +1060 -0
- package/core/__init__.py +52 -0
- package/core/ai_evaluator.py +117 -0
- package/core/autopilot_engine.py +368 -0
- package/core/clean_code_guard.py +188 -0
- package/core/engine_py/__init__.py +29 -0
- package/core/engine_py/agent_worker.py +136 -0
- package/core/engine_py/decider.py +150 -0
- package/core/engine_py/energy.py +45 -0
- package/core/engine_py/event_bus.py +63 -0
- package/core/engine_py/executor.py +186 -0
- package/core/engine_py/models.py +193 -0
- package/core/engine_py/queue.py +314 -0
- package/core/engine_py/runner.py +116 -0
- package/core/engine_py/system_workers.py +70 -0
- package/core/engine_py/toon_adapter.py +586 -0
- package/core/engine_py/verification_controller.py +208 -0
- package/core/engine_py/worker.py +167 -0
- package/core/engine_spec/event_schema.json +65 -0
- package/core/engine_spec/example_workflow.yaml +73 -0
- package/core/engine_spec/workflow_schema.json +127 -0
- package/core/hooks/__init__.py +29 -0
- package/core/hooks/adapters/__init__.py +25 -0
- package/core/hooks/adapters/claude_adapter.py +83 -0
- package/core/hooks/adapters/cli_agent_adapter.py +82 -0
- package/core/hooks/adapters/codex_adapter.py +78 -0
- package/core/hooks/adapters/cursor_adapter.py +73 -0
- package/core/hooks/adapters/gemini_adapter.py +93 -0
- package/core/hooks/adapters/homebrew_adapter.py +69 -0
- package/core/hooks/adapters/mcp_proxy.py +133 -0
- package/core/hooks/adapters/shell_adapter.py +65 -0
- package/core/hooks/dispatcher.py +118 -0
- package/core/hooks/policy_engine.py +375 -0
- package/core/hooks/session_end.py +141 -0
- package/core/hooks/types.py +147 -0
- package/core/integrations/__init__.py +28 -0
- package/core/integrations/installer.py +225 -0
- package/core/integrations/lifecycle_director.py +175 -0
- package/core/integrations/registry.py +105 -0
- package/core/multi_agent_system.py +164 -0
- package/core/skills_indexer.py +742 -0
- package/core/system/__init__.py +25 -0
- package/core/system/announcements.py +72 -0
- package/core/system/dependencies.py +69 -0
- package/core/system/doctor.py +171 -0
- package/core/system/health.py +144 -0
- package/core/system/installer.py +137 -0
- package/core/system/notifications.py +97 -0
- package/core/system/refresher.py +110 -0
- package/core/system/updater.py +167 -0
- package/core/system/version_tracker.py +65 -0
- package/docs/architecture_plan.md +7 -0
- package/docs/guides/failure-recovery.md +714 -0
- package/docs/implementation_summary.md +10 -0
- package/docs/protocols/autopilot-execution.md +148 -0
- package/docs/protocols/code-change-protocol.md +49 -0
- package/docs/protocols/context-preservation-detail.md +114 -0
- package/docs/protocols/quality-gates.md +110 -0
- package/docs/protocols/ulw-mode.md +60 -0
- package/docs/research_findings.md +10 -0
- package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
- package/install.sh +111 -0
- package/marketplace.json +37 -0
- package/package.json +81 -0
- package/skills/agentic-workflow/SKILL.md +132 -0
- package/skills/agentic-workflow/skill-spec.json +100 -0
- package/soul.md +445 -0
- package/src/engine_ts/decider.ts +186 -0
- package/src/engine_ts/event-bus.ts +57 -0
- package/src/engine_ts/executor.ts +262 -0
- package/src/engine_ts/index.ts +12 -0
- package/src/engine_ts/queue.ts +93 -0
- package/src/engine_ts/runner.ts +108 -0
- package/src/engine_ts/skills-indexer.ts +264 -0
- package/src/engine_ts/toon-adapter.ts +91 -0
- package/src/engine_ts/types.ts +134 -0
- package/src/engine_ts/verification-controller.ts +204 -0
- package/src/engine_ts/worker.ts +280 -0
- package/src/hooks/adapters/claude-adapter.ts +54 -0
- package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
- package/src/hooks/adapters/codex-adapter.ts +69 -0
- package/src/hooks/adapters/cursor-adapter.ts +60 -0
- package/src/hooks/adapters/gemini-adapter.ts +71 -0
- package/src/hooks/adapters/homebrew-adapter.ts +36 -0
- package/src/hooks/adapters/mcp-proxy.ts +66 -0
- package/src/hooks/adapters/shell-adapter.ts +42 -0
- package/src/hooks/dispatcher.ts +113 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/policy-engine.ts +376 -0
- package/src/hooks/session-end.ts +125 -0
- package/src/hooks/types.ts +61 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +23 -0
- package/src/integrations/index.ts +7 -0
- package/src/integrations/installer.ts +208 -0
- package/src/integrations/lifecycle-director.ts +139 -0
- package/src/integrations/registry.ts +82 -0
- package/src/system/announcements.ts +143 -0
- package/src/system/dependencies.ts +176 -0
- package/src/system/doctor.ts +374 -0
- package/src/system/health.ts +270 -0
- package/src/system/index.ts +14 -0
- package/src/system/installer.ts +262 -0
- package/src/system/notifications.ts +180 -0
- package/src/system/refresher.ts +207 -0
- package/src/system/types.ts +268 -0
- package/src/system/updater.ts +219 -0
- 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
|
+
}
|