framework-agents 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.
Files changed (41) hide show
  1. framework_agents/__init__.py +345 -0
  2. framework_agents/a2a_client.py +360 -0
  3. framework_agents/a2a_server.py +495 -0
  4. framework_agents/agent.py +920 -0
  5. framework_agents/checkpoint.py +231 -0
  6. framework_agents/config.py +200 -0
  7. framework_agents/conversation.py +249 -0
  8. framework_agents/errors.py +47 -0
  9. framework_agents/evals.py +127 -0
  10. framework_agents/mcp_client.py +477 -0
  11. framework_agents/memory.py +103 -0
  12. framework_agents/memory_compaction.py +63 -0
  13. framework_agents/middleware.py +1302 -0
  14. framework_agents/models/__init__.py +100 -0
  15. framework_agents/models/anthropic.py +339 -0
  16. framework_agents/models/capabilities.py +45 -0
  17. framework_agents/models/ollama.py +47 -0
  18. framework_agents/models/openai_compatible.py +409 -0
  19. framework_agents/observability.py +84 -0
  20. framework_agents/ontology_review.py +321 -0
  21. framework_agents/orchestration.py +169 -0
  22. framework_agents/pricing.py +63 -0
  23. framework_agents/prompts.py +132 -0
  24. framework_agents/rag.py +123 -0
  25. framework_agents/remote_sparql.py +213 -0
  26. framework_agents/runtime.py +88 -0
  27. framework_agents/sandbox.py +327 -0
  28. framework_agents/semantic.py +1614 -0
  29. framework_agents/serve.py +436 -0
  30. framework_agents/sessions.py +249 -0
  31. framework_agents/skills.py +213 -0
  32. framework_agents/state.py +89 -0
  33. framework_agents/streaming.py +74 -0
  34. framework_agents/swarm.py +318 -0
  35. framework_agents/tools.py +114 -0
  36. framework_agents/tracing.py +131 -0
  37. framework_agents/types.py +262 -0
  38. framework_agents/workflows.py +130 -0
  39. framework_agents-0.1.0.dist-info/METADATA +118 -0
  40. framework_agents-0.1.0.dist-info/RECORD +41 -0
  41. framework_agents-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,327 @@
