AbstractAgent 0.3.18__tar.gz → 0.4.0__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 (131) hide show
  1. {abstractagent-0.3.18/src/abstractagent.egg-info → abstractagent-0.4.0}/PKG-INFO +15 -14
  2. {abstractagent-0.3.18 → abstractagent-0.4.0}/README.md +8 -7
  3. {abstractagent-0.3.18 → abstractagent-0.4.0}/pyproject.toml +7 -7
  4. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/emit_inventory.py +6 -0
  5. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/react_runtime.py +60 -0
  6. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/react.py +2 -0
  7. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/logic/builtins.py +63 -0
  8. {abstractagent-0.3.18 → abstractagent-0.4.0/src/abstractagent.egg-info}/PKG-INFO +15 -14
  9. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent.egg-info/SOURCES.txt +2 -0
  10. abstractagent-0.4.0/src/abstractagent.egg-info/requires.txt +16 -0
  11. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_packaging_profiles.py +6 -6
  12. abstractagent-0.4.0/tests/test_react_schedule_run_tool.py +99 -0
  13. abstractagent-0.4.0/tests/test_react_tool_format_retry.py +106 -0
  14. abstractagent-0.3.18/src/abstractagent.egg-info/requires.txt +0 -16
  15. {abstractagent-0.3.18 → abstractagent-0.4.0}/LICENSE +0 -0
  16. {abstractagent-0.3.18 → abstractagent-0.4.0}/setup.cfg +0 -0
  17. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/__init__.py +0 -0
  18. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/__init__.py +0 -0
  19. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/codeact_runtime.py +0 -0
  20. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/generation_params.py +0 -0
  21. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/loop_hooks.py +0 -0
  22. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/media.py +0 -0
  23. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/memact_runtime.py +0 -0
  24. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/native_loop_registry.py +0 -0
  25. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/progress.py +0 -0
  26. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/read_orchestration.py +0 -0
  27. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/tool_allowlist.py +0 -0
  28. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/tool_failure_hints.py +0 -0
  29. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/adapters/transcripts.py +0 -0
  30. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/__init__.py +0 -0
  31. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/base.py +0 -0
  32. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/codeact.py +0 -0
  33. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/defaults.py +0 -0
  34. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/memact.py +0 -0
  35. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/task_reset.py +0 -0
  36. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/agents/unattended.py +0 -0
  37. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/logic/__init__.py +0 -0
  38. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/logic/codeact.py +0 -0
  39. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/logic/memact.py +0 -0
  40. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/logic/react.py +0 -0
  41. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/logic/types.py +0 -0
  42. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/metrics/__init__.py +0 -0
  43. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/metrics/prefix_reuse.py +0 -0
  44. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/repl.py +0 -0
  45. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/sandbox/__init__.py +0 -0
  46. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/sandbox/interface.py +0 -0
  47. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/sandbox/local.py +0 -0
  48. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/scripts/__init__.py +0 -0
  49. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/scripts/lmstudio_tool_eval.py +0 -0
  50. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/tools/__init__.py +0 -0
  51. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/tools/code_execution.py +0 -0
  52. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent/tools/self_improve.py +0 -0
  53. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent.egg-info/dependency_links.txt +0 -0
  54. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent.egg-info/entry_points.txt +0 -0
  55. {abstractagent-0.3.18 → abstractagent-0.4.0}/src/abstractagent.egg-info/top_level.txt +0 -0
  56. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_active_context_policy_adoption.py +0 -0
  57. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_adversary_hardening_2026_07_13.py +0 -0
  58. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_agent_plan_review_modes.py +0 -0
  59. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_agent_speculation_inheritance.py +0 -0
  60. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_agent_stream_inheritance.py +0 -0
  61. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_agents_integration_llm.py +0 -0
  62. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_base_agent_trace_accessors.py +0 -0
  63. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_batching_prompt_rule.py +0 -0
  64. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_budget_triage_tail.py +0 -0
  65. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_builtin_tool_specs_optional_defaults.py +0 -0
  66. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_c2447_visit_lane_honesty.py +0 -0
  67. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_claim_0029_residue_2026_07_14.py +0 -0
  68. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_codeact_logic.py +0 -0
  69. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_codeact_runtime_verifier_policy.py +0 -0
  70. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_delegate_session_route_composition.py +0 -0
  71. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_emit_inventory_drift.py +0 -0
  72. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_execute_python_sandbox.py +0 -0
  73. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_gated_remainder_wave_2026_07_14.py +0 -0
  74. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_generation_params_media_policies.py +0 -0
  75. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_history_monster_clamp.py +0 -0
  76. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_improvement_wave_2026_07_13.py +0 -0
  77. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_llm_call_payload_requires_prompt_or_messages.py +0 -0
  78. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_lmstudio_tool_eval_metrics.py +0 -0
  79. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_loop_hooks.py +0 -0
  80. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_loop_hooks_0026_follow_ups.py +0 -0
  81. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_memact_codeact_guidance_durable_transcript.py +0 -0
  82. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_memact_session_memory.py +0 -0
  83. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_memact_workflow.py +0 -0
  84. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_memory_recall_tool.py +0 -0
  85. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_native_loop_registry.py +0 -0
  86. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_open_attachment_tool_available.py +0 -0
  87. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_operator_conclude.py +0 -0
  88. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_optimal_wave_batch2_2026_07_13.py +0 -0
  89. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_parse_prompt_cache_capture.py +0 -0
  90. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_prefix_reuse_metric.py +0 -0
  91. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_progress_detection.py +0 -0
  92. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_act_only_observation.py +0 -0
  93. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_active_attachments_disable_tools_first_iteration.py +0 -0
  94. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_adapter_forwards_context_attachments_media.py +0 -0
  95. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_delegate_agent_tool.py +0 -0
  96. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_election_fence_conformance.py +0 -0
  97. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_entity_dress_conformance.py +0 -0
  98. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_guidance_durable_transcript.py +0 -0
  99. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_history_window.py +0 -0
  100. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_logic.py +0 -0
  101. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_loop_aborted_generation_retry.py +0 -0
  102. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_loop_context_transcript_levels.py +0 -0
  103. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_loop_followthrough_retry.py +0 -0
  104. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_loop_side_effect_dedupe_comms.py +0 -0
  105. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_loop_truncated_output_retry.py +0 -0
  106. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_max_iterations_concludes.py +0 -0
  107. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_phase_grant_boundary.py +0 -0
  108. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_planish_heuristic.py +0 -0
  109. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_prompt_prefix_stability.py +0 -0
  110. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_sight_lane_media_fold.py +0 -0
  111. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_turn_byte_stability.py +0 -0
  112. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_verifier_and_plan.py +0 -0
  113. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_visit_composition.py +0 -0
  114. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_visit_merge.py +0 -0
  115. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_workflow_system_prompt_and_tools_override.py +0 -0
  116. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_react_workflow_toolset_refresh.py +0 -0
  117. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_read_orchestration_guidance.py +0 -0
  118. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_reasoning_first_citizen_agent.py +0 -0
  119. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_review_failure_containment.py +0 -0
  120. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_run_start_seeds_runtime_provider_model.py +0 -0
  121. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_session_memory.py +0 -0
  122. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_sibling_loop_defects_batch.py +0 -0
  123. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_sibling_strict_transcripts_0011.py +0 -0
  124. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_stuck_streak_termination.py +0 -0
  125. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_tool_failure_hints.py +0 -0
  126. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_tool_loop_prefix_extension_a3.py +0 -0
  127. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_tool_observe_emits_result_for_tui.py +0 -0
  128. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_tools_editing.py +0 -0
  129. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_unseeded_render_limits.py +0 -0
  130. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_verifier_execution_preference.py +0 -0
  131. {abstractagent-0.3.18 → abstractagent-0.4.0}/tests/test_verifier_schema_strict.py +0 -0
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractagent
3
- Version: 0.3.18
3
+ Version: 0.4.0
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.25.0
9
- Requires-Dist: abstractruntime>=0.7.0
8
+ Requires-Dist: abstractcore[tools]>=2.30.0
9
+ Requires-Dist: abstractruntime>=0.12.0
10
10
  Provides-Extra: apple
