eigencore 0.4.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 (49) hide show
  1. eigencore/__init__.py +82 -0
  2. eigencore/agent/__init__.py +45 -0
  3. eigencore/agent/executor.py +297 -0
  4. eigencore/agent/grammar.py +121 -0
  5. eigencore/agent/policy.py +262 -0
  6. eigencore/agent/react.py +261 -0
  7. eigencore/agent/tool.py +149 -0
  8. eigencore/analysis/__init__.py +3 -0
  9. eigencore/analysis/sparsity.py +184 -0
  10. eigencore/benchmark/__init__.py +33 -0
  11. eigencore/benchmark/harness.py +342 -0
  12. eigencore/cli/__init__.py +0 -0
  13. eigencore/cli/main.py +856 -0
  14. eigencore/context/__init__.py +3 -0
  15. eigencore/context/manager.py +318 -0
  16. eigencore/engine/__init__.py +3 -0
  17. eigencore/engine/inference.py +378 -0
  18. eigencore/engine/layer_skip.py +268 -0
  19. eigencore/engine/prompt_lookup.py +39 -0
  20. eigencore/engine/pruning.py +116 -0
  21. eigencore/engine/sparse_inference.py +218 -0
  22. eigencore/engine/sparsity_experiment.py +109 -0
  23. eigencore/engine/speculative.py +238 -0
  24. eigencore/fit/__init__.py +34 -0
  25. eigencore/fit/estimator.py +310 -0
  26. eigencore/fit/gguf_meta.py +130 -0
  27. eigencore/forge.py +216 -0
  28. eigencore/hal/__init__.py +3 -0
  29. eigencore/hal/_cpuid_win.py +119 -0
  30. eigencore/hal/profiler.py +349 -0
  31. eigencore/kernels/__init__.py +28 -0
  32. eigencore/kernels/q4k.py +76 -0
  33. eigencore/kernels/reference.py +168 -0
  34. eigencore/models/__init__.py +3 -0
  35. eigencore/models/registry.py +193 -0
  36. eigencore/router/__init__.py +3 -0
  37. eigencore/router/classifier.py +176 -0
  38. eigencore/training/__init__.py +3 -0
  39. eigencore/training/convergence.py +112 -0
  40. eigencore/training/trainer.py +401 -0
  41. eigencore/tui/__init__.py +5 -0
  42. eigencore/tui/chat.py +265 -0
  43. eigencore/tui/onboarding.py +134 -0
  44. eigencore/tui/theme.py +61 -0
  45. eigencore-0.4.0.dist-info/METADATA +144 -0
  46. eigencore-0.4.0.dist-info/RECORD +49 -0
  47. eigencore-0.4.0.dist-info/WHEEL +4 -0
  48. eigencore-0.4.0.dist-info/entry_points.txt +2 -0
  49. eigencore-0.4.0.dist-info/licenses/LICENSE +21 -0