1
+ """Sandboxed code execution tools. Parity: src/sandbox/index.ts.
2
+
3
+ - run_javascript: exec in a restricted namespace (limited builtins — no
4
+ import/open/eval/process access), timeout enforced via asyncio.wait_for
5
+ cancel semantics, output = last expression value + printed output,
6
+ truncated.
7
+ - create_sandbox_backend: one rooted workspace with a shell `execute` tool
8
+ (cwd=root, scrubbed env — no secrets/proxies inherited, timeout, output
9
+ caps enforced INCREMENTALLY — the child is killed the moment accumulated
10
+ output crosses max_buffer, TS kills at 4MB — with a spill note).
11
+
12
+ Isolation, honestly scoped: this is a speed bump, NOT a (container) security
13
+ boundary. Never put secrets in the sandbox env, gate `execute` with
14
+ human-in-the-loop for sensitive work, treat outputs as untrusted.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import ast
21
+ import io
22
+ import json
23
+ import math
24
+ import os
25
+ import threading
26
+ from typing import Any, Optional
27
+
28
+ from pydantic import BaseModel, Field
29
+
30
+ from .errors import AgentError
31
+ from .tools import Tool, ToolContext, define_tool
32
+
33
+ SCRUBBED_ENV_KEYS = ("PATH", "HOME", "LANG", "LC_ALL", "TZ", "TERM", "NO_COLOR")
34
+
35
+ # Safe subset only — no open/exec/eval/__import__/getattr/globals.
36
+ _SAFE_BUILTINS: dict[str, Any] = {
37
+ "abs": abs,
38
+ "all": all,
39
+ "any": any,
40
+ "bool": bool,
41
+ "bytes": bytes,
42
+ "chr": chr,
43
+ "dict": dict,
44
+ "divmod": divmod,
45
+ "enumerate": enumerate,
46
+ "filter": filter,
47
+ "float": float,
48
+ "frozenset": frozenset,
49
+ "hash": hash,
50
+ "hex": hex,
51
+ "int": int,
52
+ "isinstance": isinstance,
53
+ "len": len,
54
+ "list": list,
55
+ "map": map,
56
+ "max": max,
57
+ "min": min,
58
+ "oct": oct,
59
+ "ord": ord,
60
+ "pow": pow,
61
+ "range": range,
62
+ "repr": repr,
63
+ "reversed": reversed,
64
+ "round": round,
65
+ "set": set,
66
+ "sorted": sorted,
67
+ "str": str,
68
+ "sum": sum,
69
+ "tuple": tuple,
70
+ "zip": zip,
71
+ }
72
+
73
+
74
+ class _JavascriptArgs(BaseModel):
75
+ code: str = Field(min_length=1, max_length=8000)
76
+
77
+
78
+ class _ExecuteArgs(BaseModel):
79
+ command: str = Field(min_length=1, max_length=4000)
80
+
81
+
82
+ def _sleep(seconds: float) -> None:
83
+ """Blocking sleep for sandboxed code (blocks only the sandbox thread)."""
84
+ threading.Event().wait(max(0.0, float(seconds)))
85
+
86
+
87
+ def _exec_in_sandbox(code: str) -> tuple[str, Any]:
88
+ """Run code in the restricted namespace -> (printed output, result value)."""
89
+ buffer = io.StringIO()
90
+
91
+ def sandbox_print(*args: Any, sep: str = " ", end: str = "\n", file: Any = None, flush: bool = False) -> None:
92
+ print(*args, sep=sep, end=end, file=buffer, flush=flush)
93
+
94
+ namespace: dict[str, Any] = {
95
+ "__builtins__": _SAFE_BUILTINS,
96
+ "print": sandbox_print,
97
+ "sleep": _sleep,
98
+ "math": math,
99
+ "json": json,
100
+ }
101
+ tree = ast.parse(code, mode="exec")
102
+ if tree.body and isinstance(tree.body[-1], ast.Expr):
103
+ # Completion-value semantics (JS parity): the last expression
104
+ # statement becomes the result.
105
+ if len(tree.body) > 1:
106
+ exec(compile(ast.Module(body=tree.body[:-1], type_ignores=[]), "<sandbox>", "exec"), namespace) # noqa: S102
107
+ result = eval(compile(ast.Expression(tree.body[-1].value), "<sandbox>", "eval"), namespace) # noqa: S307
108
+ else:
109
+ exec(compile(tree, "<sandbox>", "exec"), namespace) # noqa: S102
110
+ result = None
111
+ return buffer.getvalue(), result
112
+
113
+
114
+ async def _run_javascript(code: str, timeout_ms: float, max_output_chars: int) -> str:
115
+ """Exec in a daemon worker thread; asyncio.wait_for enforces the timeout.
116
+
117
+ Python cannot interrupt exec mid-statement, so a timeout ABANDONS the
118
+ worker (daemon — it cannot keep the process alive) and raises
119
+ SANDBOX_TIMEOUT, mirroring the TS vm timeout contract.
120
+ """
121
+
122
+ def unpacked() -> str:
123
+ printed, result = _exec_in_sandbox(code)
124
+ pieces: list[str] = []
125
+ if printed.rstrip("\n"):
126
+ pieces.append(printed.rstrip("\n"))
127
+ if result is not None:
128
+ rendered = result if isinstance(result, str) else json.dumps(result, default=str)
129
+ pieces.append(rendered)
130
+ out = "\n".join(pieces)
131
+ if len(out) > max_output_chars:
132
+ out = out[:max_output_chars] + "…[truncated]"
133
+ return out
134
+
135
+ box: dict[str, Any] = {}
136
+ done = threading.Event()
137
+
138
+ def work() -> None:
139
+ try:
140
+ box["value"] = unpacked()
141
+ except BaseException as err: # noqa: BLE001 - surfaced on the caller side
142
+ box["error"] = err
143
+ finally:
144
+ done.set()
145
+
146
+ worker = threading.Thread(target=work, daemon=True, name="sandbox-javascript")
147
+ worker.start()
148
+
149
+ async def await_done() -> None:
150
+ while not done.is_set():
151
+ await asyncio.sleep(0.005)
152
+
153
+ try:
154
+ await asyncio.wait_for(await_done(), max(timeout_ms, 1) / 1000)
155
+ except (asyncio.TimeoutError, TimeoutError):
156
+ raise AgentError("SANDBOX_TIMEOUT", f"Sandbox exceeded {timeout_ms}ms", retryable=True)
157
+ if "error" in box:
158
+ raise AgentError("SANDBOX_ERROR", f"Sandbox failed: {box['error']}", retryable=False) from box["error"]
159
+ return box["value"]
160
+
161
+
162
+ def run_javascript_tool(timeout_ms: float = 2000, max_output_chars: int = 4000) -> Tool:
163
+ """`run_javascript` Tool: sandboxed exec, no network/fs, timeout, caps."""
164
+
165
+ async def execute(args: Any, ctx: Optional[ToolContext]) -> str:
166
+ code = args.code if isinstance(args, _JavascriptArgs) else args["code"]
167
+ return await _run_javascript(code, timeout_ms, max_output_chars)
168
+
169
+ return define_tool(
170
+ name="run_javascript",
171
+ description=(
172
+ "Execute JavaScript-like code in a sandbox (no network, no fs, timeout). "
173
+ "The last expression's value becomes the result."
174
+ ),
175
+ schema=_JavascriptArgs,
176
+ execute=execute,
177
+ )
178
+
179
+
180
+ def _scrub_env(extra: Optional[dict[str, str]] = None) -> dict[str, str]:
181
+ env: dict[str, str] = {}
182
+ for key in SCRUBBED_ENV_KEYS:
183
+ value = os.environ.get(key)
184
+ if value is not None:
185
+ env[key] = value
186
+ env.update(extra or {})
187
+ return env
188
+
189
+
190
+ class _OutputOverflow(Exception):
191
+ """Internal: accumulated child output crossed ``max_buffer`` mid-run."""
192
+
193
+
194
+ class SandboxBackend:
195
+ """Local-first sandbox workspace: shell `execute` rooted at `root`."""
196
+
197
+ def __init__(
198
+ self,
199
+ root: str,
200
+ timeout_ms: float = 30000,
201
+ max_buffer: int = 4 * 1024 * 1024,
202
+ max_output_chars: int = 12000,
203
+ env: Optional[dict[str, str]] = None,
204
+ ) -> None:
205
+ self.root = os.path.abspath(root)
206
+ self.timeout_ms = timeout_ms
207
+ self.max_buffer = max_buffer
208
+ self.max_output_chars = max_output_chars
209
+ self._env = _scrub_env(env)
210
+ self._spills = 0
211
+
212
+ def tools(self) -> list[Tool]:
213
+ return [self._execute_tool()]
214
+
215
+ def _spill(self, text: str) -> str:
216
+ self._spills += 1
217
+ name = f".spill-{self._spills}.txt"
218
+ with open(os.path.join(self.root, name), "w", encoding="utf-8") as handle:
219
+ handle.write(text)
220
+ return f"{text[: self.max_output_chars]}\n…[truncated {len(text) - self.max_output_chars} chars → read {name}]"
221
+
222
+ @staticmethod
223
+ def _kill(proc: asyncio.subprocess.Process) -> None:
224
+ try:
225
+ proc.kill()
226
+ except ProcessLookupError: # pragma: no cover - already dead
227
+ pass
228
+
229
+ async def _drain_and_wait(self, proc: asyncio.subprocess.Process) -> tuple[bytes, bytes]:
230
+ """Read the child's output INCREMENTALLY (both pipes concurrently),
231
+ aborting at ``max_buffer`` MID-STREAM — unlike communicate(), which
232
+ buffers everything (a `yes` loop would fill RAM until the timeout).
233
+ The overflow abort cancels the sibling pump through the TaskGroup."""
234
+ total = 0
235
+ out: list[bytes] = []
236
+ err: list[bytes] = []
237
+
238
+ async def pump(bucket: list[bytes], stream: Optional[asyncio.StreamReader]) -> None:
239
+ nonlocal total
240
+ assert stream is not None # both pipes are PIPE in _run_command
241
+ while True:
242
+ chunk = await stream.read(64 * 1024)
243
+ if not chunk:
244
+ return
245
+ bucket.append(chunk)
246
+ total += len(chunk)
247
+ if total > self.max_buffer:
248
+ raise _OutputOverflow
249
+
250
+ try:
251
+ async with asyncio.TaskGroup() as tg:
252
+ tg.create_task(pump(out, proc.stdout))
253
+ tg.create_task(pump(err, proc.stderr))
254
+ await proc.wait()
255
+ except ExceptionGroup as eg:
256
+ # Both pipes can cross the cap in one event-loop pass; unwrap an
257
+ # overflow-only group so _run_command sees the bare signal.
258
+ overflows = [e for e in eg.exceptions if isinstance(e, _OutputOverflow)]
259
+ if len(overflows) == len(eg.exceptions):
260
+ raise (overflows[0] if len(overflows) == 1 else _OutputOverflow()) from eg
261
+ raise
262
+ return b"".join(out), b"".join(err)
263
+
264
+ async def _run_command(self, command: str) -> str:
265
+ try:
266
+ proc = await asyncio.create_subprocess_shell(
267
+ command,
268
+ cwd=self.root,
269
+ env=self._env,
270
+ stdout=asyncio.subprocess.PIPE,
271
+ stderr=asyncio.subprocess.PIPE,
272
+ )
273
+ except OSError as err: # unusable cwd / shell
274
+ raise AgentError("SANDBOX_ERROR", f"Cannot run command: {err}", retryable=False) from err
275
+ try:
276
+ stdout, stderr = await asyncio.wait_for(self._drain_and_wait(proc), self.timeout_ms / 1000)
277
+ except _OutputOverflow:
278
+ # The cap fires DURING the run: kill the child immediately (TS
279
+ # kills at 4MB) instead of buffering until the timeout.
280
+ self._kill(proc)
281
+ await proc.wait()
282
+ raise AgentError(
283
+ "SANDBOX_OUTPUT_TOO_LARGE",
284
+ "Command output exceeded the buffer; write output to a file and read it in chunks",
285
+ retryable=False,
286
+ )
287
+ except (asyncio.TimeoutError, TimeoutError):
288
+ self._kill(proc)
289
+ await proc.wait()
290
+ raise AgentError("SANDBOX_TIMEOUT", f"Command exceeded {self.timeout_ms}ms", retryable=True)
291
+ code = proc.returncode or 0
292
+ text = stdout.decode("utf-8", errors="replace")
293
+ if stderr:
294
+ text += f"\n[stderr]\n{stderr.decode('utf-8', errors='replace')}"
295
+ if len(text) > self.max_output_chars:
296
+ text = self._spill(text)
297
+ if code != 0:
298
+ text += f"\n[Command failed with exit code {code}]"
299
+ return text or "[Command succeeded with exit code 0]"
300
+
301
+ def _execute_tool(self) -> Tool:
302
+ backend = self
303
+
304
+ async def execute(args: Any, ctx: Optional[ToolContext]) -> str:
305
+ command = args.command if isinstance(args, _ExecuteArgs) else args["command"]
306
+ return await backend._run_command(command)
307
+
308
+ return define_tool(
309
+ name="execute",
310
+ description=(
311
+ "Run a shell command in the sandbox workspace (cwd=root, scrubbed env, "
312
+ "timeout). Large output spills to a file."
313
+ ),
314
+ schema=_ExecuteArgs,
315
+ execute=execute,
316
+ )
317
+
318
+
319
+ def create_sandbox_backend(
320
+ root: str,
321
+ timeout_ms: float = 30000,
322
+ max_buffer: int = 4 * 1024 * 1024,
323
+ max_output_chars: int = 12000,
324
+ env: Optional[dict[str, str]] = None,
325
+ ) -> SandboxBackend:
326
+ """Sandbox backend (execute tool) rooted at `root` (shell cwd jail)."""
327
+ return SandboxBackend(root=root, timeout_ms=timeout_ms, max_buffer=max_buffer, max_output_chars=max_output_chars, env=env)