11
- Requires-Dist: abstractcore[all-apple]>=2.25.0; extra == "apple"
12
- Requires-Dist: abstractruntime[apple]>=0.7.0; extra == "apple"
11
+ Requires-Dist: abstractcore[all-apple]>=2.30.0; extra == "apple"
12
+ Requires-Dist: abstractruntime[apple]>=0.12.0; extra == "apple"
13
13
  Provides-Extra: gpu
14
- Requires-Dist: abstractcore[all-gpu]>=2.25.0; extra == "gpu"
15
- Requires-Dist: abstractruntime[gpu]>=0.7.0; extra == "gpu"
14
+ Requires-Dist: abstractcore[all-gpu]>=2.30.0; extra == "gpu"
15
+ Requires-Dist: abstractruntime[gpu]>=0.12.0; extra == "gpu"
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=7.0; extra == "dev"
18
18
  Provides-Extra: test
@@ -68,6 +68,9 @@ flowchart LR
68
68
  - **Generation controls**: temperature, seed, media policy, prompt-cache
69
69
  identity, streaming, Core `thinking` and `speculation` (MTP) are normalized
70
70
  before LLM calls and inherited by delegated children
71
+ - **Scheduled runs** (ReAct): the `schedule_run` tool schedules work or a reminder
72
+ for later in the same conversation, with at most the agent's tools (see
73
+ [`docs/tools.md`](docs/tools.md))
71
74
  - **Observability**: durable ledger of LLM calls, tool calls, and waits
