AbstractAgent 0.3.17__tar.gz → 0.3.19__tar.gz

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 (129) hide show
  1. {abstractagent-0.3.17/src/abstractagent.egg-info → abstractagent-0.3.19}/PKG-INFO +4 -4
  2. {abstractagent-0.3.17 → abstractagent-0.3.19}/pyproject.toml +4 -4
  3. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/emit_inventory.py +6 -0
  4. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/react_runtime.py +34 -0
  5. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/sandbox/local.py +16 -2
  6. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/tools/code_execution.py +42 -5
  7. {abstractagent-0.3.17 → abstractagent-0.3.19/src/abstractagent.egg-info}/PKG-INFO +4 -4
  8. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent.egg-info/SOURCES.txt +2 -0
  9. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent.egg-info/requires.txt +3 -3
  10. abstractagent-0.3.19/tests/test_execute_python_sandbox.py +121 -0
  11. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_packaging_profiles.py +3 -3
  12. abstractagent-0.3.19/tests/test_react_tool_format_retry.py +106 -0
  13. {abstractagent-0.3.17 → abstractagent-0.3.19}/LICENSE +0 -0
  14. {abstractagent-0.3.17 → abstractagent-0.3.19}/README.md +0 -0
  15. {abstractagent-0.3.17 → abstractagent-0.3.19}/setup.cfg +0 -0
  16. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/__init__.py +0 -0
  17. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/__init__.py +0 -0
  18. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/codeact_runtime.py +0 -0
  19. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/generation_params.py +0 -0
  20. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/loop_hooks.py +0 -0
  21. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/media.py +0 -0
  22. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/memact_runtime.py +0 -0
  23. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/native_loop_registry.py +0 -0
  24. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/progress.py +0 -0
  25. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/read_orchestration.py +0 -0
  26. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/tool_allowlist.py +0 -0
  27. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/tool_failure_hints.py +0 -0
  28. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/adapters/transcripts.py +0 -0
  29. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/__init__.py +0 -0
  30. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/base.py +0 -0
  31. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/codeact.py +0 -0
  32. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/defaults.py +0 -0
  33. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/memact.py +0 -0
  34. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/react.py +0 -0
  35. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/task_reset.py +0 -0
  36. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/agents/unattended.py +0 -0
  37. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/logic/__init__.py +0 -0
  38. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/logic/builtins.py +0 -0
  39. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/logic/codeact.py +0 -0
  40. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/logic/memact.py +0 -0
  41. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/logic/react.py +0 -0
  42. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/logic/types.py +0 -0
  43. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/metrics/__init__.py +0 -0
  44. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/metrics/prefix_reuse.py +0 -0
  45. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/repl.py +0 -0
  46. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/sandbox/__init__.py +0 -0
  47. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/sandbox/interface.py +0 -0
  48. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/scripts/__init__.py +0 -0
  49. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/scripts/lmstudio_tool_eval.py +0 -0
  50. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/tools/__init__.py +0 -0
  51. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent/tools/self_improve.py +0 -0
  52. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent.egg-info/dependency_links.txt +0 -0
  53. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent.egg-info/entry_points.txt +0 -0
  54. {abstractagent-0.3.17 → abstractagent-0.3.19}/src/abstractagent.egg-info/top_level.txt +0 -0
  55. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_active_context_policy_adoption.py +0 -0
  56. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_adversary_hardening_2026_07_13.py +0 -0
  57. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_agent_plan_review_modes.py +0 -0
  58. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_agent_speculation_inheritance.py +0 -0
  59. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_agent_stream_inheritance.py +0 -0
  60. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_agents_integration_llm.py +0 -0
  61. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_base_agent_trace_accessors.py +0 -0
  62. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_batching_prompt_rule.py +0 -0
  63. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_budget_triage_tail.py +0 -0
  64. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_builtin_tool_specs_optional_defaults.py +0 -0
  65. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_c2447_visit_lane_honesty.py +0 -0
  66. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_claim_0029_residue_2026_07_14.py +0 -0
  67. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_codeact_logic.py +0 -0
  68. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_codeact_runtime_verifier_policy.py +0 -0
  69. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_delegate_session_route_composition.py +0 -0
  70. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_emit_inventory_drift.py +0 -0
  71. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_gated_remainder_wave_2026_07_14.py +0 -0
  72. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_generation_params_media_policies.py +0 -0
  73. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_history_monster_clamp.py +0 -0
  74. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_improvement_wave_2026_07_13.py +0 -0
  75. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_llm_call_payload_requires_prompt_or_messages.py +0 -0
  76. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_lmstudio_tool_eval_metrics.py +0 -0
  77. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_loop_hooks.py +0 -0
  78. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_loop_hooks_0026_follow_ups.py +0 -0
  79. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_memact_codeact_guidance_durable_transcript.py +0 -0
  80. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_memact_session_memory.py +0 -0
  81. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_memact_workflow.py +0 -0
  82. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_memory_recall_tool.py +0 -0
  83. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_native_loop_registry.py +0 -0
  84. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_open_attachment_tool_available.py +0 -0
  85. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_operator_conclude.py +0 -0
  86. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_optimal_wave_batch2_2026_07_13.py +0 -0
  87. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_parse_prompt_cache_capture.py +0 -0
  88. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_prefix_reuse_metric.py +0 -0
  89. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_progress_detection.py +0 -0
  90. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_act_only_observation.py +0 -0
  91. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_active_attachments_disable_tools_first_iteration.py +0 -0
  92. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_adapter_forwards_context_attachments_media.py +0 -0
  93. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_delegate_agent_tool.py +0 -0
  94. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_election_fence_conformance.py +0 -0
  95. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_entity_dress_conformance.py +0 -0
  96. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_guidance_durable_transcript.py +0 -0
  97. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_history_window.py +0 -0
  98. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_logic.py +0 -0
  99. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_loop_aborted_generation_retry.py +0 -0
  100. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_loop_context_transcript_levels.py +0 -0
  101. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_loop_followthrough_retry.py +0 -0
  102. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_loop_side_effect_dedupe_comms.py +0 -0
  103. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_loop_truncated_output_retry.py +0 -0
  104. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_max_iterations_concludes.py +0 -0
  105. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_phase_grant_boundary.py +0 -0
  106. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_planish_heuristic.py +0 -0
  107. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_prompt_prefix_stability.py +0 -0
  108. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_sight_lane_media_fold.py +0 -0
  109. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_turn_byte_stability.py +0 -0
  110. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_verifier_and_plan.py +0 -0
  111. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_visit_composition.py +0 -0
  112. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_visit_merge.py +0 -0
  113. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_workflow_system_prompt_and_tools_override.py +0 -0
  114. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_react_workflow_toolset_refresh.py +0 -0
  115. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_read_orchestration_guidance.py +0 -0
  116. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_reasoning_first_citizen_agent.py +0 -0
  117. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_review_failure_containment.py +0 -0
  118. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_run_start_seeds_runtime_provider_model.py +0 -0
  119. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_session_memory.py +0 -0
  120. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_sibling_loop_defects_batch.py +0 -0
  121. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_sibling_strict_transcripts_0011.py +0 -0
  122. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_stuck_streak_termination.py +0 -0
  123. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_tool_failure_hints.py +0 -0
  124. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_tool_loop_prefix_extension_a3.py +0 -0
  125. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_tool_observe_emits_result_for_tui.py +0 -0
  126. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_tools_editing.py +0 -0
  127. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_unseeded_render_limits.py +0 -0
  128. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_verifier_execution_preference.py +0 -0
  129. {abstractagent-0.3.17 → abstractagent-0.3.19}/tests/test_verifier_schema_strict.py +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractagent
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: Agent implementations using AbstractRuntime and AbstractCore
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
- Requires-Dist: abstractcore[tools]>=2.13.41
8
+ Requires-Dist: abstractcore[tools]>=2.25.0
9
9
  Requires-Dist: abstractruntime>=0.7.0
