agentjit 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.
- agentjit/__init__.py +43 -0
- agentjit/analyzer.py +302 -0
- agentjit/codegen.py +137 -0
- agentjit/compiler.py +60 -0
- agentjit/decorators.py +129 -0
- agentjit/integrations/__init__.py +5 -0
- agentjit/integrations/base.py +59 -0
- agentjit/runtime.py +128 -0
- agentjit/tracer.py +149 -0
- agentjit/types.py +155 -0
- agentjit-0.1.0.dist-info/METADATA +239 -0
- agentjit-0.1.0.dist-info/RECORD +14 -0
- agentjit-0.1.0.dist-info/WHEEL +4 -0
- agentjit-0.1.0.dist-info/licenses/LICENSE +132 -0
agentjit/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""AgentJIT: Just-In-Time Compiler for AI Agent Trajectories.
|
|
2
|
+
|
|
3
|
+
Compiles non-deterministic, expensive multi-step LLM workflows into ultra-fast
|
|
4
|
+
deterministic Python code with zero token costs and automatic speculative fallbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from agentjit.codegen import GuardViolation
|
|
8
|
+
from agentjit.compiler import compile_trajectory, create_compiled_pipeline
|
|
9
|
+
from agentjit.decorators import jit, JITWrapper
|
|
10
|
+
from agentjit.integrations.base import ToolDispatcher
|
|
11
|
+
from agentjit.runtime import CompiledPipeline
|
|
12
|
+
from agentjit.tracer import Tracer, trace_tool
|
|
13
|
+
from agentjit.types import (
|
|
14
|
+
CompilationResult,
|
|
15
|
+
ExecutionMetrics,
|
|
16
|
+
ExecutionNode,
|
|
17
|
+
Guard,
|
|
18
|
+
SourceType,
|
|
19
|
+
TraceStep,
|
|
20
|
+
Trajectory,
|
|
21
|
+
ValueSource,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"jit",
|
|
27
|
+
"JITWrapper",
|
|
28
|
+
"Tracer",
|
|
29
|
+
"trace_tool",
|
|
30
|
+
"compile_trajectory",
|
|
31
|
+
"create_compiled_pipeline",
|
|
32
|
+
"CompiledPipeline",
|
|
33
|
+
"GuardViolation",
|
|
34
|
+
"Trajectory",
|
|
35
|
+
"TraceStep",
|
|
36
|
+
"ExecutionNode",
|
|
37
|
+
"Guard",
|
|
38
|
+
"ValueSource",
|
|
39
|
+
"SourceType",
|
|
40
|
+
"CompilationResult",
|
|
41
|
+
"ExecutionMetrics",
|
|
42
|
+
"ToolDispatcher",
|
|
43
|
+
]
|
agentjit/analyzer.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Data-flow dependency analyzer and graph synthesis for agent trajectories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
from agentjit.types import (
|
|
9
|
+
ExecutionNode,
|
|
10
|
+
Guard,
|
|
11
|
+
SourceType,
|
|
12
|
+
TraceStep,
|
|
13
|
+
Trajectory,
|
|
14
|
+
ValueSource,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TrajectoryAnalyzer:
|
|
19
|
+
"""Analyzes a recorded agent trajectory to construct a deterministic execution DAG."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, trajectory: Trajectory) -> None:
|
|
22
|
+
self.trajectory = trajectory
|
|
23
|
+
self.entry_args = trajectory.entry_args
|
|
24
|
+
self.steps = trajectory.steps
|
|
25
|
+
|
|
26
|
+
def analyze(self) -> Tuple[List[ExecutionNode], List[Guard], ValueSource]:
|
|
27
|
+
"""Perform data-flow analysis and return execution nodes, guards, and final output source."""
|
|
28
|
+
nodes: List[ExecutionNode] = []
|
|
29
|
+
step_outputs: Dict[int, Any] = {}
|
|
30
|
+
|
|
31
|
+
# 1. Analyze each step and determine argument provenance
|
|
32
|
+
for step in self.steps:
|
|
33
|
+
bindings: Dict[str, ValueSource] = {}
|
|
34
|
+
for param_name, param_val in step.inputs.items():
|
|
35
|
+
source = self._resolve_value_source(param_val, step.step_id, step_outputs)
|
|
36
|
+
bindings[param_name] = source
|
|
37
|
+
|
|
38
|
+
node = ExecutionNode(
|
|
39
|
+
node_id=step.step_id,
|
|
40
|
+
tool_name=step.tool_name,
|
|
41
|
+
argument_bindings=bindings,
|
|
42
|
+
output_var_name=f"step_{step.step_id}_out",
|
|
43
|
+
)
|
|
44
|
+
nodes.append(node)
|
|
45
|
+
step_outputs[step.step_id] = step.output
|
|
46
|
+
|
|
47
|
+
# 2. Determine where the final result comes from
|
|
48
|
+
final_source = self._resolve_final_output(step_outputs)
|
|
49
|
+
|
|
50
|
+
# 3. Generate speculative execution guards
|
|
51
|
+
guards = self._generate_guards()
|
|
52
|
+
|
|
53
|
+
return nodes, guards, final_source
|
|
54
|
+
|
|
55
|
+
def _resolve_value_source(
|
|
56
|
+
self,
|
|
57
|
+
target_val: Any,
|
|
58
|
+
current_step_id: int,
|
|
59
|
+
step_outputs: Dict[int, Any],
|
|
60
|
+
) -> ValueSource:
|
|
61
|
+
"""Trace backwards to locate the origin of target_val."""
|
|
62
|
+
# 1. Direct match with entry argument
|
|
63
|
+
for arg_name, arg_val in self.entry_args.items():
|
|
64
|
+
if self._safe_equals(target_val, arg_val):
|
|
65
|
+
return ValueSource(source_type=SourceType.ENTRY_ARG, name=arg_name)
|
|
66
|
+
|
|
67
|
+
# 2. Direct match with output of an earlier step
|
|
68
|
+
for prev_id in range(current_step_id - 1, 0, -1):
|
|
69
|
+
if prev_id in step_outputs and self._safe_equals(target_val, step_outputs[prev_id]):
|
|
70
|
+
return ValueSource(source_type=SourceType.STEP_OUTPUT, step_id=prev_id)
|
|
71
|
+
|
|
72
|
+
# 3. Nested match within entry arguments (e.g. user['id'])
|
|
73
|
+
for arg_name, arg_val in self.entry_args.items():
|
|
74
|
+
found, path = self._search_nested(target_val, arg_val)
|
|
75
|
+
if found:
|
|
76
|
+
return ValueSource(
|
|
77
|
+
source_type=SourceType.NESTED_ACCESS,
|
|
78
|
+
name=arg_name,
|
|
79
|
+
key_path=path,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 4. Nested match within previous step outputs (e.g. order['customer_id'])
|
|
83
|
+
for prev_id in range(current_step_id - 1, 0, -1):
|
|
84
|
+
if prev_id in step_outputs:
|
|
85
|
+
found, path = self._search_nested(target_val, step_outputs[prev_id])
|
|
86
|
+
if found:
|
|
87
|
+
return ValueSource(
|
|
88
|
+
source_type=SourceType.NESTED_ACCESS,
|
|
89
|
+
step_id=prev_id,
|
|
90
|
+
key_path=path,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 5. Arithmetic binary operation synthesis (e.g. unit_price * quantity)
|
|
94
|
+
if isinstance(target_val, (int, float)) and not isinstance(target_val, bool):
|
|
95
|
+
bin_op = self._try_synthesize_arithmetic(target_val, current_step_id, step_outputs)
|
|
96
|
+
if bin_op is not None:
|
|
97
|
+
return bin_op
|
|
98
|
+
|
|
99
|
+
# 6. Nested dictionary structure check
|
|
100
|
+
if isinstance(target_val, dict):
|
|
101
|
+
dict_sources = {
|
|
102
|
+
k: self._resolve_value_source(v, current_step_id, step_outputs)
|
|
103
|
+
for k, v in target_val.items()
|
|
104
|
+
}
|
|
105
|
+
return ValueSource(
|
|
106
|
+
source_type=SourceType.DICT_SYNTHESIS,
|
|
107
|
+
dict_fields=dict_sources,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 7. Nested list structure check
|
|
111
|
+
if isinstance(target_val, (list, tuple)):
|
|
112
|
+
list_sources = [
|
|
113
|
+
self._resolve_value_source(item, current_step_id, step_outputs)
|
|
114
|
+
for item in target_val
|
|
115
|
+
]
|
|
116
|
+
return ValueSource(
|
|
117
|
+
source_type=SourceType.LIST_SYNTHESIS,
|
|
118
|
+
list_items=list_sources,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# 8. Invariant literal
|
|
122
|
+
return ValueSource(source_type=SourceType.LITERAL, literal_value=target_val)
|
|
123
|
+
|
|
124
|
+
def _try_synthesize_arithmetic(
|
|
125
|
+
self,
|
|
126
|
+
target_val: Union[int, float],
|
|
127
|
+
current_step_id: int,
|
|
128
|
+
step_outputs: Dict[int, Any],
|
|
129
|
+
) -> Optional[ValueSource]:
|
|
130
|
+
"""Attempt to synthesize a binary arithmetic operation matching target_val."""
|
|
131
|
+
candidates: List[Tuple[Union[int, float], ValueSource]] = []
|
|
132
|
+
|
|
133
|
+
# Collect numeric candidates from entry args
|
|
134
|
+
for arg_name, arg_val in self.entry_args.items():
|
|
135
|
+
if isinstance(arg_val, (int, float)) and not isinstance(arg_val, bool):
|
|
136
|
+
candidates.append(
|
|
137
|
+
(arg_val, ValueSource(source_type=SourceType.ENTRY_ARG, name=arg_name))
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Collect numeric candidates from previous step outputs
|
|
141
|
+
for prev_id in range(1, current_step_id):
|
|
142
|
+
out = step_outputs.get(prev_id)
|
|
143
|
+
if isinstance(out, (int, float)) and not isinstance(out, bool):
|
|
144
|
+
candidates.append(
|
|
145
|
+
(out, ValueSource(source_type=SourceType.STEP_OUTPUT, step_id=prev_id))
|
|
146
|
+
)
|
|
147
|
+
elif isinstance(out, dict):
|
|
148
|
+
for k, v in out.items():
|
|
149
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
150
|
+
candidates.append(
|
|
151
|
+
(
|
|
152
|
+
v,
|
|
153
|
+
ValueSource(
|
|
154
|
+
source_type=SourceType.NESTED_ACCESS,
|
|
155
|
+
step_id=prev_id,
|
|
156
|
+
key_path=[k],
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Test pairwise operations (prefer multiplication and addition)
|
|
162
|
+
ops = [
|
|
163
|
+
("*", lambda a, b: a * b),
|
|
164
|
+
("+", lambda a, b: a + b),
|
|
165
|
+
("-", lambda a, b: a - b),
|
|
166
|
+
("/", lambda a, b: (a / b) if b != 0 else None),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
for i in range(len(candidates)):
|
|
170
|
+
for j in range(len(candidates)):
|
|
171
|
+
if i == j:
|
|
172
|
+
continue
|
|
173
|
+
v1, src1 = candidates[i]
|
|
174
|
+
v2, src2 = candidates[j]
|
|
175
|
+
|
|
176
|
+
for op_symbol, op_fn in ops:
|
|
177
|
+
try:
|
|
178
|
+
computed = op_fn(v1, v2)
|
|
179
|
+
if computed is not None and math.isclose(
|
|
180
|
+
computed, target_val, rel_tol=1e-5, abs_tol=1e-5
|
|
181
|
+
):
|
|
182
|
+
return ValueSource(
|
|
183
|
+
source_type=SourceType.BINARY_OP,
|
|
184
|
+
left=src1,
|
|
185
|
+
right=src2,
|
|
186
|
+
operator=op_symbol,
|
|
187
|
+
)
|
|
188
|
+
except Exception:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def _resolve_final_output(self, step_outputs: Dict[int, Any]) -> ValueSource:
|
|
194
|
+
"""Find the source expression for the trajectory's final result."""
|
|
195
|
+
final_res = self.trajectory.final_result
|
|
196
|
+
if not self.steps:
|
|
197
|
+
return ValueSource(source_type=SourceType.LITERAL, literal_value=final_res)
|
|
198
|
+
|
|
199
|
+
# 1. Check if final result directly equals a step's output
|
|
200
|
+
for step_id in reversed(sorted(step_outputs.keys())):
|
|
201
|
+
if self._safe_equals(final_res, step_outputs[step_id]):
|
|
202
|
+
return ValueSource(source_type=SourceType.STEP_OUTPUT, step_id=step_id)
|
|
203
|
+
|
|
204
|
+
# 2. Check if final result is a dict composed of step outputs/entry args
|
|
205
|
+
if isinstance(final_res, dict):
|
|
206
|
+
dict_sources = {
|
|
207
|
+
k: self._resolve_value_source(v, len(self.steps) + 1, step_outputs)
|
|
208
|
+
for k, v in final_res.items()
|
|
209
|
+
}
|
|
210
|
+
return ValueSource(
|
|
211
|
+
source_type=SourceType.DICT_SYNTHESIS,
|
|
212
|
+
dict_fields=dict_sources,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# 3. Check if final result is a list composed of step outputs/entry args
|
|
216
|
+
if isinstance(final_res, (list, tuple)):
|
|
217
|
+
list_sources = [
|
|
218
|
+
self._resolve_value_source(item, len(self.steps) + 1, step_outputs)
|
|
219
|
+
for item in final_res
|
|
220
|
+
]
|
|
221
|
+
return ValueSource(
|
|
222
|
+
source_type=SourceType.LIST_SYNTHESIS,
|
|
223
|
+
list_items=list_sources,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# 4. Check if nested in the last step output
|
|
227
|
+
last_id = self.steps[-1].step_id
|
|
228
|
+
if last_id in step_outputs:
|
|
229
|
+
found, path = self._search_nested(final_res, step_outputs[last_id])
|
|
230
|
+
if found:
|
|
231
|
+
return ValueSource(
|
|
232
|
+
source_type=SourceType.NESTED_ACCESS,
|
|
233
|
+
step_id=last_id,
|
|
234
|
+
key_path=path,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Default to output of last step
|
|
238
|
+
return ValueSource(source_type=SourceType.STEP_OUTPUT, step_id=last_id)
|
|
239
|
+
|
|
240
|
+
def _generate_guards(self) -> List[Guard]:
|
|
241
|
+
"""Generate speculative validation guards for the entry arguments."""
|
|
242
|
+
guards: List[Guard] = []
|
|
243
|
+
for arg_name, arg_val in self.entry_args.items():
|
|
244
|
+
if arg_val is not None:
|
|
245
|
+
# Non-null guard
|
|
246
|
+
guards.append(
|
|
247
|
+
Guard(
|
|
248
|
+
target_param=arg_name,
|
|
249
|
+
guard_type="non_null",
|
|
250
|
+
condition_code=f"{arg_name} is not None",
|
|
251
|
+
description=f"Argument '{arg_name}' must not be None",
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
# Type guard
|
|
255
|
+
type_name = type(arg_val).__name__
|
|
256
|
+
guards.append(
|
|
257
|
+
Guard(
|
|
258
|
+
target_param=arg_name,
|
|
259
|
+
guard_type="type_match",
|
|
260
|
+
condition_code=f"isinstance({arg_name}, {type_name})",
|
|
261
|
+
description=f"Argument '{arg_name}' must be of type {type_name}",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
return guards
|
|
265
|
+
|
|
266
|
+
def _search_nested(
|
|
267
|
+
self,
|
|
268
|
+
target: Any,
|
|
269
|
+
haystack: Any,
|
|
270
|
+
max_depth: int = 4,
|
|
271
|
+
) -> Tuple[bool, List[Union[str, int]]]:
|
|
272
|
+
"""Recursively search for target inside a nested dictionary/list structure."""
|
|
273
|
+
if max_depth <= 0 or target is None or haystack is None:
|
|
274
|
+
return False, []
|
|
275
|
+
|
|
276
|
+
if isinstance(haystack, dict):
|
|
277
|
+
for k, v in haystack.items():
|
|
278
|
+
if self._safe_equals(target, v):
|
|
279
|
+
return True, [k]
|
|
280
|
+
found, subpath = self._search_nested(target, v, max_depth - 1)
|
|
281
|
+
if found:
|
|
282
|
+
return True, [k] + subpath
|
|
283
|
+
|
|
284
|
+
elif isinstance(haystack, (list, tuple)):
|
|
285
|
+
for idx, item in enumerate(haystack):
|
|
286
|
+
if self._safe_equals(target, item):
|
|
287
|
+
return True, [idx]
|
|
288
|
+
found, subpath = self._search_nested(target, item, max_depth - 1)
|
|
289
|
+
if found:
|
|
290
|
+
return True, [idx] + subpath
|
|
291
|
+
|
|
292
|
+
return False, []
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _safe_equals(a: Any, b: Any) -> bool:
|
|
296
|
+
"""Safe equality comparison avoiding numpy/pandas ambiguity."""
|
|
297
|
+
if a is b:
|
|
298
|
+
return True
|
|
299
|
+
try:
|
|
300
|
+
return bool(a == b)
|
|
301
|
+
except Exception:
|
|
302
|
+
return False
|
agentjit/codegen.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""AST and Python source code generator for compiled agent trajectories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from agentjit.types import (
|
|
9
|
+
CompilationResult,
|
|
10
|
+
ExecutionNode,
|
|
11
|
+
Guard,
|
|
12
|
+
Trajectory,
|
|
13
|
+
ValueSource,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GuardViolation(Exception):
|
|
18
|
+
"""Raised at runtime when a compiled pipeline's speculative guard fails."""
|
|
19
|
+
def __init__(self, message: str, param: str = "") -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.param = param
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CodeGenerator:
|
|
25
|
+
"""Synthesizes human-readable, typed, and executable Python code from an execution DAG."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
entry_params: List[str],
|
|
30
|
+
nodes: List[ExecutionNode],
|
|
31
|
+
guards: List[Guard],
|
|
32
|
+
final_source: ValueSource,
|
|
33
|
+
tool_registry: Dict[str, Callable],
|
|
34
|
+
func_name: str = "compiled_pipeline",
|
|
35
|
+
) -> None:
|
|
36
|
+
self.entry_params = entry_params
|
|
37
|
+
self.nodes = nodes
|
|
38
|
+
self.guards = guards
|
|
39
|
+
self.final_source = final_source
|
|
40
|
+
self.tool_registry = tool_registry
|
|
41
|
+
self.func_name = func_name
|
|
42
|
+
|
|
43
|
+
def generate_source(self) -> str:
|
|
44
|
+
"""Generate formatted Python source code for the compiled pipeline."""
|
|
45
|
+
lines: List[str] = []
|
|
46
|
+
params_str = ", ".join(self.entry_params)
|
|
47
|
+
lines.append(f"def {self.func_name}({params_str}):")
|
|
48
|
+
lines.append(' """JIT-compiled trajectory pipeline generated by AgentJIT.')
|
|
49
|
+
lines.append(" Executes deterministically in sub-millisecond time with zero token cost.")
|
|
50
|
+
lines.append(' """')
|
|
51
|
+
|
|
52
|
+
# 1. Speculative Guards
|
|
53
|
+
if self.guards:
|
|
54
|
+
lines.append(" # --- Speculative Guards ---")
|
|
55
|
+
for g in self.guards:
|
|
56
|
+
# Add validation statement
|
|
57
|
+
lines.append(f" if not ({g.condition_code}):")
|
|
58
|
+
escaped_desc = g.description.replace('"', '\\"')
|
|
59
|
+
lines.append(
|
|
60
|
+
f' raise GuardViolation("{escaped_desc}", param="{g.target_param}")'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
lines.append("")
|
|
64
|
+
lines.append(" # --- Execution Steps ---")
|
|
65
|
+
var_map: Dict[int, str] = {}
|
|
66
|
+
|
|
67
|
+
# 2. Tool invocations
|
|
68
|
+
for node in self.nodes:
|
|
69
|
+
var_name = node.output_var_name
|
|
70
|
+
var_map[node.node_id] = var_name
|
|
71
|
+
|
|
72
|
+
args_parts: List[str] = []
|
|
73
|
+
for param_k, val_src in node.argument_bindings.items():
|
|
74
|
+
arg_expr = val_src.render_expression(var_map)
|
|
75
|
+
args_parts.append(f"{param_k}={arg_expr}")
|
|
76
|
+
|
|
77
|
+
args_str = ", ".join(args_parts)
|
|
78
|
+
lines.append(
|
|
79
|
+
f" {var_name} = _tools[{repr(node.tool_name)}]({args_str})"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 3. Final return
|
|
83
|
+
lines.append("")
|
|
84
|
+
lines.append(" # --- Return Final Result ---")
|
|
85
|
+
return_expr = self.final_source.render_expression(var_map)
|
|
86
|
+
lines.append(f" return {return_expr}")
|
|
87
|
+
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
def compile(self) -> CompilationResult:
|
|
91
|
+
"""Generate source code, validate AST, and compile into an executable callable."""
|
|
92
|
+
source = self.generate_source()
|
|
93
|
+
|
|
94
|
+
# Validate syntax via AST
|
|
95
|
+
try:
|
|
96
|
+
tree = ast.parse(source)
|
|
97
|
+
except SyntaxError as e:
|
|
98
|
+
return CompilationResult(
|
|
99
|
+
success=False,
|
|
100
|
+
source_code=source,
|
|
101
|
+
error=f"Syntax error in synthesized code: {e}",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Execute compilation in controlled namespace
|
|
105
|
+
exec_globals: Dict[str, Any] = {
|
|
106
|
+
"_tools": self.tool_registry,
|
|
107
|
+
"GuardViolation": GuardViolation,
|
|
108
|
+
"isinstance": isinstance,
|
|
109
|
+
"int": int,
|
|
110
|
+
"float": float,
|
|
111
|
+
"str": str,
|
|
112
|
+
"bool": bool,
|
|
113
|
+
"dict": dict,
|
|
114
|
+
"list": list,
|
|
115
|
+
}
|
|
116
|
+
exec_locals: Dict[str, Any] = {}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
code_obj = compile(tree, filename="<agentjit_compiled>", mode="exec")
|
|
120
|
+
exec(code_obj, exec_globals, exec_locals)
|
|
121
|
+
compiled_func = exec_locals.get(self.func_name)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return CompilationResult(
|
|
124
|
+
success=False,
|
|
125
|
+
source_code=source,
|
|
126
|
+
error=f"Execution error during module compilation: {e}",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return CompilationResult(
|
|
130
|
+
success=True,
|
|
131
|
+
source_code=source,
|
|
132
|
+
compiled_callable=compiled_func,
|
|
133
|
+
nodes=self.nodes,
|
|
134
|
+
guards=self.guards,
|
|
135
|
+
entry_parameters=self.entry_params,
|
|
136
|
+
output_expression=self.final_source.render_expression({}),
|
|
137
|
+
)
|
agentjit/compiler.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""High-level compiler coordinating analysis, code generation, and pipeline assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from agentjit.analyzer import TrajectoryAnalyzer
|
|
8
|
+
from agentjit.codegen import CodeGenerator
|
|
9
|
+
from agentjit.runtime import CompiledPipeline
|
|
10
|
+
from agentjit.types import CompilationResult, Trajectory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def compile_trajectory(
|
|
14
|
+
trajectory: Trajectory,
|
|
15
|
+
tool_registry: Dict[str, Callable],
|
|
16
|
+
function_name: str = "compiled_agent_pipeline",
|
|
17
|
+
) -> CompilationResult:
|
|
18
|
+
"""Compile a recorded agent trajectory into an optimized Python pipeline.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
trajectory: The traced sequence of tool calls and entry parameters.
|
|
22
|
+
tool_registry: Mapping from tool names to their Python callable implementations.
|
|
23
|
+
function_name: Name of the generated Python function.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
CompilationResult containing synthesized source code and executable function.
|
|
27
|
+
"""
|
|
28
|
+
analyzer = TrajectoryAnalyzer(trajectory)
|
|
29
|
+
nodes, guards, final_source = analyzer.analyze()
|
|
30
|
+
|
|
31
|
+
entry_params = list(trajectory.entry_args.keys())
|
|
32
|
+
|
|
33
|
+
generator = CodeGenerator(
|
|
34
|
+
entry_params=entry_params,
|
|
35
|
+
nodes=nodes,
|
|
36
|
+
guards=guards,
|
|
37
|
+
final_source=final_source,
|
|
38
|
+
tool_registry=tool_registry,
|
|
39
|
+
func_name=function_name,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return generator.compile()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_compiled_pipeline(
|
|
46
|
+
trajectory: Trajectory,
|
|
47
|
+
tool_registry: Dict[str, Callable],
|
|
48
|
+
fallback_fn: Optional[Callable] = None,
|
|
49
|
+
function_name: str = "compiled_agent_pipeline",
|
|
50
|
+
) -> CompiledPipeline:
|
|
51
|
+
"""Compile a trajectory and wrap it in an executable CompiledPipeline."""
|
|
52
|
+
compilation = compile_trajectory(trajectory, tool_registry, function_name)
|
|
53
|
+
if not compilation.success:
|
|
54
|
+
raise RuntimeError(f"Failed to compile trajectory: {compilation.error}")
|
|
55
|
+
|
|
56
|
+
return CompiledPipeline(
|
|
57
|
+
compilation=compilation,
|
|
58
|
+
fallback_fn=fallback_fn,
|
|
59
|
+
estimated_uncompiled_latency_ms=max(100.0, trajectory.total_duration_ms),
|
|
60
|
+
)
|
agentjit/decorators.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""High-level @jit decorator for automatic tracing, compilation, and execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import Any, Callable, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from agentjit.compiler import create_compiled_pipeline
|
|
10
|
+
from agentjit.runtime import CompiledPipeline
|
|
11
|
+
from agentjit.tracer import Tracer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JITWrapper:
|
|
15
|
+
"""Manages the lifecycle of a JIT-compiled agent function."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
func: Callable,
|
|
20
|
+
warmup_runs: int = 1,
|
|
21
|
+
auto_compile: bool = True,
|
|
22
|
+
function_name: Optional[str] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.func = func
|
|
25
|
+
self.warmup_runs = max(1, warmup_runs)
|
|
26
|
+
self.auto_compile = auto_compile
|
|
27
|
+
self.function_name = function_name or f"compiled_{func.__name__}"
|
|
28
|
+
|
|
29
|
+
self._runs_completed = 0
|
|
30
|
+
self._pipeline: Optional[CompiledPipeline] = None
|
|
31
|
+
self._last_trajectory = None
|
|
32
|
+
|
|
33
|
+
functools.update_wrapper(self, func)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_compiled(self) -> bool:
|
|
37
|
+
"""True if the agent workflow has been successfully compiled."""
|
|
38
|
+
return self._pipeline is not None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def pipeline(self) -> Optional[CompiledPipeline]:
|
|
42
|
+
"""Access the underlying CompiledPipeline instance."""
|
|
43
|
+
return self._pipeline
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def source_code(self) -> Optional[str]:
|
|
47
|
+
"""View the compiled Python code."""
|
|
48
|
+
if self._pipeline:
|
|
49
|
+
return self._pipeline.source_code
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def stats(self) -> Dict[str, Any]:
|
|
54
|
+
"""Access performance and execution statistics."""
|
|
55
|
+
if self._pipeline:
|
|
56
|
+
return self._pipeline.stats
|
|
57
|
+
return {"status": "uncompiled", "warmup_progress": f"{self._runs_completed}/{self.warmup_runs}"}
|
|
58
|
+
|
|
59
|
+
def __call__(self, *args, **kwargs) -> Any:
|
|
60
|
+
# If already compiled, execute optimized pipeline
|
|
61
|
+
if self._pipeline is not None:
|
|
62
|
+
return self._pipeline(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
# Warmup / Tracing phase
|
|
65
|
+
sig = inspect.signature(self.func)
|
|
66
|
+
bound = sig.bind(*args, **kwargs)
|
|
67
|
+
bound.apply_defaults()
|
|
68
|
+
entry_args = dict(bound.arguments)
|
|
69
|
+
|
|
70
|
+
with Tracer(entry_args=entry_args) as tracer:
|
|
71
|
+
result = self.func(*args, **kwargs)
|
|
72
|
+
tracer.set_final_result(result)
|
|
73
|
+
self._last_trajectory = tracer.trajectory
|
|
74
|
+
|
|
75
|
+
self._runs_completed += 1
|
|
76
|
+
|
|
77
|
+
# Check if warmup threshold reached
|
|
78
|
+
if self.auto_compile and self._runs_completed >= self.warmup_runs:
|
|
79
|
+
self._compile_from_tracer(tracer)
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def _compile_from_tracer(self, tracer: Tracer) -> None:
|
|
84
|
+
"""Trigger compilation using the captured trajectory and tools."""
|
|
85
|
+
try:
|
|
86
|
+
self._pipeline = create_compiled_pipeline(
|
|
87
|
+
trajectory=tracer.trajectory,
|
|
88
|
+
tool_registry=tracer._registered_tools,
|
|
89
|
+
fallback_fn=self.func,
|
|
90
|
+
function_name=self.function_name,
|
|
91
|
+
)
|
|
92
|
+
except Exception as err:
|
|
93
|
+
# If compilation fails, remain in uncompiled fallback mode
|
|
94
|
+
self._pipeline = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def jit(
|
|
98
|
+
func: Optional[Callable] = None,
|
|
99
|
+
*,
|
|
100
|
+
warmup_runs: int = 1,
|
|
101
|
+
auto_compile: bool = True,
|
|
102
|
+
function_name: Optional[str] = None,
|
|
103
|
+
) -> Any:
|
|
104
|
+
"""Decorator to JIT-compile an AI agent workflow.
|
|
105
|
+
|
|
106
|
+
Usage:
|
|
107
|
+
@jit
|
|
108
|
+
def my_agent(order_id: int, country: str):
|
|
109
|
+
order = fetch_order(order_id)
|
|
110
|
+
tax = calculate_vat(order["amount"], country)
|
|
111
|
+
return update_db(order_id, tax)
|
|
112
|
+
|
|
113
|
+
# 1st run: Executes dynamic agent, records trajectory, compiles to pure Python
|
|
114
|
+
result1 = my_agent(402, "Germany")
|
|
115
|
+
|
|
116
|
+
# 2nd run: Executes in <1ms directly with ZERO LLM calls and zero token cost!
|
|
117
|
+
result2 = my_agent(403, "France")
|
|
118
|
+
"""
|
|
119
|
+
def decorator(fn: Callable) -> JITWrapper:
|
|
120
|
+
return JITWrapper(
|
|
121
|
+
fn,
|
|
122
|
+
warmup_runs=warmup_runs,
|
|
123
|
+
auto_compile=auto_compile,
|
|
124
|
+
function_name=function_name,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if func is not None:
|
|
128
|
+
return decorator(func)
|
|
129
|
+
return decorator
|