72
75
 
73
76
  Where this lives in code (source of truth):
@@ -105,13 +108,11 @@ Native Python hardware profile cascades are available for deployment manifests:
105
108
  AbstractCore and AbstractRuntime profiles; AbstractAgent itself remains
106
109
  provider/runtime agnostic.
107
110
 
108
- AbstractAgent 0.3.17 requires `abstractcore[tools]>=2.13.41` and
109
- `AbstractRuntime>=0.7.0` (the runtime release with the 50k-token history
110
- window, which ReAct applies to its requests when a host sets
111
- `_runtime.history_window_tokens`, as the entity visit does; 0.5.0 brought live
112
- token streaming, on top of turn grounding, in-flight cancellation and
113
- speculation inheritance). AbstractRuntime 0.7.0 itself requires AbstractCore
114
- 2.18.0 or newer.
111
+ AbstractAgent 0.4.0 requires `abstractcore[tools]>=2.30.0` and
112
+ `AbstractRuntime>=0.12.0` (the runtime release with the `SCHEDULE_RUN` effect
113
+ behind the ReAct `schedule_run` tool, on top of the 50k-token history window
114
+ that ReAct applies when a host sets `_runtime.history_window_tokens`, live token
115
+ streaming, turn grounding, in-flight cancellation and speculation inheritance).
115
116
 
116
117
  Note: the repository may be ahead of the latest published PyPI release. To verify what you installed:
117
118
 
@@ -47,6 +47,9 @@ flowchart LR
47
47
  - **Generation controls**: temperature, seed, media policy, prompt-cache
48
48
  identity, streaming, Core `thinking` and `speculation` (MTP) are normalized
49
49
  before LLM calls and inherited by delegated children
50
+ - **Scheduled runs** (ReAct): the `schedule_run` tool schedules work or a reminder
51
+ for later in the same conversation, with at most the agent's tools (see
52
+ [`docs/tools.md`](docs/tools.md))
50
53
  - **Observability**: durable ledger of LLM calls, tool calls, and waits
51
54
 
52
55
  Where this lives in code (source of truth):
@@ -84,13 +87,11 @@ Native Python hardware profile cascades are available for deployment manifests:
84
87
  AbstractCore and AbstractRuntime profiles; AbstractAgent itself remains
85
88
  provider/runtime agnostic.
86
89
 
87
- AbstractAgent 0.3.17 requires `abstractcore[tools]>=2.13.41` and
88
- `AbstractRuntime>=0.7.0` (the runtime release with the 50k-token history
89
- window, which ReAct applies to its requests when a host sets
90
- `_runtime.history_window_tokens`, as the entity visit does; 0.5.0 brought live
91
- token streaming, on top of turn grounding, in-flight cancellation and
92
- speculation inheritance). AbstractRuntime 0.7.0 itself requires AbstractCore
93
- 2.18.0 or newer.
90
+ AbstractAgent 0.4.0 requires `abstractcore[tools]>=2.30.0` and
91
+ `AbstractRuntime>=0.12.0` (the runtime release with the `SCHEDULE_RUN` effect
92
+ behind the ReAct `schedule_run` tool, on top of the 50k-token history window
93
+ that ReAct applies when a host sets `_runtime.history_window_tokens`, live token
94
+ streaming, turn grounding, in-flight cancellation and speculation inheritance).
94
95
 