10
10
  Provides-Extra: apple
11
- Requires-Dist: abstractcore[all-apple]>=2.13.41; extra == "apple"
11
+ Requires-Dist: abstractcore[all-apple]>=2.25.0; extra == "apple"
12
12
  Requires-Dist: abstractruntime[apple]>=0.7.0; extra == "apple"
13
13
  Provides-Extra: gpu
14
- Requires-Dist: abstractcore[all-gpu]>=2.13.41; extra == "gpu"
14
+ Requires-Dist: abstractcore[all-gpu]>=2.25.0; extra == "gpu"
15
15
  Requires-Dist: abstractruntime[gpu]>=0.7.0; extra == "gpu"
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -4,22 +4,22 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "abstractagent"
7
- version = "0.3.17"
7
+ version = "0.3.19"
8
8
  description = "Agent implementations using AbstractRuntime and AbstractCore"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "abstractcore[tools]>=2.13.41",
12
+ "abstractcore[tools]>=2.25.0",
13
13
  "abstractruntime>=0.7.0",
14
14
  ]
15
15
 
16
16
  [project.optional-dependencies]
17
17
  apple = [
18
- "abstractcore[all-apple]>=2.13.41",
18
+ "abstractcore[all-apple]>=2.25.0",
19
19
  "abstractruntime[apple]>=0.7.0",
20
20
  ]