eigencore/__init__.py ADDED
@@ -0,0 +1,82 @@
1
+ """EigenCore — CPU-first LLM intelligence runtime."""
2
+
3
+ __version__ = "0.4.0"
4
+
5
+ from eigencore.agent.executor import ToolExecutor, ToolResult
6
+ from eigencore.agent.grammar import GBNFBuilder
7
+ from eigencore.agent.policy import AuditLog, PolicyEngine, ToolCapability, default_policy
8
+ from eigencore.agent.react import AgentResult, AgentStep, ReActAgent, StepKind
9
+ from eigencore.agent.tool import Tool, ToolParam, ToolRegistry
10
+ from eigencore.benchmark import (
11
+ DEFAULT_CASES,
12
+ BenchmarkReport,
13
+ QualityCase,
14
+ build_report,
15
+ run_cases,
16
+ )
17
+ from eigencore.context.manager import ContextManager
18
+ from eigencore.engine.inference import GenerationConfig, GenerationResult, InferenceEngine
19
+ from eigencore.engine.layer_skip import LayerSkipScheduler, SkipPlan, SkipStrategy
20
+ from eigencore.engine.sparse_inference import SparseExecutionPlan, SparsityCache, SparsityPredictor
21
+ from eigencore.engine.speculative import SpeculativeDecoder, SpeculativeResult
22
+ from eigencore.fit import (
23
+ FitEstimate,
24
+ FitVerdict,
25
+ ModelGeometry,
26
+ estimate_fit,
27
+ max_context_for_budget,
28
+ )
29
+ from eigencore.forge import Forge
30
+ from eigencore.hal.profiler import HardwareProfile, profile_hardware
31
+ from eigencore.kernels import ScalarBackend, rms_norm
32
+ from eigencore.models.registry import ModelRegistry, ModelSpec
33
+ from eigencore.router.classifier import RoutingDecision, TaskDomain, TaskRouter
34
+
35
+ __all__ = [
36
+ "DEFAULT_CASES",
37
+ "AgentResult",
38
+ "AgentStep",
39
+ "AuditLog",
40
+ "BenchmarkReport",
41
+ "ContextManager",
42
+ "FitEstimate",
43
+ "FitVerdict",
44
+ "Forge",
45
+ "GBNFBuilder",
46
+ "GenerationConfig",
47
+ "GenerationResult",
48
+ "HardwareProfile",
49
+ "InferenceEngine",
50
+ "LayerSkipScheduler",
51
+ "ModelGeometry",
52
+ "ModelRegistry",
53
+ "ModelSpec",
54
+ "PolicyEngine",
55
+ "QualityCase",
56
+ "ReActAgent",
57
+ "RoutingDecision",
58
+ "ScalarBackend",
59
+ "SkipPlan",
60
+ "SkipStrategy",
61
+ "SparseExecutionPlan",
62
+ "SparsityCache",
63
+ "SparsityPredictor",
64
+ "SpeculativeDecoder",
65
+ "SpeculativeResult",
66
+ "StepKind",
67
+ "TaskDomain",
68
+ "TaskRouter",
69
+ "Tool",
70
+ "ToolCapability",
71
+ "ToolExecutor",
72
+ "ToolParam",
73
+ "ToolRegistry",
74
+ "ToolResult",
75
+ "build_report",
76
+ "default_policy",
77
+ "estimate_fit",
78
+ "max_context_for_budget",
79
+ "profile_hardware",
80
+ "rms_norm",
81
+ "run_cases",
82
+ ]
@@ -0,0 +1,45 @@
1
+ """Agent framework — tool calling, GBNF grammars, and ReAct reasoning."""
2
+
3
+ from eigencore.agent.executor import ToolExecutor, ToolResult
4
+ from eigencore.agent.grammar import GBNFBuilder
5
+ from eigencore.agent.policy import (
6
+ ApprovalManager,
7
+ ApprovalMode,
8
+ ApprovalRequest,
9
+ AuditEntry,
10
+ AuditLog,
11
+ PolicyDecision,
12
+ PolicyEngine,
13
+ ToolCapability,
14
+ default_policy,
15
+ redact_args,
16
+ redact_text,
17
+ resolve_in_workspace,
18
+ )
19
+ from eigencore.agent.react import AgentResult, AgentStep, ReActAgent, StepKind
20
+ from eigencore.agent.tool import Tool, ToolParam, ToolRegistry
21
+
22
+ __all__ = [
23
+ "ApprovalManager",
24
+ "ApprovalMode",
25
+ "ApprovalRequest",
26
+ "AgentResult",
27
+ "AgentStep",
28
+ "AuditEntry",
29
+ "AuditLog",
30
+ "GBNFBuilder",
31
+ "PolicyDecision",
32
+ "PolicyEngine",
33
+ "ReActAgent",
34
+ "StepKind",
35
+ "Tool",
36
+ "ToolCapability",
37
+ "ToolExecutor",
38
+ "ToolParam",
39
+ "ToolRegistry",
40
+ "ToolResult",
41
+ "default_policy",
42
+ "redact_args",
43
+ "redact_text",
44
+ "resolve_in_workspace",
45
+ ]
@@ -0,0 +1,297 @@
1
+ """Safe tool execution with validation and timeouts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import pickle
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum, auto
12
+ from typing import Any
13
+
14
+ from eigencore.agent.policy import (
15
+ ApprovalManager,
16
+ PolicyEngine,
17
+ default_policy,
18
+ redact_text,
19
+ resolve_in_workspace,
20
+ )
21
+ from eigencore.agent.tool import Tool, ToolRegistry
22
+
23
+
24
+ class ToolStatus(Enum):
25
+ SUCCESS = auto()
26
+ ERROR = auto()
27
+ TIMEOUT = auto()
28
+ VALIDATION_ERROR = auto()
29
+ POLICY_DENIED = auto()
30
+
31
+
32
+ @dataclass
33
+ class ToolResult:
34
+ """Result of executing a tool."""
35
+
36
+ tool_name: str
37
+ status: ToolStatus
38
+ output: Any = None
39
+ error: str | None = None
40
+ elapsed_seconds: float = 0.0
41
+
42
+ @property
43
+ def ok(self) -> bool:
44
+ return self.status == ToolStatus.SUCCESS
45
+
46
+ def format_for_prompt(self) -> str:
47
+ if self.ok:
48
+ return f"[{self.tool_name}] Result: {self.output}"
49
+ return f"[{self.tool_name}] Error ({self.status.name}): {self.error}"
50
+
51
+
52
+ @dataclass
53
+ class ExecutionLog:
54
+ """Tracks all tool executions in an agent run."""
55
+
56
+ entries: list[ToolResult] = field(default_factory=list)
57
+
58
+ def add(self, result: ToolResult) -> None:
59
+ self.entries.append(result)
60
+
61
+ @property
62
+ def total_calls(self) -> int:
63
+ return len(self.entries)
64
+
65
+ @property
66
+ def success_count(self) -> int:
67
+ return sum(1 for e in self.entries if e.ok)
68
+
69
+ @property
70
+ def error_count(self) -> int:
71
+ return sum(1 for e in self.entries if not e.ok)
72
+
73
+ @property
74
+ def total_time(self) -> float:
75
+ return sum(e.elapsed_seconds for e in self.entries)
76
+
77
+
78
+ class ToolExecutor:
79
+ """Executes tools with policy enforcement, validation, killable timeouts, logging.
80
+
81
+ Deny-by-default: when no policy is passed, a `default_policy()` (READ-only,
82
+ workspace-confined) is used — an executor never runs a tool just because a
83
+ caller forgot to attach a policy.
84
+ """
85
+
86
+ DEFAULT_TIMEOUT = 30.0
87
+
88
+ def __init__(
89
+ self,
90
+ registry: ToolRegistry,
91
+ timeout: float = DEFAULT_TIMEOUT,
92
+ max_output_chars: int = 4096,
93
+ policy: PolicyEngine | None = None,
94
+ max_cpu_seconds: int = 60,
95
+ max_memory_mb: int = 4096,
96
+ max_file_size_mb: int = 64,
97
+ max_processes: int = 0, # 0 = do not set RLIMIT_NPROC (breaks library imports); >0 enables it
98
+ approver: ApprovalManager | None = None,
99
+ ask_fn=None, # callable(tool_name, args) -> bool; required when approver needs a human
100
+ ):
101
+ self.registry = registry
102
+ self.timeout = timeout
103
+ self.max_output_chars = max_output_chars
104
+ self.policy = policy if policy is not None else default_policy()
105
+ # POSIX resource limits applied to the child process (best-effort;
106
+ # silently skipped on platforms without resource/preexec_fn).
107
+ self.max_cpu_seconds = max_cpu_seconds
108
+ self.max_memory_mb = max_memory_mb
109
+ self.max_file_size_mb = max_file_size_mb
110
+ self.max_processes = max_processes
111
+ self.approver = approver
112
+ self.ask_fn = ask_fn
113
+ self.log = ExecutionLog()
114
+
115
+ def execute(self, tool_name: str, args: dict[str, Any]) -> ToolResult:
116
+ tool = self.registry.get(tool_name)
117
+ if tool is None:
118
+ result = ToolResult(
119
+ tool_name=tool_name,
120
+ status=ToolStatus.ERROR,
121
+ error=f"Unknown tool: '{tool_name}'. Available: {self.registry.tool_names}",
122
+ )
123
+ self.log.add(result)
124
+ return result
125
+
126
+ # Policy enforcement happens BEFORE validation/running, in code —
127
+ # never in prompt text and never in the UI layer.
128
+ decision = self.policy.check(tool, args)
129
+ if not decision.allowed:
130
+ result = ToolResult(
131
+ tool_name=tool_name,
132
+ status=ToolStatus.POLICY_DENIED,
133
+ error=f"Denied by policy: {decision.reason}",
134
+ )
135
+ self.log.add(result)
136
+ if self.policy.audit_log is not None:
137
+ entry = self.policy.audit_log.entries[-1]
138
+ entry.status = ToolStatus.POLICY_DENIED.name
139
+ return result
140
+
141
+ # Approval gate: after policy, before validation/running. Enforcement
142
+ # is here in code; the UI only renders and collects the answer.
143
+ if self.approver is not None and self.approver.needs_approval(tool_name):
144
+ if self.ask_fn is None:
145
+ result = ToolResult(
146
+ tool_name=tool_name,
147
+ status=ToolStatus.POLICY_DENIED,
148
+ error="Denied: approval required but no approval callback is attached",
149
+ )
150
+ self.log.add(result)
151
+ return result
152
+ approved = bool(self.ask_fn(tool_name, args))
153
+ self.approver.record(tool_name, approved)
154
+ if not approved:
155
+ result = ToolResult(
156
+ tool_name=tool_name,
157
+ status=ToolStatus.POLICY_DENIED,
158
+ error="Denied by user approval",
159
+ )
160
+ self.log.add(result)
161
+ return result
162
+
163
+ try:
164
+ validated_args = tool.validate_args(args)
165
+ except (ValueError, TypeError) as e:
166
+ result = ToolResult(
167
+ tool_name=tool_name,
168
+ status=ToolStatus.VALIDATION_ERROR,
169
+ error=str(e),
170
+ )
171
+ self.log.add(result)
172
+ return result
173
+
174
+ # Resolve declared path params to workspace-confined absolute paths
175
+ # BEFORE the handler sees them (no raw outside-workspace paths).
176
+ if self.policy.workspace_root is not None:
177
+ for param in tool.parameters:
178
+ if (
179
+ param.is_path
180
+ and param.name in validated_args
181
+ and isinstance(validated_args[param.name], str)
182
+ ):
183
+ validated_args[param.name] = str(
184
+ resolve_in_workspace(validated_args[param.name], self.policy.workspace_root)
185
+ )
186
+
187
+ result = self._run_with_timeout(tool, validated_args)
188
+ self.log.add(result)
189
+ if self.policy.audit_log is not None:
190
+ entry = self.policy.audit_log.entries[-1]
191
+ entry.status = result.status.name
192
+ entry.elapsed_seconds = result.elapsed_seconds
193
+ return result
194
+
195
+ def _run_with_timeout(self, tool: Tool, args: dict[str, Any]) -> ToolResult:
196
+ """Run the handler in a child `python` process; on timeout, kill it.
197
+
198
+ A thread-pool timeout cannot stop a running handler; a child process
199
+ can. Uses subprocess+pickle (not multiprocessing spawn) because spawn
200
+ is unreliable under pytest on Windows. Handlers must be picklable
201
+ (top-level functions) — lambdas/closures are rejected up front.
202
+ """
203
+ start = time.perf_counter()
204
+ try:
205
+ payload = pickle.dumps((tool.handler, args))
206
+ except Exception as e:
207
+ return ToolResult(
208
+ tool_name=tool.name,
209
+ status=ToolStatus.ERROR,
210
+ error=f"handler/args not picklable for process isolation: {e}",
211
+ elapsed_seconds=time.perf_counter() - start,
212
+ )
213
+ proc = subprocess.Popen(
214
+ [sys.executable, "-c", _WORKER_CODE],
215
+ stdin=subprocess.PIPE,
216
+ stdout=subprocess.PIPE,
217
+ stderr=subprocess.DEVNULL,
218
+ preexec_fn=self._limits_fn() if os.name == "posix" else None,
219
+ )
220
+ try:
221
+ out, _ = proc.communicate(input=payload, timeout=self.timeout)
222
+ except subprocess.TimeoutExpired:
223
+ proc.kill()
224
+ proc.communicate()
225
+ return ToolResult(
226
+ tool_name=tool.name,
227
+ status=ToolStatus.TIMEOUT,
228
+ error=f"Tool execution timed out after {self.timeout}s (process killed)",
229
+ elapsed_seconds=self.timeout,
230
+ )
231
+ elapsed = time.perf_counter() - start
232
+ if proc.returncode != 0 or not out:
233
+ return ToolResult(
234
+ tool_name=tool.name,
235
+ status=ToolStatus.ERROR,
236
+ error=f"handler process exited with code {proc.returncode}",
237
+ elapsed_seconds=elapsed,
238
+ )
239
+ try:
240
+ kind, value = pickle.loads(out)
241
+ except Exception:
242
+ return ToolResult(
243
+ tool_name=tool.name,
244
+ status=ToolStatus.ERROR,
245
+ error="handler process returned an unreadable result",
246
+ elapsed_seconds=elapsed,
247
+ )
248
+ if kind == "ok":
249
+ output = value
250
+ if isinstance(value, str):
251
+ output = redact_text(value)
252
+ if len(output) > self.max_output_chars:
253
+ output = output[: self.max_output_chars] + "... [truncated]"
254
+ return ToolResult(
255
+ tool_name=tool.name,
256
+ status=ToolStatus.SUCCESS,
257
+ output=output,
258
+ elapsed_seconds=elapsed,
259
+ )
260
+ return ToolResult(
261
+ tool_name=tool.name,
262
+ status=ToolStatus.ERROR,
263
+ error=redact_text(value),
264
+ elapsed_seconds=elapsed,
265
+ )
266
+
267
+ def _limits_fn(self):
268
+ """Build a preexec_fn applying rlimits to the child (POSIX only)."""
269
+ import resource
270
+
271
+ max_cpu = self.max_cpu_seconds
272
+ max_mem = self.max_memory_mb * 1024 * 1024
273
+ max_fsize = self.max_file_size_mb * 1024 * 1024
274
+ max_nproc = self.max_processes
275
+
276
+ def apply() -> None:
277
+ resource.setrlimit(resource.RLIMIT_CPU, (max_cpu, max_cpu + 5))
278
+ resource.setrlimit(resource.RLIMIT_AS, (max_mem, max_mem))
279
+ resource.setrlimit(resource.RLIMIT_FSIZE, (max_fsize, max_fsize))
280
+ if max_nproc > 0:
281
+ resource.setrlimit(resource.RLIMIT_NPROC, (max_nproc, max_nproc))
282
+
283
+ return apply
284
+
285
+
286
+ _WORKER_CODE = """
287
+ import pickle, sys, traceback
288
+ try:
289
+ handler, args = pickle.loads(sys.stdin.buffer.read())
290
+ try:
291
+ result = handler(**args)
292
+ sys.stdout.buffer.write(pickle.dumps(("ok", result)))
293
+ except Exception as e:
294
+ sys.stdout.buffer.write(pickle.dumps(("err", f"{type(e).__name__}: {e}\\n{traceback.format_exc()}")))
295
+ except Exception as e:
296
+ sys.stdout.buffer.write(pickle.dumps(("err", f"unpickle failed: {e}")))
297
+ """
@@ -0,0 +1,121 @@
1
+ """GBNF grammar builder for structured LLM output.
2
+
3
+ GBNF (GGML BNF) is llama.cpp's grammar format for constraining token
4
+ generation. On CPU this is nearly free — the grammar mask is applied
5
+ during sampling with no extra forward passes. This gives CPU inference
6
+ a structural advantage: guaranteed valid JSON tool calls without
7
+ retries or validation loops.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from eigencore.agent.tool import Tool, ToolRegistry
13
+
14
+
15
+ class GBNFBuilder:
16
+ """Builds GBNF grammars that constrain model output to valid structures."""
17
+
18
+ # reusable primitives
19
+ _WS = "ws ::= [ \\t\\n]*"
20
+ _STRING = r'string ::= "\"" ([^"\\] | "\\" .)* "\""'
21
+ _INTEGER = 'integer ::= "-"? [0-9]+'
22
+ _NUMBER = 'number ::= "-"? [0-9]+ ("." [0-9]+)?'
23
+ _BOOLEAN = 'boolean ::= "true" | "false"'
24
+
25
+ @classmethod
26
+ def tool_call_grammar(cls, registry: ToolRegistry) -> str:
27
+ if not registry.tools:
28
+ raise ValueError("Cannot build grammar for empty tool registry")
29
+
30
+ rules: list[str] = []
31
+ tool_alternatives: list[str] = []
32
+
33
+ for tool in registry.tools:
34
+ rule_name = f"call_{tool.name}"
35
+ tool_alternatives.append(rule_name)
36
+ rules.append(cls._tool_rule(tool, rule_name))
37
+
38
+ root = 'root ::= ws "{" ws tool_call ws "}" ws'
39
+ tool_union = "tool_call ::= " + " | ".join(tool_alternatives)
40
+
41
+ parts = [
42
+ root,
43
+ tool_union,
44
+ *rules,
45
+ cls._WS,
46
+ cls._STRING,
47
+ cls._INTEGER,
48
+ cls._NUMBER,
49
+ cls._BOOLEAN,
50
+ ]
51
+ return "\n".join(parts)
52
+
53
+ @classmethod
54
+ def json_object_grammar(cls, properties: dict[str, str]) -> str:
55
+ """Build a grammar for a fixed JSON object shape.
56
+
57
+ Args:
58
+ properties: mapping of key name → type ("string", "integer", "number", "boolean")
59
+ """
60
+ if not properties:
61
+ return 'root ::= ws "{" ws "}" ws\n' + cls._WS
62
+
63
+ pairs: list[str] = []
64
+ for key, val_type in properties.items():
65
+ type_rule = cls._type_to_rule(val_type)
66
+ pairs.append(f'"\\"" "{key}" "\\"" ws ":" ws {type_rule}')
67
+
68
+ body = ' ws "," ws '.join(pairs)
69
+ root = f'root ::= ws "{{" ws {body} ws "}}" ws'
70
+ return "\n".join([root, cls._WS, cls._STRING, cls._INTEGER, cls._NUMBER, cls._BOOLEAN])
71
+
72
+ @classmethod
73
+ def _tool_rule(cls, tool: Tool, rule_name: str) -> str:
74
+ name_literal = f'"\\"{tool.name}\\""'
75
+ tool_name_part = f'"\\"tool\\"" ws ":" ws {name_literal}'
76
+
77
+ if not tool.parameters:
78
+ args_part = '"\\"args\\"" ws ":" ws "{" ws "}"'
79
+ return f'{rule_name} ::= {tool_name_part} ws "," ws {args_part}'
80
+
81
+ param_pairs: list[str] = []
82
+ for param in tool.parameters:
83
+ type_rule = cls._type_to_rule(param.type)
84
+ if param.enum:
85
+ enum_alts = " | ".join(f'"\\"{v}\\""' for v in param.enum)
86
+ type_rule = f"({enum_alts})"
87
+ param_pair = f'"\\"{param.name}\\"" ws ":" ws {type_rule}'
88
+ param_pairs.append(param_pair)
89
+
90
+ args_body = ' ws "," ws '.join(param_pairs)
91
+ args_part = f'"\\"args\\"" ws ":" ws "{{" ws {args_body} ws "}}"'
92
+ return f'{rule_name} ::= {tool_name_part} ws "," ws {args_part}'
93
+
94
+ @staticmethod
95
+ def _type_to_rule(type_name: str) -> str:
96
+ mapping = {
97
+ "string": "string",
98
+ "integer": "integer",
99
+ "number": "number",
100
+ "boolean": "boolean",
101
+ }
102
+ rule = mapping.get(type_name)
103
+ if rule is None:
104
+ raise ValueError(f"Unsupported type for GBNF: {type_name}")
105
+ return rule
106
+
107
+ @classmethod
108
+ def thought_or_action_grammar(cls, registry: ToolRegistry) -> str:
109
+ """Grammar for ReAct: model must output either a thought or a tool call."""
110
+ tool_grammar = cls.tool_call_grammar(registry)
111
+
112
+ thought_rule = 'thought ::= "\\"thought\\"" ws ":" ws string'
113
+ action_rule = 'action ::= "\\"action\\"" ws ":" ws "{" ws tool_call ws "}"'
114
+
115
+ new_root = 'root ::= ws "{" ws (thought_block | action_block) ws "}" ws'
116
+ thought_block = 'thought_block ::= "\\"type\\"" ws ":" ws "\\"thought\\"" ws "," ws thought'
117
+ action_block = 'action_block ::= "\\"type\\"" ws ":" ws "\\"action\\"" ws "," ws action'
118
+
119
+ remaining = "\n".join(tool_grammar.split("\n")[1:])
120
+ parts = [new_root, thought_block, action_block, thought_rule, action_rule, remaining]
121
+ return "\n".join(parts)