95
96
  Note: the repository may be ahead of the latest published PyPI release. To verify what you installed:
96
97
 
@@ -4,23 +4,23 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "abstractagent"
7
- version = "0.3.18"
7
+ version = "0.4.0"
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.25.0",
13
- "abstractruntime>=0.7.0",
12
+ "abstractcore[tools]>=2.30.0",
13
+ "abstractruntime>=0.12.0",
14
14
  ]
15
15
 
16
16
  [project.optional-dependencies]
17
17
  apple = [
18
- "abstractcore[all-apple]>=2.25.0",
19
- "abstractruntime[apple]>=0.7.0",
18
+ "abstractcore[all-apple]>=2.30.0",
19
+ "abstractruntime[apple]>=0.12.0",
20
20
  ]
21
21
  gpu = [
22
- "abstractcore[all-gpu]>=2.25.0",
23
- "abstractruntime[gpu]>=0.7.0",
22
+ "abstractcore[all-gpu]>=2.30.0",
23
+ "abstractruntime[gpu]>=0.12.0",
24
24
  ]
25
25
  dev = [
26
26
  "pytest>=7.0",
@@ -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
 
@@ -1736,6 +1740,7 @@ def create_react_workflow(
1736
1740
  "remember_note",
1737
1741
  "compact_memory",
1738
1742
  "delegate_agent",
1743
+ "schedule_run",
1739
1744
  "update_plan",
1740
1745
  }
1741
1746
 
@@ -2496,6 +2501,36 @@ def create_react_workflow(
2496
2501
  emit("parse_tool_calls", {"count": len(tool_calls)})
2497
2502
  return StepPlan(node_id="parse", next_node="act")
2498
2503
 
2504
+ # Core owns syntax recognition; the agent owns bounded recovery. Retry
2505
+ # only when zero calls were accepted, so a partial batch is never replayed.
2506
+ metadata = response.get("metadata") if isinstance(response, dict) else None
2507
+ tool_error = metadata.get("tool_call_error") if isinstance(metadata, dict) else None
2508
+ if isinstance(tool_error, dict) and tool_error.get("code") in {"invalid_tool_syntax", "unavailable_tool"}:
2509
+ retries = int(temp.get("tool_format_retries", 0) or 0)
2510
+ if retries >= 2 or cycle_i >= max_iterations:
2511
+ raise RuntimeError("Tool-call format repair failed: no executable tool calls after bounded retries.")
2512
+ specs = runtime_ns.get("tool_specs") or []
2513
+ names = [str(spec["name"]) for spec in specs if isinstance(spec, dict) and spec.get("name")]
2514
+ if not names:
2515
+ raise RuntimeError("The model attempted a tool call, but this run has no tools configured.")
2516
+ temp["tool_format_retries"] = retries + 1
2517
+ _push_inbox(
2518
+ runtime_ns,
2519
+ "Your previous tool call was rejected; nothing was executed. "
2520
+ "Use only the available tools: " + ", ".join(names) + ". "
2521
+ # No format of our own: the tool-calling format is the one the
2522
+ # request's instructions carry (native structured calls, or the
2523
+ # provider's prompted text format) — a hard-coded text syntax
2524
+ # here would teach a native tool-calling model the wrong one.
2525
+ "Follow the tool-calling format and argument schemas supplied in the instructions, "
2526
+ "with an actual available tool name and valid arguments. "
2527
+ "Emit the corrected call now, or explain why you cannot complete the task without it.",
2528
+ )
2529
+ emit("parse_retry_tool_format", {"cycle": cycle_i, "attempt": retries + 1, "code": tool_error["code"]})
2530
+ if last_call_media:
2531
+ temp["pending_media"] = last_call_media
2532
+ return StepPlan(node_id="parse", next_node="reason")
2533
+
2499
2534
  # If the model hit an output limit, treat the step as incomplete and continue.
2500
2535
  # Retry-nudge lane honesty (iteration-3 adversary P0, 2026-07-19 — the
2501
2536
  # c2447 chrome class at the PARSE boundary): these nudges ride the
@@ -3012,6 +3047,31 @@ def create_react_workflow(
3012
3047
  next_node="observe",
3013
3048
  )
3014
3049
 
3050
+ if name == "schedule_run":
3051
+ # Framework backlog 1045: a run LATER (at a time, after a delay, or on a trigger
3052
+ # event) with at most this run's tools, never schedule_run itself (no recursion,
3053
+ # like delegate_agent). The runtime intersects again with the run tree's ceiling
3054
+ # and the host creates the one-shot automation (EffectType.SCHEDULE_RUN).
3055
+ tools_raw = args.get("tools")
3056
+ if tools_raw is None:
3057
+ wake_tools = [t for t in allow if t != "schedule_run"]
3058
+ else:
3059
+ parent_allow = set(allow)
3060
+ wake_tools = [t for t in _normalize_allowlist(tools_raw) if t in parent_allow and t != "schedule_run"]
3061
+ payload = {
3062
+ k: args[k]
3063
+ for k in ("at", "after_s", "on", "task", "note", "mode", "title", "notify")
3064
+ if args.get(k) is not None
3065
+ }
3066
+ payload["tools"] = wake_tools
3067
+ payload["tool_name"] = "schedule_run"
3068
+ payload["call_id"] = str(tc.get("call_id") or "")
3069
+ return StepPlan(
3070
+ node_id="act",
3071
+ effect=Effect(type=EffectType.SCHEDULE_RUN, payload=payload, result_key="_temp.tool_results"),
3072
+ next_node="observe",
3073
+ )
3074
+
3015
3075
  # Unknown builtin: continue.
3016
3076
  return StepPlan(node_id="act", next_node="act" if temp.get("pending_tool_calls") else "reason")
3017
3077
 
@@ -24,6 +24,7 @@ from ..logic.builtins import (
24
24
  ASK_USER_TOOL,
25
25
  COMPACT_MEMORY_TOOL,
26
26
  DELEGATE_AGENT_TOOL,
27
+ SCHEDULE_RUN_TOOL,
27
28
  INSPECT_VARS_TOOL,
28
29
  OPEN_ATTACHMENT_TOOL,
29
30
  RECALL_MEMORY_TOOL,
@@ -121,6 +122,7 @@ class ReactAgent(BaseAgent):
121
122
  REMEMBER_NOTE_TOOL,
122
123
  COMPACT_MEMORY_TOOL,
123
124
  DELEGATE_AGENT_TOOL,
125
+ SCHEDULE_RUN_TOOL,
124
126
  UPDATE_PLAN_TOOL,
125
127
  *tool_defs,
126
128
  ]
@@ -364,3 +364,66 @@ DELEGATE_AGENT_TOOL = ToolDefinition(
364
364
  "inspect a few files, search for specific symbols, summarize findings)."
365
365
  ),
366
366
  )
367
+
368
+ SCHEDULE_RUN_TOOL = ToolDefinition(
369
+ name="schedule_run",
370
+ description="Schedule a run for later (at a time, after a delay, or on a trigger event) with a task or a reminder note.",
371
+ parameters={
372
+ "after_s": {
373
+ "type": "number",
374
+ "description": "Start this many seconds from now (e.g. 300 for five minutes). Give one of after_s, at or on.",
375
+ "default": None,
376
+ },
377
+ "at": {
378
+ "type": "string",
379
+ "description": "Start at this time: an RFC 3339 timestamp with its offset, e.g. 2026-10-26T09:00:00+01:00.",
380
+ "default": None,
381
+ },
382
+ "on": {
383
+ "type": "object",
384
+ "description": (
385
+ "Start when a trigger event arrives: {source_id, source_version?, connection_id?, config?}, "
386
+ "e.g. {\"source_id\": \"feed.item\", \"connection_id\": \"tc_...\"}. One run, on the first event."
387
+ ),
388
+ "default": None,
389
+ },
390
+ "task": {
391
+ "type": "string",
392
+ "description": "What the scheduled run should do (self-contained). Give a task or a note.",
393
+ "default": None,
394
+ },
395
+ "note": {
396
+ "type": "string",
397
+ "description": "A reminder to bring back to the person at that time (no tools are used).",
398
+ "default": None,
399
+ },
400
+ "tools": {
401
+ "type": "array",
402
+ "items": {"type": "string"},
403
+ "description": "Tools the scheduled run may use (a subset of yours). When omitted it inherits yours.",
404
+ "default": None,
405
+ },
406
+ "mode": {
407
+ "type": "string",
408
+ "description": "same_session (default: a new turn of this conversation) or independent (a fresh conversation).",
409
+ "default": "same_session",
410
+ },
411
+ "title": {
412
+ "type": "string",
413
+ "description": "A short title for the scheduled run (optional).",
414
+ "default": None,
415
+ },
416
+ "notify": {
417
+ "type": "boolean",
418
+ "description": (
419
+ "Send the person a notification with the scheduled run's answer (optional). Default: true "
420
+ "for a note (a reminder reaches them), false for a task."
421
+ ),
422
+ "default": None,
423
+ },
424
+ },
425
+ when_to_use=(
426
+ "Use when the person asks to be reminded later, or when work must happen later or when something "
427
+ "arrives. The run joins this conversation by default, with at most your tools."
428
+ ),
429
+ )
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractagent
3
- Version: 0.3.18
3
+ Version: 0.4.0
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.25.0
9
- Requires-Dist: abstractruntime>=0.7.0
8
+ Requires-Dist: abstractcore[tools]>=2.30.0
9
+ Requires-Dist: abstractruntime>=0.12.0
10
10
  Provides-Extra: apple
11
- Requires-Dist: abstractcore[all-apple]>=2.25.0; extra == "apple"
12
- Requires-Dist: abstractruntime[apple]>=0.7.0; extra == "apple"
11
+ Requires-Dist: abstractcore[all-apple]>=2.30.0; extra == "apple"
12
+ Requires-Dist: abstractruntime[apple]>=0.12.0; extra == "apple"
13
13
  Provides-Extra: gpu
14
- Requires-Dist: abstractcore[all-gpu]>=2.25.0; extra == "gpu"
15
- Requires-Dist: abstractruntime[gpu]>=0.7.0; extra == "gpu"
14
+ Requires-Dist: abstractcore[all-gpu]>=2.30.0; extra == "gpu"
15
+ Requires-Dist: abstractruntime[gpu]>=0.12.0; extra == "gpu"
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=7.0; extra == "dev"
18
18
  Provides-Extra: test
@@ -68,6 +68,9 @@ flowchart LR
68
68
  - **Generation controls**: temperature, seed, media policy, prompt-cache
69
69
  identity, streaming, Core `thinking` and `speculation` (MTP) are normalized
70
70
  before LLM calls and inherited by delegated children
71
+ - **Scheduled runs** (ReAct): the `schedule_run` tool schedules work or a reminder
72
+ for later in the same conversation, with at most the agent's tools (see
73
+ [`docs/tools.md`](docs/tools.md))
71
74
  - **Observability**: durable ledger of LLM calls, tool calls, and waits
