@animalabs/agent-framework 0.6.10 → 0.7.2

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 (83) hide show
  1. package/dist/src/agent.d.ts +2 -0
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +3 -0
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/code-execution/index.d.ts +14 -0
  6. package/dist/src/code-execution/index.d.ts.map +1 -0
  7. package/dist/src/code-execution/index.js +13 -0
  8. package/dist/src/code-execution/index.js.map +1 -0
  9. package/dist/src/code-execution/py-runner.d.ts +91 -0
  10. package/dist/src/code-execution/py-runner.d.ts.map +1 -0
  11. package/dist/src/code-execution/py-runner.js +364 -0
  12. package/dist/src/code-execution/py-runner.js.map +1 -0
  13. package/dist/src/code-execution/runtime-py.d.ts +32 -0
  14. package/dist/src/code-execution/runtime-py.d.ts.map +1 -0
  15. package/dist/src/code-execution/runtime-py.js +265 -0
  16. package/dist/src/code-execution/runtime-py.js.map +1 -0
  17. package/dist/src/code-execution/tool-definition.d.ts +16 -0
  18. package/dist/src/code-execution/tool-definition.d.ts.map +1 -0
  19. package/dist/src/code-execution/tool-definition.js +45 -0
  20. package/dist/src/code-execution/tool-definition.js.map +1 -0
  21. package/dist/src/framework.d.ts +157 -15
  22. package/dist/src/framework.d.ts.map +1 -1
  23. package/dist/src/framework.js +1068 -104
  24. package/dist/src/framework.js.map +1 -1
  25. package/dist/src/index.d.ts +2 -0
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/mcpl/channel-registry.d.ts +99 -7
  30. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  31. package/dist/src/mcpl/channel-registry.js +374 -49
  32. package/dist/src/mcpl/channel-registry.js.map +1 -1
  33. package/dist/src/mcpl/prose-grammar.d.ts +24 -0
  34. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -0
  35. package/dist/src/mcpl/prose-grammar.js +33 -0
  36. package/dist/src/mcpl/prose-grammar.js.map +1 -0
  37. package/dist/src/mcpl/prose-stream-router.d.ts +58 -0
  38. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -0
  39. package/dist/src/mcpl/prose-stream-router.js +168 -0
  40. package/dist/src/mcpl/prose-stream-router.js.map +1 -0
  41. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  42. package/dist/src/modules/workspace/index.js +13 -4
  43. package/dist/src/modules/workspace/index.js.map +1 -1
  44. package/dist/src/types/agent.d.ts +17 -0
  45. package/dist/src/types/agent.d.ts.map +1 -1
  46. package/dist/src/types/framework.d.ts +33 -0
  47. package/dist/src/types/framework.d.ts.map +1 -1
  48. package/dist/src/types/trace.d.ts +32 -0
  49. package/dist/src/types/trace.d.ts.map +1 -1
  50. package/dist/test/channel-registry-routing.test.js +186 -10
  51. package/dist/test/channel-registry-routing.test.js.map +1 -1
  52. package/dist/test/code-execution.test.d.ts +11 -0
  53. package/dist/test/code-execution.test.d.ts.map +1 -0
  54. package/dist/test/code-execution.test.js +412 -0
  55. package/dist/test/code-execution.test.js.map +1 -0
  56. package/dist/test/explicit-prose-routing.test.d.ts +11 -0
  57. package/dist/test/explicit-prose-routing.test.d.ts.map +1 -0
  58. package/dist/test/explicit-prose-routing.test.js +247 -0
  59. package/dist/test/explicit-prose-routing.test.js.map +1 -0
  60. package/dist/test/present-while-acting.test.js +85 -8
  61. package/dist/test/present-while-acting.test.js.map +1 -1
  62. package/dist/test/prose-stream-router.test.d.ts +2 -0
  63. package/dist/test/prose-stream-router.test.d.ts.map +1 -0
  64. package/dist/test/prose-stream-router.test.js +87 -0
  65. package/dist/test/prose-stream-router.test.js.map +1 -0
  66. package/dist/test/trunk-channel-routing.test.js +2 -2
  67. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +4 -4
  70. package/src/agent.ts +3 -0
  71. package/src/code-execution/index.ts +19 -0
  72. package/src/code-execution/py-runner.ts +433 -0
  73. package/src/code-execution/runtime-py.ts +265 -0
  74. package/src/code-execution/tool-definition.ts +52 -0
  75. package/src/framework.ts +1160 -107
  76. package/src/index.ts +17 -0
  77. package/src/mcpl/channel-registry.ts +454 -54
  78. package/src/mcpl/prose-grammar.ts +40 -0
  79. package/src/mcpl/prose-stream-router.ts +161 -0
  80. package/src/modules/workspace/index.ts +13 -4
  81. package/src/types/agent.ts +18 -0
  82. package/src/types/framework.ts +35 -0
  83. package/src/types/trace.ts +47 -1
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Python runtime for client-side programmatic tool calling (PTC).
3
+ *
4
+ * This is the interpreter-side half of the `code_execution` tool: a small
5
+ * asyncio REPL that mimics the environment Anthropic's server-side PTC was
6
+ * trained on (see docs/en/agents-and-tools/tool-use/programmatic-tool-calling):
7
+ *
8
+ * - scripts run with top-level `await` (PyCF_ALLOW_TOP_LEVEL_AWAIT)
9
+ * - host tools are injected as async functions taking a single dict and
10
+ * returning a string (the tool_result text)
11
+ * - a pending tool call that gets no response raises TimeoutError with the
12
+ * same message shape the managed runtime produces
13
+ * - interpreter globals persist across execs (container-reuse semantics)
14
+ *
15
+ * Embedded as a TS string because tsc does not copy non-TS assets to dist/;
16
+ * PyRunner materializes it to a temp file at spawn.
17
+ *
18
+ * Protocol (JSON lines):
19
+ * host -> py : {op:"init", tools:[{py_name,tool_name}], call_timeout_s}
20
+ * {op:"exec", id, code}
21
+ * {op:"tool_result", id, result}
22
+ * {op:"cancel", id, reason?}
23
+ * py -> host : {op:"ready"}
24
+ * {op:"tool_call", id, exec_id, name, args}
25
+ * {op:"exec_result", id, stdout, stderr, return_code}
26
+ *
27
+ * IMPORTANT: the python source below must not contain backticks or the
28
+ * sequence dollar+brace (TS template literal syntax). String.raw preserves
29
+ * backslashes, so \n inside python string literals is fine.
30
+ */
31
+
32
+ export const PYTHON_RUNTIME_SOURCE: string = String.raw`
33
+ import ast
34
+ import asyncio
35
+ import inspect
36
+ import io
37
+ import json
38
+ import sys
39
+ import threading
40
+ import traceback
41
+
42
+ PROTO_OUT = sys.stdout # protocol channel (real stdout)
43
+ REAL_STDERR = sys.stderr # runtime diagnostics outside execs
44
+ CALL_TIMEOUT_S = 270.0
45
+ OUTPUT_CAP_CHARS = 2_000_000
46
+
47
+ _send_lock = threading.Lock()
48
+
49
+
50
+ def send(obj):
51
+ line = json.dumps(obj, ensure_ascii=False, default=str)
52
+ with _send_lock:
53
+ PROTO_OUT.write(line + "\n")
54
+ PROTO_OUT.flush()
55
+
56
+
57
+ class CappedIO(io.StringIO):
58
+ """StringIO that stops recording past a cap (keeps a truncation flag)."""
59
+
60
+ def __init__(self, cap):
61
+ super().__init__()
62
+ self._cap = cap
63
+ self.truncated = False
64
+
65
+ def write(self, s):
66
+ if not isinstance(s, str):
67
+ s = str(s)
68
+ if self.tell() >= self._cap:
69
+ self.truncated = True
70
+ return len(s)
71
+ remaining = self._cap - self.tell()
72
+ if len(s) > remaining:
73
+ self.truncated = True
74
+ return super().write(s[:remaining])
75
+ return super().write(s)
76
+
77
+
78
+ # Persistent interpreter state (mirrors container reuse: variables survive
79
+ # between exec calls until the host reclaims the interpreter).
80
+ SCRIPT_GLOBALS = {"__name__": "__main__", "__builtins__": __builtins__}
81
+ _injected_py_names = set()
82
+
83
+ _pending_tool_futures = {}
84
+ _next_tool_call_id = 0
85
+ _current_exec_id = None
86
+ _current_exec_task = None
87
+
88
+
89
+ def _make_tool_fn(tool_name, py_name):
90
+ async def tool_fn(args=None):
91
+ global _next_tool_call_id
92
+ if args is None:
93
+ args = {}
94
+ if not isinstance(args, dict):
95
+ raise TypeError(
96
+ "tool functions take a single dict of arguments, e.g. await "
97
+ + py_name + "({...})"
98
+ )
99
+ _next_tool_call_id += 1
100
+ call_id = "t" + str(_next_tool_call_id)
101
+ fut = asyncio.get_running_loop().create_future()
102
+ _pending_tool_futures[call_id] = fut
103
+ send({
104
+ "op": "tool_call",
105
+ "id": call_id,
106
+ "exec_id": _current_exec_id,
107
+ "name": tool_name,
108
+ "args": args,
109
+ })
110
+ try:
111
+ return await asyncio.wait_for(fut, timeout=CALL_TIMEOUT_S)
112
+ except asyncio.TimeoutError:
113
+ raise TimeoutError(
114
+ "Calling tool ['" + tool_name + "'] timed out (no response after "
115
+ + str(int(CALL_TIMEOUT_S)) + "s)."
116
+ )
117
+ finally:
118
+ _pending_tool_futures.pop(call_id, None)
119
+
120
+ tool_fn.__name__ = py_name
121
+ tool_fn.__qualname__ = py_name
122
+ tool_fn.__doc__ = (
123
+ "Host tool '" + tool_name + "'. Takes a single dict of arguments and "
124
+ "returns the tool result as a string."
125
+ )
126
+ return tool_fn
127
+
128
+
129
+ def handle_init(msg):
130
+ global CALL_TIMEOUT_S
131
+ timeout = msg.get("call_timeout_s")
132
+ if isinstance(timeout, (int, float)) and timeout > 0:
133
+ CALL_TIMEOUT_S = float(timeout)
134
+
135
+ # Remove functions injected by a previous init that are no longer present
136
+ # (tool list changed) without touching user-defined globals.
137
+ new_tools = msg.get("tools") or []
138
+ new_py_names = set()
139
+ tools_dict = {}
140
+ for entry in new_tools:
141
+ py_name = entry.get("py_name")
142
+ tool_name = entry.get("tool_name")
143
+ if not py_name or not tool_name:
144
+ continue
145
+ fn = _make_tool_fn(tool_name, py_name)
146
+ SCRIPT_GLOBALS[py_name] = fn
147
+ tools_dict[tool_name] = fn
148
+ new_py_names.add(py_name)
149
+ for stale in _injected_py_names - new_py_names:
150
+ SCRIPT_GLOBALS.pop(stale, None)
151
+ _injected_py_names.clear()
152
+ _injected_py_names.update(new_py_names)
153
+ # Exact-name escape hatch: tools["mcpl--server--tool"]({...})
154
+ SCRIPT_GLOBALS["tools"] = tools_dict
155
+
156
+
157
+ async def _run_script(exec_id, code):
158
+ global _current_exec_id, _current_exec_task
159
+ out = CappedIO(OUTPUT_CAP_CHARS)
160
+ err = CappedIO(OUTPUT_CAP_CHARS)
161
+ old_out, old_err, old_in = sys.stdout, sys.stderr, sys.stdin
162
+ sys.stdout, sys.stderr = out, err
163
+ sys.stdin = io.StringIO("") # input() must not steal the protocol channel
164
+ return_code = 0
165
+ try:
166
+ compiled = compile(code, "<script>", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
167
+ result = eval(compiled, SCRIPT_GLOBALS)
168
+ if inspect.iscoroutine(result):
169
+ await result
170
+ except asyncio.CancelledError:
171
+ err.write("\nKeyboardInterrupt: script cancelled by host\n")
172
+ return_code = 1
173
+ except BaseException:
174
+ traceback.print_exc(file=err)
175
+ return_code = 1
176
+ finally:
177
+ sys.stdout, sys.stderr, sys.stdin = old_out, old_err, old_in
178
+ _current_exec_id = None
179
+ _current_exec_task = None
180
+ # Fail any tool futures still pending: their exec is gone.
181
+ for fut in list(_pending_tool_futures.values()):
182
+ if not fut.done():
183
+ fut.cancel()
184
+ _pending_tool_futures.clear()
185
+
186
+ stdout_text = out.getvalue()
187
+ if out.truncated:
188
+ stdout_text += "\n[stdout truncated at " + str(OUTPUT_CAP_CHARS) + " chars]"
189
+ stderr_text = err.getvalue()
190
+ if err.truncated:
191
+ stderr_text += "\n[stderr truncated at " + str(OUTPUT_CAP_CHARS) + " chars]"
192
+ send({
193
+ "op": "exec_result",
194
+ "id": exec_id,
195
+ "stdout": stdout_text,
196
+ "stderr": stderr_text,
197
+ "return_code": return_code,
198
+ })
199
+
200
+
201
+ async def main():
202
+ global _current_exec_id, _current_exec_task
203
+ loop = asyncio.get_running_loop()
204
+ queue = asyncio.Queue()
205
+
206
+ def reader():
207
+ for raw_line in sys.stdin:
208
+ loop.call_soon_threadsafe(queue.put_nowait, raw_line)
209
+ loop.call_soon_threadsafe(queue.put_nowait, None)
210
+
211
+ threading.Thread(target=reader, daemon=True).start()
212
+ send({"op": "ready"})
213
+
214
+ while True:
215
+ line = await queue.get()
216
+ if line is None:
217
+ break
218
+ line = line.strip()
219
+ if not line:
220
+ continue
221
+ try:
222
+ msg = json.loads(line)
223
+ except Exception:
224
+ REAL_STDERR.write("[pytc-runtime] bad protocol line\n")
225
+ REAL_STDERR.flush()
226
+ continue
227
+ op = msg.get("op")
228
+ if op == "init":
229
+ handle_init(msg)
230
+ elif op == "exec":
231
+ if _current_exec_task is not None and not _current_exec_task.done():
232
+ send({
233
+ "op": "exec_result",
234
+ "id": msg.get("id"),
235
+ "stdout": "",
236
+ "stderr": "RuntimeError: another script is already running in this interpreter",
237
+ "return_code": 1,
238
+ })
239
+ continue
240
+ _current_exec_id = msg.get("id")
241
+ _current_exec_task = asyncio.ensure_future(
242
+ _run_script(msg.get("id"), msg.get("code") or "")
243
+ )
244
+ elif op == "tool_result":
245
+ fut = _pending_tool_futures.get(msg.get("id"))
246
+ if fut is not None and not fut.done():
247
+ fut.set_result(str(msg.get("result", "")))
248
+ elif op == "cancel":
249
+ if _current_exec_task is not None and not _current_exec_task.done():
250
+ _current_exec_task.cancel()
251
+ elif op == "exit":
252
+ break
253
+
254
+ # Drain: give a cancelled exec a moment to emit its exec_result.
255
+ if _current_exec_task is not None and not _current_exec_task.done():
256
+ _current_exec_task.cancel()
257
+ try:
258
+ await asyncio.wait_for(_current_exec_task, timeout=2)
259
+ except BaseException:
260
+ pass
261
+
262
+
263
+ if __name__ == "__main__":
264
+ asyncio.run(main())
265
+ `;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Synthesized `code_execution` tool definition.
3
+ *
4
+ * The name, input shape ({code}), and description framing deliberately track
5
+ * Anthropic's server-side programmatic tool calling so models' trained
6
+ * priors transfer: python scripts, tools as async functions taking a single
7
+ * dict and returning a string, stdout as the return channel, persistent
8
+ * interpreter state, asyncio.gather for fan-out.
9
+ */
10
+
11
+ import type { ToolDefinition } from '../types/index.js';
12
+
13
+ export const CODE_EXECUTION_TOOL_NAME = 'code_execution';
14
+
15
+ export function buildCodeExecutionToolDefinition(opts?: {
16
+ idleReclaimMs?: number;
17
+ toolCallTimeoutMs?: number;
18
+ }): ToolDefinition {
19
+ const idleMinutes = Math.max(1, Math.round((opts?.idleReclaimMs ?? 300_000) / 60_000));
20
+ const callTimeoutSeconds = Math.round((opts?.toolCallTimeoutMs ?? 270_000) / 1000);
21
+ return {
22
+ name: CODE_EXECUTION_TOOL_NAME,
23
+ description:
24
+ 'Run Python code that can call your other tools programmatically. ' +
25
+ 'Every tool you have is available inside the script as an async Python function: ' +
26
+ "the function name is the tool name with '--' replaced by '__' " +
27
+ "(e.g. tool 'mcpl--discord--fetch_history' is the function mcpl__discord__fetch_history). " +
28
+ 'Each function takes a single dict of arguments and returns a string — the same text ' +
29
+ 'the tool would have returned to you directly; parse structured results with json.loads. ' +
30
+ "An exact-name lookup dict is also available: tools['mcpl--discord--fetch_history']({...}). " +
31
+ 'Use top-level await; run independent calls in parallel with asyncio.gather. ' +
32
+ 'Only what you print() (plus stderr and the exit code) comes back to you — intermediate ' +
33
+ 'tool results stay in the script, so filter or aggregate large data there and print only ' +
34
+ 'what you need. Interpreter state (variables, imports) persists across code_execution ' +
35
+ `calls but is reclaimed after ~${idleMinutes} minutes idle. A tool call that receives no ` +
36
+ `response within ~${callTimeoutSeconds}s raises TimeoutError inside the script. ` +
37
+ 'Use this when fanning out across many items, looping over tool calls, or when tool ' +
38
+ 'results are large and you only need a slice or summary. Call tools directly (not via ' +
39
+ 'code) when a single call answers the question or when you need to reason about each ' +
40
+ 'result before deciding the next step.',
41
+ inputSchema: {
42
+ type: 'object' as const,
43
+ properties: {
44
+ code: {
45
+ type: 'string',
46
+ description: 'Python code to execute. Top-level await is allowed.',
47
+ },
48
+ },
49
+ required: ['code'],
50
+ },
51
+ };
52
+ }