21
21
  gpu = [
22
- "abstractcore[all-gpu]>=2.13.41",
22
+ "abstractcore[all-gpu]>=2.25.0",
23
23
  "abstractruntime[gpu]>=0.7.0",
24
24
  ]
25
25
  dev = [
@@ -85,6 +85,12 @@ REACT_STEPS = COMMON_STEPS | frozenset(
85
85
  "parse_retry_empty",
86
86
  "parse_retry_plan_only",
87
87
  "parse_retry_truncated",
88
+ # Tool-call format repair (2026-10-03, shipped round 14): AbstractCore
89
+ # reported `metadata.tool_call_error` (code invalid_tool_syntax |
90
+ # unavailable_tool) and accepted ZERO calls; the model is asked once
91
+ # more with the tool roster — payload {cycle, attempt, code}. At most
92
+ # two per turn; the third raises (the run fails with the sentence).
93
+ "parse_retry_tool_format",
88
94
  # 0017 work half (2026-07-21), nudge-then-stop (2026-08-21): fires on
89
95
  # every detection of a consecutive-identical or A-B-A-B tool-batch
90
96
  # streak; payload {kind: repeat|oscillation, span, key, answered,
@@ -1446,6 +1446,10 @@ def reset_react_turn(run_vars: Dict[str, Any]) -> None:
1446
1446
  # retry stash is the same lifecycle class.
1447
1447
  "pending_media",
1448
1448
  "last_call_media",
1449
+ # Tool-call format repair budget is per TURN (two retries each):
1450
+ # without this pop, two format failures anywhere in a conversation
1451
+ # made every later failing turn raise at once.
1452
+ "tool_format_retries",
1449
1453
  ):
1450
1454
  temp.pop(key, None)
1451
1455
 
@@ -2496,6 +2500,36 @@ def create_react_workflow(
2496
2500
  emit("parse_tool_calls", {"count": len(tool_calls)})
2497
2501
  return StepPlan(node_id="parse", next_node="act")
2498
2502
 
2503
+ # Core owns syntax recognition; the agent owns bounded recovery. Retry
2504
+ # only when zero calls were accepted, so a partial batch is never replayed.
2505
+ metadata = response.get("metadata") if isinstance(response, dict) else None
2506
+ tool_error = metadata.get("tool_call_error") if isinstance(metadata, dict) else None
2507
+ if isinstance(tool_error, dict) and tool_error.get("code") in {"invalid_tool_syntax", "unavailable_tool"}:
2508
+ retries = int(temp.get("tool_format_retries", 0) or 0)
2509
+ if retries >= 2 or cycle_i >= max_iterations:
2510
+ raise RuntimeError("Tool-call format repair failed: no executable tool calls after bounded retries.")
2511
+ specs = runtime_ns.get("tool_specs") or []
2512
+ names = [str(spec["name"]) for spec in specs if isinstance(spec, dict) and spec.get("name")]
2513
+ if not names:
2514
+ raise RuntimeError("The model attempted a tool call, but this run has no tools configured.")
2515
+ temp["tool_format_retries"] = retries + 1
2516
+ _push_inbox(
2517
+ runtime_ns,
2518
+ "Your previous tool call was rejected; nothing was executed. "
2519
+ "Use only the available tools: " + ", ".join(names) + ". "
2520
+ # No format of our own: the tool-calling format is the one the
2521
+ # request's instructions carry (native structured calls, or the
2522
+ # provider's prompted text format) — a hard-coded text syntax
2523
+ # here would teach a native tool-calling model the wrong one.
2524
+ "Follow the tool-calling format and argument schemas supplied in the instructions, "
2525
+ "with an actual available tool name and valid arguments. "
2526
+ "Emit the corrected call now, or explain why you cannot complete the task without it.",
2527
+ )
2528
+ emit("parse_retry_tool_format", {"cycle": cycle_i, "attempt": retries + 1, "code": tool_error["code"]})
2529
+ if last_call_media:
2530
+ temp["pending_media"] = last_call_media
2531
+ return StepPlan(node_id="parse", next_node="reason")
2532
+
2499
2533
  # If the model hit an output limit, treat the step as incomplete and continue.
2500
2534
  # Retry-nudge lane honesty (iteration-3 adversary P0, 2026-07-19 — the
2501
2535
  # c2447 chrome class at the PARSE boundary): these nudges ride the
@@ -11,7 +11,7 @@ import os
11
11
  import subprocess
12
12
  import sys
13
13
  import time
14
- from typing import Optional
14
+ from typing import Any, Optional
15
15
 
16
16
  from .interface import ExecutionResult
17
17
 
@@ -22,9 +22,14 @@ class LocalSandbox:
22
22
  *,
23
23
  cwd: Optional[str] = None,
24
24
  python_executable: Optional[str] = None,
25
+ command_sandbox: Any = None,
25
26
  ):
27
+ """`command_sandbox`: an AbstractCore `abstractcore.tools.sandbox.Sandbox` (round 12). When
28
+ given, the interpreter runs inside it (OS sandbox bound to the run's workspaces, the host's
29
+ scrubbed environment, a private TMPDIR); the caller refuses before when it `refuses`."""
26
30
  self._cwd = cwd or os.getcwd()
27
31
  self._python = python_executable or sys.executable
32
+ self._command_sandbox = command_sandbox
28
33
 
29
34
  def reset(self) -> None:
30
35
  # Stateless sandbox (new subprocess per call).
@@ -47,10 +52,19 @@ class LocalSandbox:
47
52
  except (TypeError, ValueError):
48
53
  t = 0.0
49
54
  eff_timeout = t if t > 0 else None
55
+ argv = [self._python, "-c", code]
56
+ env = None
57
+ box = self._command_sandbox
58
+ if box is not None:
59
+ if box.kind == "unsandboxed":
60
+ env = box.env_for()
61
+ else:
62
+ argv, env = box.wrap(argv, self._cwd)
50
63
  try:
51
64
  completed = subprocess.run(
52
- [self._python, "-c", code],
65
+ argv,
53
66
  cwd=self._cwd,
67
+ env=env,
54
68
  capture_output=True,
55
69
  text=True,
56
70
  timeout=eff_timeout,
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Optional
6
+
5
7
  from abstractcore.tools import tool
6
8
 
7
9
 
@@ -25,16 +27,30 @@ def _bound_output(text: str, *, max_chars: int) -> str:
25
27
  )
26
28
 
27
29
 
30
+ # Round 12: the one sentence when the installed AbstractCore has no command sandbox.
31
+ NO_CORE_SANDBOX = (
32
+ "Commands are refused: the installed AbstractCore has no command sandbox "
33
+ "(abstractcore.tools.sandbox, AbstractCore 2.25.0 or newer)."
34
+ )
35
+
36
+
28
37
  @tool(
29
38
  name="execute_python",
30
- description="Execute a Python snippet in a subprocess sandbox. Returns stdout/stderr/exit_code in full.",
39
+ description="Execute a Python snippet in a subprocess, sandboxed to this run's workspaces. Returns stdout/stderr/exit_code in full.",
31
40
  when_to_use="When you need to compute, inspect files, or transform data using Python code",
41
+ # Stamped by the host (AbstractRuntime) with the run's workspace set; never model-facing.
42
+ hide_args=["_sandbox"],
32
43
  )
33
- def execute_python(code: str, timeout_s: float = 0.0, max_output_chars: int = 0) -> dict:
44
+ def execute_python(code: str, timeout_s: float = 0.0, max_output_chars: int = 0, _sandbox: Optional[dict] = None) -> dict:
34
45
  """Execute Python code in a local subprocess.
35
46
 
36
47
  Notes:
37
- - This is a dev sandbox. It is not a hardened security boundary.
48
+ - Round 12: when the host stamps `_sandbox` (the run's effective workspace set), the
49
+ interpreter runs inside AbstractCore's OS sandbox (macOS sandbox-exec, Linux bwrap/Landlock),
50
+ starting in the run's private workspace with the host's scrubbed environment. No sandbox
51
+ on the host (or an AbstractCore without one) = refused (`error` carries the sentence,
52
+ nothing runs). A host that configured AbstractCore's command policy refuses an unstamped
53
+ call. Library use without a host policy runs as before.
38
54
  - `timeout_s <= 0` (the default) means NO timeout (ADR-0027: no low default
39
55
  timeouts on correctness-critical paths — a 10s default silently killed
40
56
  legitimate long computations and reported them as errors). Pass a positive
@@ -48,7 +64,24 @@ def execute_python(code: str, timeout_s: float = 0.0, max_output_chars: int = 0)
48
64
 
49
65
  from ..sandbox.local import LocalSandbox
50
66
 
51
- sandbox = LocalSandbox()
67
+ try:
68
+ from abstractcore.tools import sandbox as core_sandbox
69
+ except ImportError:
70
+ return {"stdout": "", "stderr": "", "exit_code": 126, "duration_ms": 0.0, "error": NO_CORE_SANDBOX, "success": False}
71
+ box = core_sandbox.sandbox_for_tool_call(_sandbox)
72
+ if box.refuses:
73
+ return {
74
+ "stdout": "",
75
+ "stderr": "",
76
+ "exit_code": 126,
77
+ "duration_ms": 0.0,
78
+ "error": box.refusal(),
79
+ "success": False,
80
+ "sandbox": box.describe(),
81
+ "sandbox_line": box.rendered_line(),
82
+ }
83
+ bound = box if (box.spec is not None or box.kind != core_sandbox.KIND_UNSANDBOXED) else None
84
+ sandbox = LocalSandbox(cwd=box.spec.private_workspace if box.spec is not None else None, command_sandbox=bound)
52
85
  #[WARNING:TIMEOUT] caller-supplied only; <= 0 = unlimited (ADR-0027)
53
86
  timeout = float(timeout_s)
54
87
  result = sandbox.execute(code, timeout_s=timeout if timeout > 0 else None)
@@ -56,11 +89,15 @@ def execute_python(code: str, timeout_s: float = 0.0, max_output_chars: int = 0)
56
89
  cap = int(max_output_chars)
57
90
  except (TypeError, ValueError):
58
91
  cap = 0
59
- return {
92
+ out = {
60
93
  "stdout": _bound_output(result.stdout, max_chars=cap),
61
94
  "stderr": _bound_output(result.stderr, max_chars=cap),
62
95
  "exit_code": int(result.exit_code),
63
96
  "duration_ms": float(result.duration_ms),
64
97
  "error": result.error,
65
98
  }
99
+ if bound is not None:
100
+ out["sandbox"] = bound.describe()
101
+ out["sandbox_line"] = bound.rendered_line()
102
+ return out
66
103
 
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractagent
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: Agent implementations using AbstractRuntime and AbstractCore
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
- Requires-Dist: abstractcore[tools]>=2.13.41
8
+ Requires-Dist: abstractcore[tools]>=2.25.0
9
9
  Requires-Dist: abstractruntime>=0.7.0
10
10
  Provides-Extra: apple
11
- Requires-Dist: abstractcore[all-apple]>=2.13.41; extra == "apple"
11
+ Requires-Dist: abstractcore[all-apple]>=2.25.0; extra == "apple"
12
12
  Requires-Dist: abstractruntime[apple]>=0.7.0; extra == "apple"
13
13
  Provides-Extra: gpu
14
- Requires-Dist: abstractcore[all-gpu]>=2.13.41; extra == "gpu"
14
+ Requires-Dist: abstractcore[all-gpu]>=2.25.0; extra == "gpu"
15
15
  Requires-Dist: abstractruntime[gpu]>=0.7.0; extra == "gpu"
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -63,6 +63,7 @@ tests/test_codeact_logic.py
63
63
  tests/test_codeact_runtime_verifier_policy.py
64
64
  tests/test_delegate_session_route_composition.py
65
65
  tests/test_emit_inventory_drift.py
66
+ tests/test_execute_python_sandbox.py
66
67
  tests/test_gated_remainder_wave_2026_07_14.py
67
68
  tests/test_generation_params_media_policies.py
68
69
  tests/test_history_monster_clamp.py
@@ -102,6 +103,7 @@ tests/test_react_phase_grant_boundary.py
102
103
  tests/test_react_planish_heuristic.py
103
104
  tests/test_react_prompt_prefix_stability.py
104
105
  tests/test_react_sight_lane_media_fold.py
106
+ tests/test_react_tool_format_retry.py
105
107
  tests/test_react_turn_byte_stability.py
106
108
  tests/test_react_verifier_and_plan.py
107
109
  tests/test_react_visit_composition.py
@@ -1,15 +1,15 @@
1
- abstractcore[tools]>=2.13.41
1
+ abstractcore[tools]>=2.25.0
2
2
  abstractruntime>=0.7.0
3
3
 
4
4
  [apple]
5
- abstractcore[all-apple]>=2.13.41
5
+ abstractcore[all-apple]>=2.25.0
6
6
  abstractruntime[apple]>=0.7.0
7
7
 
8
8
  [dev]
9
9
  pytest>=7.0
10
10
 
11
11
  [gpu]
12
- abstractcore[all-gpu]>=2.13.41
12
+ abstractcore[all-gpu]>=2.25.0
13
13
  abstractruntime[gpu]>=0.7.0
14
14
 
15
15
  [test]
@@ -0,0 +1,121 @@
1
+ """Round 12: execute_python runs inside AbstractCore's OS command sandbox.
2
+
3
+ Real macOS tests (sandbox-exec) with scratch folders and markers; the runtime path
4
+ (AbstractRuntime stamps `_sandbox` on the CodeAct tool call) is exercised end to end."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ from abstractcore.tools import sandbox as core_sandbox
15
+ from abstractcore.tools.sandbox import SandboxRow, SandboxSpec
16
+
17
+ from abstractagent.tools.code_execution import NO_CORE_SANDBOX, execute_python
18
+
19
+ MARK = "R12-AGENT-REFUSED-MARKER"
20
+
21
+ macos_only = pytest.mark.skipif(
22
+ sys.platform != "darwin" or not os.access(core_sandbox.SANDBOX_EXEC, os.X_OK),
23
+ reason="real sandbox test: this host is not macOS with /usr/bin/sandbox-exec (Linux backends are tested in abstractcore)",
24
+ )
25
+
26
+
27
+ @pytest.fixture(autouse=True)
28
+ def _fresh_host():
29
+ core_sandbox._reset_host_for_tests()
30
+ yield
31
+ core_sandbox._reset_host_for_tests()
32
+
33
+
34
+ @pytest.fixture
35
+ def t(tmp_path, monkeypatch):
36
+ root = Path(os.path.realpath(tmp_path))
37
+ for d in ("home", "priv", "refused", "rw"):
38
+ (root / d).mkdir()
39
+ (root / "refused/secret.txt").write_text(MARK)
40
+ monkeypatch.setenv("HOME", str(root / "home"))
41
+ return root
42
+
43
+
44
+ def _stamp(t: Path, refused=True):
45
+ return SandboxSpec(
46
+ private_workspace=str(t / "priv"),
47
+ posture="any_except_denied",
48
+ allowed=(SandboxRow(str(t / "rw"), "rw"),),
49
+ refused=(str(t / "refused"),) if refused else (),
50
+ ).to_stamp()
51
+
52
+
53
+ def test_sandbox_arg_is_hidden_from_the_model():
54
+ assert "_sandbox" not in execute_python._tool_definition.parameters
55
+
56
+
57
+ @macos_only
58
+ def test_refused_path_is_unreadable_from_python(t):
59
+ code = f"print(open({str(t / 'refused/secret.txt')!r}).read())"
60
+ res = execute_python(code, _sandbox=_stamp(t))
61
+ assert MARK not in res["stdout"] and res["exit_code"] != 0
62
+ assert "Operation not permitted" in res["stderr"]
63
+ assert res["sandbox"]["kind"] == "macos-sandbox-exec" and res["sandbox_line"] == "Sandbox: macOS sandbox-exec"
64
+ ok = execute_python(code, _sandbox=_stamp(t, refused=False))
65
+ assert MARK in ok["stdout"] # positive control
66
+
67
+
68
+ @macos_only
69
+ def test_starts_in_the_private_workspace_with_a_private_tmpdir(t):
70
+ res = execute_python("import os,tempfile;open('w.txt','w').write('x');print(os.getcwd());print(tempfile.gettempdir())", _sandbox=_stamp(t))
71
+ assert res["stdout"].split() == [str(t / "priv"), str(t / "priv/.tmp")]
72
+ assert (t / "priv/w.txt").exists()
73
+
74
+
75
+ def test_fail_closed_without_a_sandbox(t, monkeypatch):
76
+ monkeypatch.setattr(core_sandbox, "host_sandbox_kind", lambda posture="allowed_only": core_sandbox.KIND_NONE)
77
+ res = execute_python(f"open({str(t / 'priv/ran')!r},'w')", _sandbox=_stamp(t))
78
+ assert res["success"] is False and res["error"].startswith("Commands are not sandboxed on this gateway host")
79
+ assert not (t / "priv/ran").exists()
80
+
81
+
82
+ def test_fail_closed_without_core_sandbox_module(t, monkeypatch):
83
+ import abstractcore.tools as core_tools
84
+
85
+ monkeypatch.setitem(sys.modules, "abstractcore.tools.sandbox", None)
86
+ monkeypatch.delattr(core_tools, "sandbox", raising=False)
87
+ res = execute_python(f"open({str(t / 'priv/ran')!r},'w')", _sandbox=_stamp(t))
88
+ assert res["success"] is False and res["error"] == NO_CORE_SANDBOX
89
+ assert not (t / "priv/ran").exists()
90
+
91
+
92
+ def test_configured_host_refuses_unstamped_call(t):
93
+ core_sandbox.configure_host(env={"PATH": "/usr/bin:/bin"})
94
+ res = execute_python(f"open({str(t / 'priv/ran')!r},'w')")
95
+ assert res["success"] is False and res["error"] == core_sandbox.REFUSED_NO_SPEC
96
+ assert not (t / "priv/ran").exists()
97
+
98
+
99
+ @macos_only
100
+ def test_codeact_tool_call_through_the_runtime_is_sandboxed(t):
101
+ """A CodeAct-style TOOL_CALLS effect: the runtime stamps the run's workspaces on
102
+ execute_python; reading a refused workspace fails inside the sandbox."""
103
+ from abstractruntime.core.models import Effect, EffectType, RunState
104
+ from abstractruntime.integrations.abstractcore.effect_handlers import make_tool_calls_handler
105
+ from abstractruntime.integrations.abstractcore.tool_executor import MappingToolExecutor
106
+
107
+ handler = make_tool_calls_handler(tools=MappingToolExecutor.from_tools([execute_python]))
108
+ run_vars = {
109
+ "workspace_root": str(t / "priv"),
110
+ "workspace_access_mode": "all_except_ignored",
111
+ "workspace_allowed_paths": [str(t / "rw")],
112
+ "workspace_ignored_paths": str(t / "refused"),
113
+ }
114
+ run = RunState.new(workflow_id="wf", entry_node="n", session_id="s", vars=run_vars)
115
+ code = f"print(open({str(t / 'refused/secret.txt')!r}).read())"
116
+ effect = Effect(type=EffectType.TOOL_CALLS, payload={"tool_calls": [{"call_id": "c1", "name": "execute_python", "arguments": {"code": code, "_sandbox": {"private_workspace": "/", "posture": "any_except_denied"}}}]})
117
+ outcome = handler(run, effect, None)
118
+ res = outcome.result["results"][0]
119
+ out = res["output"]
120
+ assert MARK not in str(out)
121
+ assert out["sandbox"]["kind"] == "macos-sandbox-exec" and out["sandbox"]["refused"] == [str(t / "refused")]
@@ -18,7 +18,7 @@ def _project() -> dict:
18
18
  def test_base_dependency_floors_match_gateway_alignment() -> None:
19
19
  deps = set(_project()["dependencies"])
20
20
 
21
- assert "abstractcore[tools]>=2.13.41" in deps
21
+ assert "abstractcore[tools]>=2.25.0" in deps
22
22
  assert "abstractruntime>=0.7.0" in deps
23
23
 
24
24
 
@@ -26,11 +26,11 @@ def test_hardware_profile_extras_are_core_runtime_cascades() -> None:
26
26
  extras = _project()["optional-dependencies"]
27
27
 
28
28
  assert set(extras["apple"]) == {
29
- "abstractcore[all-apple]>=2.13.41",
29
+ "abstractcore[all-apple]>=2.25.0",
30
30
  "abstractruntime[apple]>=0.7.0",
31
31
  }
32
32
  assert set(extras["gpu"]) == {
33
- "abstractcore[all-gpu]>=2.13.41",
33
+ "abstractcore[all-gpu]>=2.25.0",
34
34
  "abstractruntime[gpu]>=0.7.0",
35
35
  }
36
36
  assert "all-apple" not in extras
@@ -0,0 +1,106 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+
5
+ from abstractagent.adapters.react_runtime import create_react_workflow
6
+ from abstractagent.logic.react import ReActLogic
7
+ from abstractcore.tools import ToolDefinition
8
+ from abstractruntime.core.models import EffectType, RunStatus
9
+ from abstractruntime.core.runtime import EffectOutcome, Runtime
10
+ from abstractruntime.storage.in_memory import InMemoryLedgerStore, InMemoryRunStore
11
+
12
+
13
+ @pytest.mark.parametrize("recover", [False, True])
14
+ def test_format_repair_is_bounded_and_never_executes_rejected_calls(recover):
15
+ requests: list[dict[str, Any]] = []
16
+ executed = []
17
+
18
+ def llm(run, effect, next_node):
19
+ requests.append(effect.payload)
20
+ if len(requests) > 1:
21
+ assert "nothing was executed" in str(effect.payload["messages"])
22
+ # The repair names no text format of its own (a native tool-calling
23
+ # provider must never be taught a text syntax): it points at the
24
+ # request's own instructions and lists the roster.
25
+ assert "<tool_call>" not in str(effect.payload["messages"])
26
+ assert "format and argument schemas supplied in the instructions" in str(effect.payload["messages"])
27
+ assert "Use only the available tools: web_search." in str(effect.payload["messages"])
28
+ if recover and len(requests) == 2:
29
+ return EffectOutcome.completed({"content": "", "tool_calls": [{"name": "web_search", "arguments": {"query": "test"}, "call_id": "one"}]})
30
+ if recover and len(requests) == 3:
31
+ return EffectOutcome.completed({"content": "Found it."})
32
+ return EffectOutcome.completed({"content": "Broken tool block", "metadata": {
33
+ "tool_call_error": {"code": "invalid_tool_syntax", "available_tools": ["web_search"]}}})
34
+
35
+ def tool(run, effect, next_node):
36
+ executed.extend(effect.payload["tool_calls"])
37
+ return EffectOutcome.completed({"mode": "executed", "results": [{"name": "web_search", "call_id": "one", "success": True, "output": "found"}]})
38
+
39
+ runtime = Runtime(run_store=InMemoryRunStore(), ledger_store=InMemoryLedgerStore(),
40
+ effect_handlers={EffectType.LLM_CALL: llm, EffectType.TOOL_CALLS: tool})
41
+ workflow = create_react_workflow(logic=ReActLogic(tools=[ToolDefinition(name="web_search", description="Search", parameters={"query": {"type": "string"}})]),
42
+ workflow_id="format-repair", provider="stub", model="stub", allowed_tools=["web_search"])
43
+ rid = runtime.start(workflow=workflow, vars={"context": {"task": "Search", "messages": []}, "_runtime": {"inbox": []}})
44
+ def drive():
45
+ for _ in range(80):
46
+ state = runtime.tick(workflow=workflow, run_id=rid, max_steps=1)
47
+ if state.status in (RunStatus.COMPLETED, RunStatus.FAILED):
48
+ return state
49
+ if recover:
50
+ assert drive().status == RunStatus.COMPLETED
51
+ else:
52
+ # GatewayRunner persists planning exceptions as FAILED (tick_exception).
53
+ with pytest.raises(RuntimeError, match="format repair failed"):
54
+ drive()
55
+ assert len(requests) == 3
56
+ assert len(executed) == (1 if recover else 0)
57
+
58
+
59
+ def test_format_repair_budget_is_per_turn_in_a_composed_conversation():
60
+ """Two turns of one run (visit composition): each turn has two rejected formats and
61
+ still recovers. Without `reset_react_turn` clearing `tool_format_retries`, turn 2's
62
+ first rejection would raise at once (the budget was per conversation)."""
63
+ from abstractagent.adapters.react_runtime import reset_react_turn
64
+ from abstractruntime.core.models import StepPlan
65
+ from abstractruntime.core.spec import WorkflowSpec
66
+
67
+ bad = {"content": "Broken tool block", "metadata": {"tool_call_error": {"code": "invalid_tool_syntax", "available_tools": ["web_search"]}}}
68
+ script = [bad, bad, {"content": "Answer one."}, bad, bad, {"content": "Answer two."}]
69
+ calls = {"n": 0}
70
+ events: list[str] = []
71
+ answers: list[str] = []
72
+
73
+ def llm(run, effect, next_node):
74
+ i = calls["n"]
75
+ calls["n"] += 1
76
+ return EffectOutcome.completed(dict(script[min(i, len(script) - 1)]))
77
+
78
+ react = create_react_workflow(
79
+ logic=ReActLogic(tools=[ToolDefinition(name="web_search", description="Search", parameters={"query": {"type": "string"}})]),
80
+ workflow_id="format-repair-turns", provider="stub", model="stub", allowed_tools=["web_search"],
81
+ final_next_node="park", on_step=lambda step, data: events.append(step),
82
+ )
83
+
84
+ def park(run, ctx):
85
+ temp = run.vars.get("_temp") or {}
86
+ answers.append(str(temp.get("final_answer") or ""))
87
+ if len(answers) >= 2:
88
+ return StepPlan(node_id="park", complete_output={"turns": len(answers)})
89
+ reset_react_turn(run.vars)
90
+ run.vars["context"]["messages"].append({"role": "user", "content": "Second message"})
91
+ return StepPlan(node_id="park", next_node="reason")
92
+
93
+ nodes = dict(react.nodes)
94
+ nodes["park"] = park
95
+ workflow = WorkflowSpec(workflow_id=react.workflow_id, entry_node=react.entry_node, nodes=nodes)
96
+ runtime = Runtime(run_store=InMemoryRunStore(), ledger_store=InMemoryLedgerStore(), effect_handlers={EffectType.LLM_CALL: llm})
97
+ rid = runtime.start(workflow=workflow, vars={"context": {"task": "First message", "messages": []}, "_runtime": {"inbox": []}})
98
+ state = None
99
+ for _ in range(120):
100
+ state = runtime.tick(workflow=workflow, run_id=rid, max_steps=1)
101
+ if state.status in (RunStatus.COMPLETED, RunStatus.FAILED):
102
+ break
103
+ assert state is not None and state.status == RunStatus.COMPLETED, state
104
+ assert answers == ["Answer one.", "Answer two."]
105
+ assert events.count("parse_retry_tool_format") == 4
106
+ assert calls["n"] == 6
File without changes
File without changes
File without changes