72
75
 
73
76
  Where this lives in code (source of truth):
@@ -105,13 +108,11 @@ Native Python hardware profile cascades are available for deployment manifests:
105
108
  AbstractCore and AbstractRuntime profiles; AbstractAgent itself remains
106
109
  provider/runtime agnostic.
107
110
 
108
- AbstractAgent 0.3.17 requires `abstractcore[tools]>=2.13.41` and
109
- `AbstractRuntime>=0.7.0` (the runtime release with the 50k-token history
110
- window, which ReAct applies to its requests when a host sets
111
- `_runtime.history_window_tokens`, as the entity visit does; 0.5.0 brought live
112
- token streaming, on top of turn grounding, in-flight cancellation and
113
- speculation inheritance). AbstractRuntime 0.7.0 itself requires AbstractCore
114
- 2.18.0 or newer.
111
+ AbstractAgent 0.4.0 requires `abstractcore[tools]>=2.30.0` and
112
+ `AbstractRuntime>=0.12.0` (the runtime release with the `SCHEDULE_RUN` effect
113
+ behind the ReAct `schedule_run` tool, on top of the 50k-token history window
114
+ that ReAct applies when a host sets `_runtime.history_window_tokens`, live token
115
+ streaming, turn grounding, in-flight cancellation and speculation inheritance).
115
116
 
116
117
  Note: the repository may be ahead of the latest published PyPI release. To verify what you installed:
117
118
 
@@ -102,7 +102,9 @@ tests/test_react_max_iterations_concludes.py
102
102
  tests/test_react_phase_grant_boundary.py
103
103
  tests/test_react_planish_heuristic.py
104
104
  tests/test_react_prompt_prefix_stability.py
105
+ tests/test_react_schedule_run_tool.py
105
106
  tests/test_react_sight_lane_media_fold.py
107
+ tests/test_react_tool_format_retry.py
106
108
  tests/test_react_turn_byte_stability.py
107
109
  tests/test_react_verifier_and_plan.py
108
110
  tests/test_react_visit_composition.py
@@ -0,0 +1,16 @@
1
+ abstractcore[tools]>=2.30.0
2
+ abstractruntime>=0.12.0
3
+
4
+ [apple]
5
+ abstractcore[all-apple]>=2.30.0
6
+ abstractruntime[apple]>=0.12.0
7
+
8
+ [dev]
9
+ pytest>=7.0
10
+
11
+ [gpu]
12
+ abstractcore[all-gpu]>=2.30.0
13
+ abstractruntime[gpu]>=0.12.0
14
+
15
+ [test]
16
+ pytest>=7.0
@@ -18,20 +18,20 @@ 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.25.0" in deps
22
- assert "abstractruntime>=0.7.0" in deps
21
+ assert "abstractcore[tools]>=2.30.0" in deps
22
+ assert "abstractruntime>=0.12.0" in deps
23
23
 
24
24
 
25
25
  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.25.0",
30
- "abstractruntime[apple]>=0.7.0",
29
+ "abstractcore[all-apple]>=2.30.0",
30
+ "abstractruntime[apple]>=0.12.0",
31
31
  }
32
32
  assert set(extras["gpu"]) == {
33
- "abstractcore[all-gpu]>=2.25.0",
34
- "abstractruntime[gpu]>=0.7.0",
33
+ "abstractcore[all-gpu]>=2.30.0",
34
+ "abstractruntime[gpu]>=0.12.0",
35
35
  }
36
36
  assert "all-apple" not in extras
37
37
  assert "all-gpu" not in extras
@@ -0,0 +1,99 @@
1
+ """The `schedule_run` builtin (framework backlog 1045): a ReAct call becomes ONE `SCHEDULE_RUN` effect
2
+ with at most the agent's tools and never `schedule_run` itself; the host creates the wake."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import pytest
9
+
10
+ from abstractagent.adapters.react_runtime import create_react_workflow
11
+ from abstractagent.logic.builtins import SCHEDULE_RUN_TOOL
12
+ from abstractagent.logic.react import ReActLogic
13
+ from abstractcore.tools import ToolDefinition
14
+ from abstractruntime.core.models import Effect, EffectType, RunState, RunStatus
15
+ from abstractruntime.core.runtime import EffectOutcome, Runtime
16
+ from abstractruntime.scheduler.registry import WorkflowRegistry
17
+ from abstractruntime.storage.in_memory import InMemoryLedgerStore, InMemoryRunStore
18
+
19
+
20
+ def _run(arguments: Dict[str, Any], *, allowed_tools: List[str], handler: Any) -> RunState:
21
+ calls: Dict[str, int] = {}
22
+
23
+ def llm_handler(run: RunState, effect: Effect, default_next_node: Optional[str]) -> EffectOutcome:
24
+ calls[run.run_id] = calls.get(run.run_id, 0) + 1
25
+ if calls[run.run_id] == 1:
26
+ return EffectOutcome.completed({"content": "Scheduling.", "finish_reason": "tool_calls",
27
+ "tool_calls": [{"name": "schedule_run", "arguments": arguments, "call_id": "call_1"}]})
28
+ return EffectOutcome.completed({"content": "Done.", "tool_calls": [], "finish_reason": "stop"})
29
+
30
+ runtime = Runtime(run_store=InMemoryRunStore(), ledger_store=InMemoryLedgerStore(),
31
+ effect_handlers={EffectType.LLM_CALL: llm_handler}, workflow_registry=WorkflowRegistry())
32
+ runtime.set_schedule_run_handler(handler)
33
+ tool_defs = [SCHEDULE_RUN_TOOL, ToolDefinition(name="read_file", description="Read", parameters={}),
34
+ ToolDefinition(name="execute_command", description="Run", parameters={})]
35
+ workflow = create_react_workflow(logic=ReActLogic(tools=tool_defs), workflow_id="react_agent")
36
+ runtime.workflow_registry.register(workflow)
37
+ run_id = runtime.start(workflow=workflow, session_id="chat-S",
38
+ vars={"context": {"task": "Remind me", "messages": []}, "_runtime": {"inbox": [], "allowed_tools": allowed_tools}})
39
+ for _ in range(200):
40
+ state = runtime.tick(workflow=workflow, run_id=run_id, max_steps=1)
41
+ if state.status in (RunStatus.COMPLETED, RunStatus.FAILED):
42
+ break
43
+ return runtime.get_state(run_id)
44
+
45
+
46
+ def _tool_messages(final: RunState) -> List[str]:
47
+ msgs = (final.output or {}).get("messages") or []
48
+ return [str(m.get("content") or "") for m in msgs if isinstance(m, dict) and m.get("role") == "tool"]
49
+
50
+
51
+ @pytest.mark.basic
52
+ def test_schedule_run_passes_at_most_the_agents_tools_never_itself() -> None:
53
+ plans: List[Dict[str, Any]] = []
54
+
55
+ def host(run, plan):
56
+ plans.append(plan)
57
+ return {"automation_id": "auto-1"}
58
+
59
+ final = _run({"after_s": 300, "task": "check the build", "tools": ["read_file", "execute_command", "schedule_run"]},
60
+ allowed_tools=["schedule_run", "read_file"], handler=host)
61
+ assert final.status == RunStatus.COMPLETED
62
+ (plan,) = plans
63
+ assert plan["allowed_tools"] == ["read_file"] # execute_command is not the agent's; schedule_run never passes on
64
+ assert plan["session_id"] == "chat-S" and plan["mode"] == "same_session" and plan["kind"] == "task"
65
+ assert plan["input_data"]["_runtime"]["allowed_tools"] == ["read_file"]
66
+ assert any("auto-1" in m for m in _tool_messages(final))
67
+
68
+
69
+ @pytest.mark.basic
70
+ def test_schedule_run_inherits_the_agents_tools_when_omitted_and_a_note_gets_none() -> None:
71
+ plans: List[Dict[str, Any]] = []
72
+ final = _run({"after_s": 60, "note": "stretch"}, allowed_tools=["schedule_run", "read_file", "execute_command"],
73
+ handler=lambda run, plan: plans.append(plan) or {"automation_id": "auto-2"})
74
+ assert final.status == RunStatus.COMPLETED
75
+ assert plans[0]["allowed_tools"] == [] and plans[0]["kind"] == "note"
76
+ plans.clear()
77
+ _run({"after_s": 60, "task": "t"}, allowed_tools=["schedule_run", "read_file", "execute_command"],
78
+ handler=lambda run, plan: plans.append(plan) or {"automation_id": "auto-3"})
79
+ assert plans[0]["allowed_tools"] == ["execute_command", "read_file"]
80
+
81
+
82
+ @pytest.mark.basic
83
+ def test_schedule_run_outside_the_allowlist_is_refused_without_calling_the_host() -> None:
84
+ plans: List[Dict[str, Any]] = []
85
+ final = _run({"after_s": 60, "note": "x"}, allowed_tools=["read_file"], handler=lambda run, plan: plans.append(plan) or {"automation_id": "x"})
86
+ assert final.status == RunStatus.COMPLETED
87
+ assert plans == []
88
+ assert any("schedule_run" in m for m in _tool_messages(final))
89
+
90
+
91
+ @pytest.mark.basic
92
+ def test_schedule_run_passes_notify_and_notes_notify_by_default() -> None:
93
+ plans: List[Dict[str, Any]] = []
94
+ host = lambda run, plan: plans.append(plan) or {"automation_id": "a"} # noqa: E731
95
+ _run({"after_s": 60, "note": "drink water"}, allowed_tools=["schedule_run"], handler=host)
96
+ _run({"after_s": 60, "task": "t", "tools": []}, allowed_tools=["schedule_run"], handler=host)
97
+ _run({"after_s": 60, "task": "t", "tools": [], "notify": True}, allowed_tools=["schedule_run"], handler=host)
98
+ assert [p["notify"] for p in plans] == [True, False, True]
99
+ assert plans[0]["notify_title"] == "drink water"
@@ -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
@@ -1,16 +0,0 @@
1
- abstractcore[tools]>=2.25.0
2
- abstractruntime>=0.7.0
3
-
4
- [apple]
5
- abstractcore[all-apple]>=2.25.0
6
- abstractruntime[apple]>=0.7.0
7
-
8
- [dev]
9
- pytest>=7.0
10
-
11
- [gpu]
12
- abstractcore[all-gpu]>=2.25.0
13
- abstractruntime[gpu]>=0.7.0
14
-
15
- [test]
16
- pytest>=7.0
File without changes
File without changes