AbstractAgent 0.3.13__tar.gz → 0.3.15__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.
- {abstractagent-0.3.13/src/abstractagent.egg-info → abstractagent-0.3.15}/PKG-INFO +14 -10
- {abstractagent-0.3.13 → abstractagent-0.3.15}/README.md +8 -6
- {abstractagent-0.3.13 → abstractagent-0.3.15}/pyproject.toml +7 -4
- abstractagent-0.3.15/src/abstractagent/adapters/announced_calls.py +463 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/codeact_runtime.py +142 -4
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/emit_inventory.py +25 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/memact_runtime.py +143 -3
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/react_runtime.py +304 -30
- {abstractagent-0.3.13 → abstractagent-0.3.15/src/abstractagent.egg-info}/PKG-INFO +14 -10
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent.egg-info/SOURCES.txt +3 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent.egg-info/requires.txt +5 -3
- abstractagent-0.3.15/tests/test_agent_stream_inheritance.py +56 -0
- abstractagent-0.3.15/tests/test_announced_tool_reprompt.py +751 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_c2447_visit_lane_honesty.py +2 -2
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_packaging_profiles.py +3 -3
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_loop_followthrough_retry.py +3 -1
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_reasoning_first_citizen_agent.py +78 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/LICENSE +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/setup.cfg +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/generation_params.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/loop_hooks.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/media.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/native_loop_registry.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/progress.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/read_orchestration.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/tool_allowlist.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/tool_failure_hints.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/adapters/transcripts.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/base.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/codeact.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/defaults.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/memact.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/react.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/task_reset.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/agents/unattended.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/logic/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/logic/builtins.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/logic/codeact.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/logic/memact.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/logic/react.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/logic/types.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/metrics/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/metrics/prefix_reuse.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/repl.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/sandbox/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/sandbox/interface.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/sandbox/local.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/scripts/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/scripts/lmstudio_tool_eval.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/tools/__init__.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/tools/code_execution.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent/tools/self_improve.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent.egg-info/dependency_links.txt +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent.egg-info/entry_points.txt +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/src/abstractagent.egg-info/top_level.txt +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_active_context_policy_adoption.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_adversary_hardening_2026_07_13.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_agent_plan_review_modes.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_agent_speculation_inheritance.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_agents_integration_llm.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_base_agent_trace_accessors.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_batching_prompt_rule.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_budget_triage_tail.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_builtin_tool_specs_optional_defaults.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_claim_0029_residue_2026_07_14.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_codeact_logic.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_codeact_runtime_verifier_policy.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_delegate_session_route_composition.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_emit_inventory_drift.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_gated_remainder_wave_2026_07_14.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_generation_params_media_policies.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_history_monster_clamp.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_improvement_wave_2026_07_13.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_llm_call_payload_requires_prompt_or_messages.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_lmstudio_tool_eval_metrics.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_loop_hooks.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_loop_hooks_0026_follow_ups.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_memact_codeact_guidance_durable_transcript.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_memact_session_memory.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_memact_workflow.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_memory_recall_tool.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_native_loop_registry.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_open_attachment_tool_available.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_operator_conclude.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_optimal_wave_batch2_2026_07_13.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_parse_prompt_cache_capture.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_prefix_reuse_metric.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_progress_detection.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_act_only_observation.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_active_attachments_disable_tools_first_iteration.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_adapter_forwards_context_attachments_media.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_delegate_agent_tool.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_election_fence_conformance.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_entity_dress_conformance.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_guidance_durable_transcript.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_logic.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_loop_aborted_generation_retry.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_loop_context_transcript_levels.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_loop_side_effect_dedupe_comms.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_loop_truncated_output_retry.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_max_iterations_concludes.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_phase_grant_boundary.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_planish_heuristic.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_prompt_prefix_stability.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_sight_lane_media_fold.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_turn_byte_stability.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_verifier_and_plan.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_visit_composition.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_visit_merge.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_workflow_system_prompt_and_tools_override.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_react_workflow_toolset_refresh.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_read_orchestration_guidance.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_review_failure_containment.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_run_start_seeds_runtime_provider_model.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_session_memory.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_sibling_loop_defects_batch.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_sibling_strict_transcripts_0011.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_stuck_streak_termination.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_tool_failure_hints.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_tool_loop_prefix_extension_a3.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_tool_observe_emits_result_for_tui.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_tools_editing.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_unseeded_render_limits.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_verifier_execution_preference.py +0 -0
- {abstractagent-0.3.13 → abstractagent-0.3.15}/tests/test_verifier_schema_strict.py +0 -0
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abstractagent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.15
|
|
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
8
|
Requires-Dist: abstractcore[tools]>=2.13.41
|
|
9
|
-
Requires-Dist: abstractruntime>=0.
|
|
9
|
+
Requires-Dist: abstractruntime>=0.5.0
|
|
10
10
|
Provides-Extra: apple
|
|
11
11
|
Requires-Dist: abstractcore[all-apple]>=2.13.41; extra == "apple"
|
|
12
|
-
Requires-Dist: abstractruntime[apple]>=0.
|
|
12
|
+
Requires-Dist: abstractruntime[apple]>=0.5.0; extra == "apple"
|
|
13
13
|
Provides-Extra: gpu
|
|
14
14
|
Requires-Dist: abstractcore[all-gpu]>=2.13.41; extra == "gpu"
|
|
15
|
-
Requires-Dist: abstractruntime[gpu]>=0.
|
|
15
|
+
Requires-Dist: abstractruntime[gpu]>=0.5.0; extra == "gpu"
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Requires-Dist: jinja2>=3.1; extra == "dev"
|
|
18
19
|
Provides-Extra: test
|
|
19
20
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
21
|
+
Requires-Dist: jinja2>=3.1; extra == "test"
|
|
20
22
|
Dynamic: license-file
|
|
21
23
|
|
|
22
24
|
# AbstractAgent
|
|
@@ -46,12 +48,14 @@ flowchart LR
|
|
|
46
48
|
- Getting started: [`docs/getting-started.md`](docs/getting-started.md)
|
|
47
49
|
- API reference: [`docs/api.md`](docs/api.md)
|
|
48
50
|
- Loop hooks (listen/steer/capture): [`docs/hooks.md`](docs/hooks.md)
|
|
49
|
-
- FAQ
|
|
51
|
+
- FAQ: [`docs/faq.md`](docs/faq.md)
|
|
52
|
+
- Troubleshooting: [`docs/troubleshooting.md`](docs/troubleshooting.md)
|
|
50
53
|
- Architecture (diagrams): [`docs/architecture.md`](docs/architecture.md)
|
|
51
54
|
- Changelog: [`CHANGELOG.md`](CHANGELOG.md)
|
|
52
55
|
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
53
56
|
- Security: [`SECURITY.md`](SECURITY.md)
|
|
54
57
|
- Acknowledgements: [`ACKNOWLEDMENTS.md`](ACKNOWLEDMENTS.md)
|
|
58
|
+
- Code of conduct: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
|
|
55
59
|
|
|
56
60
|
## What you get
|
|
57
61
|
|
|
@@ -103,9 +107,10 @@ Native Python hardware profile cascades are available for deployment manifests:
|
|
|
103
107
|
AbstractCore and AbstractRuntime profiles; AbstractAgent itself remains
|
|
104
108
|
provider/runtime agnostic.
|
|
105
109
|
|
|
106
|
-
AbstractAgent 0.3.
|
|
107
|
-
`AbstractRuntime>=0.
|
|
108
|
-
in-flight cancellation and speculation
|
|
110
|
+
AbstractAgent 0.3.15 requires `abstractcore[tools]>=2.13.41` and
|
|
111
|
+
`AbstractRuntime>=0.5.0` (the runtime release that provides live token
|
|
112
|
+
streaming, on top of turn grounding, in-flight cancellation and speculation
|
|
113
|
+
inheritance). AbstractRuntime 0.5.0 itself requires AbstractCore 2.16.0 or newer.
|
|
109
114
|
|
|
110
115
|
Note: the repository may be ahead of the latest published PyPI release. To verify what you installed:
|
|
111
116
|
|
|
@@ -129,8 +134,7 @@ print(state.output["answer"])
|
|
|
129
134
|
Tip: these loops send the full transcript plus ~19 tool schemas every cycle —
|
|
130
135
|
prefer a tool-capable model. On Ollama, raise the context window to the model's
|
|
131
136
|
maximum available context or the server silently truncates from the oldest
|
|
132
|
-
content first: per-call `llm_kwargs={"num_ctx": <model max>}`
|
|
133
|
-
2026-07-13) or server-side `OLLAMA_CONTEXT_LENGTH=<model max> ollama serve`.
|
|
137
|
+
content first: per-call `llm_kwargs={"num_ctx": <model max>}` or server-side `OLLAMA_CONTEXT_LENGTH=<model max> ollama serve`.
|
|
134
138
|
House rule: maximum available context unless you explicitly choose otherwise —
|
|
135
139
|
a fixed lower number is a hidden ceiling (see [`docs/faq.md`](docs/faq.md)).
|
|
136
140
|
|
|
@@ -25,12 +25,14 @@ flowchart LR
|
|
|
25
25
|
- Getting started: [`docs/getting-started.md`](docs/getting-started.md)
|
|
26
26
|
- API reference: [`docs/api.md`](docs/api.md)
|
|
27
27
|
- Loop hooks (listen/steer/capture): [`docs/hooks.md`](docs/hooks.md)
|
|
28
|
-
- FAQ
|
|
28
|
+
- FAQ: [`docs/faq.md`](docs/faq.md)
|
|
29
|
+
- Troubleshooting: [`docs/troubleshooting.md`](docs/troubleshooting.md)
|
|
29
30
|
- Architecture (diagrams): [`docs/architecture.md`](docs/architecture.md)
|
|
30
31
|
- Changelog: [`CHANGELOG.md`](CHANGELOG.md)
|
|
31
32
|
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
32
33
|
- Security: [`SECURITY.md`](SECURITY.md)
|
|
33
34
|
- Acknowledgements: [`ACKNOWLEDMENTS.md`](ACKNOWLEDMENTS.md)
|
|
35
|
+
- Code of conduct: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
|
|
34
36
|
|
|
35
37
|
## What you get
|
|
36
38
|
|
|
@@ -82,9 +84,10 @@ Native Python hardware profile cascades are available for deployment manifests:
|
|
|
82
84
|
AbstractCore and AbstractRuntime profiles; AbstractAgent itself remains
|
|
83
85
|
provider/runtime agnostic.
|
|
84
86
|
|
|
85
|
-
AbstractAgent 0.3.
|
|
86
|
-
`AbstractRuntime>=0.
|
|
87
|
-
in-flight cancellation and speculation
|
|
87
|
+
AbstractAgent 0.3.15 requires `abstractcore[tools]>=2.13.41` and
|
|
88
|
+
`AbstractRuntime>=0.5.0` (the runtime release that provides live token
|
|
89
|
+
streaming, on top of turn grounding, in-flight cancellation and speculation
|
|
90
|
+
inheritance). AbstractRuntime 0.5.0 itself requires AbstractCore 2.16.0 or newer.
|
|
88
91
|
|
|
89
92
|
Note: the repository may be ahead of the latest published PyPI release. To verify what you installed:
|
|
90
93
|
|
|
@@ -108,8 +111,7 @@ print(state.output["answer"])
|
|
|
108
111
|
Tip: these loops send the full transcript plus ~19 tool schemas every cycle —
|
|
109
112
|
prefer a tool-capable model. On Ollama, raise the context window to the model's
|
|
110
113
|
maximum available context or the server silently truncates from the oldest
|
|
111
|
-
content first: per-call `llm_kwargs={"num_ctx": <model max>}`
|
|
112
|
-
2026-07-13) or server-side `OLLAMA_CONTEXT_LENGTH=<model max> ollama serve`.
|
|
114
|
+
content first: per-call `llm_kwargs={"num_ctx": <model max>}` or server-side `OLLAMA_CONTEXT_LENGTH=<model max> ollama serve`.
|
|
113
115
|
House rule: maximum available context unless you explicitly choose otherwise —
|
|
114
116
|
a fixed lower number is a hidden ceiling (see [`docs/faq.md`](docs/faq.md)).
|
|
115
117
|
|
|
@@ -4,29 +4,32 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "abstractagent"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.15"
|
|
8
8
|
description = "Agent implementations using AbstractRuntime and AbstractCore"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"abstractcore[tools]>=2.13.41",
|
|
13
|
-
"abstractruntime>=0.
|
|
13
|
+
"abstractruntime>=0.5.0",
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
[project.optional-dependencies]
|
|
17
17
|
apple = [
|
|
18
18
|
"abstractcore[all-apple]>=2.13.41",
|
|
19
|
-
"abstractruntime[apple]>=0.
|
|
19
|
+
"abstractruntime[apple]>=0.5.0",
|
|
20
20
|
]
|
|
21
21
|
gpu = [
|
|
22
22
|
"abstractcore[all-gpu]>=2.13.41",
|
|
23
|
-
"abstractruntime[gpu]>=0.
|
|
23
|
+
"abstractruntime[gpu]>=0.5.0",
|
|
24
24
|
]
|
|
25
25
|
dev = [
|
|
26
26
|
"pytest>=7.0",
|
|
27
|
+
"jinja2>=3.1",
|
|
27
28
|
]
|
|
28
29
|
test = [
|
|
29
30
|
"pytest>=7.0",
|
|
31
|
+
# Renders the vendored Qwen3.6 chat template (tests/test_announced_tool_reprompt.py).
|
|
32
|
+
"jinja2>=3.1",
|
|
30
33
|
]
|
|
31
34
|
|
|
32
35
|
[tool.setuptools.packages.find]
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Replies that announce tool use but carry no runnable call (mission AGX, 2026-09-26).
|
|
2
|
+
|
|
3
|
+
Two shapes end a loop step with NO tool call while the model clearly meant to
|
|
4
|
+
act. Before this module both became the run's final answer:
|
|
5
|
+
|
|
6
|
+
1. ANNOUNCED — a short intent sentence and nothing else. Evidence
|
|
7
|
+
(untracked/missions-2026-09-25/XP/REPORT.md, Qwen3.8-27B on MLX): at
|
|
8
|
+
iteration 3 the model wrote "I have strong material. Let me verify a couple
|
|
9
|
+
of key specifics (oil price level, gold, ...) before writing the digest."
|
|
10
|
+
and stopped, 5/5 gateway runs and 15/15 provider-level generations. The run
|
|
11
|
+
"completed" with that sentence as its answer and no digest.
|
|
12
|
+
2. UNRUNNABLE — tool-call markup the provider could not turn into a call: a
|
|
13
|
+
tool name that was not offered, an envelope cut off mid-parameter, calls
|
|
14
|
+
drafted inside the thinking block that abstractcore >= 2.16.0 could not
|
|
15
|
+
recover (it recovers the clean trailing ones itself and reports the rest in
|
|
16
|
+
`metadata.warnings` / `metadata.unparsed_tool_call`). Published as an
|
|
17
|
+
answer, the user sees raw markup (backlog 0918).
|
|
18
|
+
|
|
19
|
+
The fix is ONE re-prompt that shows the model its own reply VERBATIM. The XP
|
|
20
|
+
evidence is specific about the verbatim part: re-prompting with the raw reply
|
|
21
|
+
made the model re-issue the same calls as visible calls 5/5; re-prompting with
|
|
22
|
+
the reply as the runtime records it (content "") made it believe the tools had
|
|
23
|
+
already run, 5/5. A second failure ends the step with a visible error — the
|
|
24
|
+
announcement or the markup is never published as the answer.
|
|
25
|
+
|
|
26
|
+
Pure functions only; each loop adapter owns its own routing.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
33
|
+
|
|
34
|
+
from .transcripts import extract_reasoning_text
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"NOT_AN_ANSWER_KINDS",
|
|
38
|
+
"REASON_ANNOUNCED",
|
|
39
|
+
"REASON_UNRUNNABLE",
|
|
40
|
+
"classify_no_call_reply",
|
|
41
|
+
"has_negated_intent",
|
|
42
|
+
"is_not_an_answer",
|
|
43
|
+
"resolve_checks",
|
|
44
|
+
"looks_like_tool_announcement",
|
|
45
|
+
"no_call_error_text",
|
|
46
|
+
"no_call_stop_reason",
|
|
47
|
+
"quote_reply",
|
|
48
|
+
"reprompt_text",
|
|
49
|
+
"tool_calls_from_reasoning",
|
|
50
|
+
"verbatim_reply",
|
|
51
|
+
"visible_content",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
REASON_ANNOUNCED = "announced_tool_use"
|
|
55
|
+
REASON_UNRUNNABLE = "unrunnable_tool_call"
|
|
56
|
+
|
|
57
|
+
# Replies at or above this many prose characters are never judged
|
|
58
|
+
# "announcement-only": a real answer is longer than one intent sentence, and a
|
|
59
|
+
# false positive costs a model turn and, twice, the answer itself.
|
|
60
|
+
ANNOUNCEMENT_MAX_CHARS = 300
|
|
61
|
+
|
|
62
|
+
_FENCED_BLOCK_RE = re.compile(r"(?s)```[^\n`]*\n.*?```")
|
|
63
|
+
_THINK_BLOCK_RE = re.compile(r"(?is)<think>.*?(</think>|$)")
|
|
64
|
+
|
|
65
|
+
# Openers of the tool-call envelopes the prompted parsers understand (mirrors
|
|
66
|
+
# abstractcore's `_TOOL_ENVELOPE_OPENER_RE`), plus Qwen's bare `<function=`.
|
|
67
|
+
_MARKUP_RE = re.compile(
|
|
68
|
+
r"(?i)<tool_call\b|<\|tool_call\|>|<\|tool_call>|<\|tool_call_start\|>|<function_call>|```tool_code|<function=[\w.:-]+>"
|
|
69
|
+
)
|
|
70
|
+
_MARKUP_STRIP_RE = re.compile(
|
|
71
|
+
r"(?is)"
|
|
72
|
+
r"<tool_call\b.*?(</tool_call>|$)|"
|
|
73
|
+
r"<\|tool_call\|>.*?(<\|/tool_call\|>|$)|"
|
|
74
|
+
r"<\|tool_call>.*?(<tool_call\|>|$)|"
|
|
75
|
+
r"<\|tool_call_start\|>.*?(<\|tool_call_end\|>|$)|"
|
|
76
|
+
r"<function_call>.*?(</function_call>|$)|"
|
|
77
|
+
r"```tool_code.*?(```|$)|"
|
|
78
|
+
r"<function=[\w.:-]+>.*?(</function>|$)"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# abstractcore's warning sentences for tool syntax that produced no call
|
|
82
|
+
# (providers/base.py `_warn_unrecognized_tool_syntax`,
|
|
83
|
+
# `_recover_tool_calls_from_reasoning`; streaming.py unparsed envelopes).
|
|
84
|
+
_TOOL_WARNING_RE = re.compile(r"(?i)^\s*(tool call not recognized|tool-call syntax|unparsed tool call)")
|
|
85
|
+
_UNKNOWN_NAMES_RE = re.compile(r"the model called ((?:'[^']*'(?:, )?)+)")
|
|
86
|
+
|
|
87
|
+
# First-person, forward-looking intent. The apostrophe is REQUIRED in "I'll":
|
|
88
|
+
# the optional-apostrophe spelling matches the word "ill".
|
|
89
|
+
_INTENT_RE = re.compile(
|
|
90
|
+
r"(?i)\b(let me|let['’]s|i will|i['’]ll|i am going to|i['’]m going to|i (?:just )?need to|i should|next,? i|now i)\b"
|
|
91
|
+
)
|
|
92
|
+
_NEGATED_INTENT_RE = re.compile(
|
|
93
|
+
r"(?i)\b(i will not|i won['’]t|i['’]ll not|i cannot|i can['’]t|i do not need to|i don['’]t need to|"
|
|
94
|
+
r"i need to stop|i should not|i shouldn['’]t|no need to)\b"
|
|
95
|
+
)
|
|
96
|
+
# Verbs that name work a tool would do. Broader than the long-reply heuristic
|
|
97
|
+
# in react_runtime (`_DEFERRED_ACTION_VERB_RE`) because it only applies to
|
|
98
|
+
# SHORT replies: every XP failure used one of verify/get/gather/confirm/fill/
|
|
99
|
+
# pull/do ... pass/check, none of which the old list knew.
|
|
100
|
+
_TOOL_VERBS = (
|
|
101
|
+
r"read|open|search|list|skim|inspect|explore|scan|run|execute|edit|fetch|download|creat(?:e|ing)|"
|
|
102
|
+
r"verify|check|double-check|confirm|validate|get|gather|grab|pull|look(?:\s+up|\s+into|\s+at)?|find|"
|
|
103
|
+
r"research|dig|fill|collect|query|call|test|browse|visit|retrieve|load|review|investigate|grep|"
|
|
104
|
+
r"examine|analy[sz]e|compare|write|save|update|modify|patch|install|build|compile|apply|"
|
|
105
|
+
r"do (?:one|a|another|two|some)"
|
|
106
|
+
)
|
|
107
|
+
_TOOL_VERB_RE = re.compile(r"(?i)\b(" + _TOOL_VERBS + r")\b")
|
|
108
|
+
# The model waiting on the user is a legitimate final reply.
|
|
109
|
+
_WAITING_RE = re.compile(
|
|
110
|
+
r"(?i)\b(let me know|what would you like|would you like|do you want|tell me|shall i|should i|"
|
|
111
|
+
r"i['’]ll wait|i will wait|waiting for|if you (?:want|need|like|prefer)|"
|
|
112
|
+
r"once you|when you|after you|unless you|until you|if you say|your (?:go|confirmation|approval))\b"
|
|
113
|
+
)
|
|
114
|
+
# Intent followed by a verb that TALKS rather than acts: "Let me summarize:",
|
|
115
|
+
# "Next, I recommend ...". Never an announcement of tool work.
|
|
116
|
+
_NON_ACTION_AFTER_INTENT_RE = re.compile(
|
|
117
|
+
r"(?i)\b(?:let me|let['’]s|i will|i['’]ll|i am going to|i['’]m going to|i should|next,? i|now i)\s+"
|
|
118
|
+
r"(?:also\s+|briefly\s+|just\s+)?"
|
|
119
|
+
r"(?:summari[sz]e|explain|recommend|suggest|mention|note|clarify|add|point out|conclude|answer|say|"
|
|
120
|
+
r"be (?:clear|brief|honest)|start by saying|wrap up)\b"
|
|
121
|
+
)
|
|
122
|
+
_PROGRESSIVE_TOOL_VERBS = (
|
|
123
|
+
r"(?:checking|fetching|searching|looking up|reading|running|verifying|pulling|gathering|grabbing|opening|"
|
|
124
|
+
r"loading|querying|downloading|inspecting|scanning|grepping|confirming|retrieving)"
|
|
125
|
+
)
|
|
126
|
+
# Present-progressive or elliptical action statements with no first person:
|
|
127
|
+
# "Checking the remaining two sources now.", "Proceeding to fetch ...",
|
|
128
|
+
# "One more search to confirm ..., then the digest.", "First, a quick search for ...",
|
|
129
|
+
# "Okay, fetching the Reuters article next.", "Time to check the CI logs.".
|
|
130
|
+
_ACTION_STATEMENT_RE = re.compile(
|
|
131
|
+
r"(?i)^(?:"
|
|
132
|
+
r"proceeding to \w+|"
|
|
133
|
+
+ _PROGRESSIVE_TOOL_VERBS
|
|
134
|
+
+ r"\b.*\b(?:now|next|then|first)\b|"
|
|
135
|
+
r"(?:okay|ok),?\s+" + _PROGRESSIVE_TOOL_VERBS + r"\b|"
|
|
136
|
+
r"time to (?:" + _TOOL_VERBS + r")\b|"
|
|
137
|
+
r"(?:one|two|a few) more (?:search|searches|check|checks|lookup|lookups|fetch|fetches|pass|read|reads)\b|"
|
|
138
|
+
r"first,? (?:a|one) (?:quick )?(?:search|check|lookup|fetch|pass|read)\b|"
|
|
139
|
+
r"i need (?:a bit |a little |some )?more (?:data|info|information|detail|details|context|evidence|sources)\b"
|
|
140
|
+
r")"
|
|
141
|
+
)
|
|
142
|
+
_SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!])\s+")
|
|
143
|
+
# A reply that STARTS by delivering is a final answer, whatever intent words follow.
|
|
144
|
+
_DELIVERY_START_RE = re.compile(
|
|
145
|
+
r"(?i)^\s*(done|finished|completed|all set|all done|here (?:is|are)|here['’]s|final answer|in summary|"
|
|
146
|
+
r"to summarize|summary)\b"
|
|
147
|
+
)
|
|
148
|
+
_HEADING_RE = re.compile(r"(?m)^(#{1,6}\s+\S|\*\*\S)")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Transcript message kinds that hold a FAILED reply. Budget terminals that pick
|
|
152
|
+
# "the agent's last words" from the transcript must skip them — they are the
|
|
153
|
+
# replies this module exists to keep from being published.
|
|
154
|
+
NOT_AN_ANSWER_KINDS = frozenset({"reprompted_reply", "reprompt_failed_reply"})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_not_an_answer(message: Any) -> bool:
|
|
158
|
+
meta = message.get("metadata") if isinstance(message, dict) else None
|
|
159
|
+
return isinstance(meta, dict) and meta.get("kind") in NOT_AN_ANSWER_KINDS
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _flag(raw: Any) -> bool:
|
|
163
|
+
if isinstance(raw, str):
|
|
164
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
165
|
+
return bool(raw)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def resolve_checks(runtime_ns: Any, *, suppressed: bool) -> Tuple[bool, bool]:
|
|
169
|
+
"""(check_announcement, check_unrunnable), two independent switches.
|
|
170
|
+
|
|
171
|
+
- `_runtime.check_plan` controls ONLY the announcement heuristic (a guess
|
|
172
|
+
about intent): absent = ON in task lanes, OFF in visit lanes, where
|
|
173
|
+
"I will read that entry again" is musing.
|
|
174
|
+
- `_runtime.check_unrunnable_calls` controls the unrunnable-markup check
|
|
175
|
+
(a fact about the reply): absent = ON in every lane. Turning the
|
|
176
|
+
heuristic off must never re-enable publishing raw tool markup.
|
|
177
|
+
"""
|
|
178
|
+
ns = runtime_ns if isinstance(runtime_ns, dict) else {}
|
|
179
|
+
raw_plan = ns.get("check_plan")
|
|
180
|
+
check_announcement = (not suppressed) if raw_plan is None else _flag(raw_plan)
|
|
181
|
+
raw_unrunnable = ns.get("check_unrunnable_calls")
|
|
182
|
+
check_unrunnable = True if raw_unrunnable is None else _flag(raw_unrunnable)
|
|
183
|
+
return check_announcement, check_unrunnable
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _prose(text: Any) -> str:
|
|
187
|
+
s = str(text or "")
|
|
188
|
+
s = _FENCED_BLOCK_RE.sub("", s)
|
|
189
|
+
return s.strip()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _strip_markup(text: str) -> str:
|
|
193
|
+
try:
|
|
194
|
+
return _MARKUP_STRIP_RE.sub("", text or "").strip()
|
|
195
|
+
except Exception:
|
|
196
|
+
return str(text or "").strip()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _metadata(response: Any) -> Dict[str, Any]:
|
|
200
|
+
meta = response.get("metadata") if isinstance(response, dict) else None
|
|
201
|
+
return meta if isinstance(meta, dict) else {}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def tool_calls_from_reasoning(response: Any) -> int:
|
|
205
|
+
"""How many of this reply's tool calls abstractcore recovered from the thinking block."""
|
|
206
|
+
try:
|
|
207
|
+
n = int(_metadata(response).get("tool_calls_from_reasoning") or 0)
|
|
208
|
+
except (TypeError, ValueError):
|
|
209
|
+
return 0
|
|
210
|
+
return n if n > 0 else 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def has_negated_intent(text: Any) -> bool:
|
|
214
|
+
""""I will not run that", "I can't ...": a refusal, never an announcement."""
|
|
215
|
+
return bool(_NEGATED_INTENT_RE.search(_prose(text)))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _last_sentence(prose: str) -> str:
|
|
219
|
+
lines = [ln.strip() for ln in prose.splitlines() if ln.strip()]
|
|
220
|
+
last_line = lines[-1] if lines else prose
|
|
221
|
+
parts = [p.strip() for p in _SENTENCE_SPLIT_RE.split(last_line) if p.strip()]
|
|
222
|
+
return parts[-1] if parts else last_line
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def looks_like_tool_announcement(text: Any) -> bool:
|
|
226
|
+
"""True when a SHORT reply ENDS on an announcement of tool work.
|
|
227
|
+
|
|
228
|
+
Conservative on purpose — a false positive costs a model turn and, twice,
|
|
229
|
+
the answer. It fires only when the prose (fences and markup removed) is
|
|
230
|
+
shorter than `ANNOUNCEMENT_MAX_CHARS` and its LAST sentence is the
|
|
231
|
+
announcement: first-person intent followed by a tool verb ("Let me verify
|
|
232
|
+
..."), or an action statement ("Checking the remaining two sources now.").
|
|
233
|
+
Every XP failure ends that way. Never an announcement: questions,
|
|
234
|
+
replies waiting on the user ("... unless you say otherwise", "once you
|
|
235
|
+
confirm"), intent that talks rather than acts ("Let me summarize: ..."),
|
|
236
|
+
refusals ("I will not run that"), replies that start by delivering
|
|
237
|
+
("Done.", "Here is ..."), structured answers. English only (documented
|
|
238
|
+
limit: docs/agents.md, backlog 0033).
|
|
239
|
+
"""
|
|
240
|
+
prose = _strip_markup(_prose(text))
|
|
241
|
+
if not prose or len(prose) >= ANNOUNCEMENT_MAX_CHARS:
|
|
242
|
+
return False
|
|
243
|
+
if prose.rstrip().endswith("?"):
|
|
244
|
+
return False
|
|
245
|
+
if _WAITING_RE.search(prose) or _DELIVERY_START_RE.search(prose) or _HEADING_RE.search(prose):
|
|
246
|
+
return False
|
|
247
|
+
if _NEGATED_INTENT_RE.search(prose):
|
|
248
|
+
return False
|
|
249
|
+
last = _last_sentence(prose)
|
|
250
|
+
if _ACTION_STATEMENT_RE.search(last):
|
|
251
|
+
return True
|
|
252
|
+
intent = _INTENT_RE.search(last)
|
|
253
|
+
if not intent:
|
|
254
|
+
return False
|
|
255
|
+
if _NON_ACTION_AFTER_INTENT_RE.search(last):
|
|
256
|
+
return False
|
|
257
|
+
return bool(_TOOL_VERB_RE.search(last[intent.end():]))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _unrunnable_detail(response: Any, content: str) -> Optional[str]:
|
|
261
|
+
"""Name the tool syntax that produced no call, or None when there is none."""
|
|
262
|
+
meta = _metadata(response)
|
|
263
|
+
unparsed = meta.get("unparsed_tool_call")
|
|
264
|
+
if unparsed:
|
|
265
|
+
reason = unparsed.get("reason") if isinstance(unparsed, dict) else None
|
|
266
|
+
return f"the tool call was cut off or malformed ({reason})" if reason else "the tool call was cut off or malformed"
|
|
267
|
+
warnings = meta.get("warnings")
|
|
268
|
+
if isinstance(warnings, list):
|
|
269
|
+
for w in warnings:
|
|
270
|
+
if not isinstance(w, str) or not _TOOL_WARNING_RE.search(w):
|
|
271
|
+
continue
|
|
272
|
+
names = _UNKNOWN_NAMES_RE.search(w)
|
|
273
|
+
if names:
|
|
274
|
+
return f"the model asked for {names.group(1)}, which is not an available tool"
|
|
275
|
+
if "inside the model's reasoning" in w:
|
|
276
|
+
return "the tool calls were written inside the model's thinking, where they cannot run"
|
|
277
|
+
return "the tool-call syntax could not be parsed"
|
|
278
|
+
raw_content = str(response.get("content") or "") if isinstance(response, dict) else content
|
|
279
|
+
if _MARKUP_RE.search(_prose(_THINK_BLOCK_RE.sub("", raw_content))):
|
|
280
|
+
return "the reply contains tool-call markup that did not become a tool call"
|
|
281
|
+
# The thinking block only counts when there is no visible reply: a model
|
|
282
|
+
# that considered a call while thinking and then answered has answered.
|
|
283
|
+
if not _THINK_BLOCK_RE.sub("", raw_content).strip():
|
|
284
|
+
for text in (raw_content, content, extract_reasoning_text(response)):
|
|
285
|
+
if _MARKUP_RE.search(_prose(text)):
|
|
286
|
+
return "the tool calls were written inside the model's thinking, where they cannot run"
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def classify_no_call_reply(
|
|
291
|
+
response: Any,
|
|
292
|
+
content: Any,
|
|
293
|
+
*,
|
|
294
|
+
tools_offered: bool,
|
|
295
|
+
check_announcement: bool = True,
|
|
296
|
+
check_unrunnable: bool = True,
|
|
297
|
+
) -> Tuple[str, str]:
|
|
298
|
+
"""Classify a reply that carried NO tool call: (reason, detail) or ("", "").
|
|
299
|
+
|
|
300
|
+
`content` is the loop's parsed content (which may already be the reasoning
|
|
301
|
+
fallback when the visible content was empty). The caller must only ask
|
|
302
|
+
about replies whose tool-call list is empty.
|
|
303
|
+
"""
|
|
304
|
+
text = str(content or "")
|
|
305
|
+
raw_content = str(response.get("content") or "") if isinstance(response, dict) else text
|
|
306
|
+
visible = _strip_markup(_prose(_THINK_BLOCK_RE.sub("", raw_content)))
|
|
307
|
+
|
|
308
|
+
if check_unrunnable:
|
|
309
|
+
detail = _unrunnable_detail(response, text)
|
|
310
|
+
# Markup/warnings with no visible answer, or with only a short one: the
|
|
311
|
+
# reply is the failed call. A long visible answer that merely also
|
|
312
|
+
# carries markup (docs about tool syntax, say) stays an answer.
|
|
313
|
+
if detail and (not visible or len(visible) < ANNOUNCEMENT_MAX_CHARS):
|
|
314
|
+
if not tools_offered:
|
|
315
|
+
detail = f"{detail}; no tools are available in this step"
|
|
316
|
+
return REASON_UNRUNNABLE, detail
|
|
317
|
+
|
|
318
|
+
if check_announcement and tools_offered and looks_like_tool_announcement(text):
|
|
319
|
+
return REASON_ANNOUNCED, "the reply announced tool use but contained no tool call"
|
|
320
|
+
return "", ""
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def visible_content(response: Any) -> str:
|
|
324
|
+
"""The reply's visible content exactly as the provider returned it (may be "")."""
|
|
325
|
+
return str(response.get("content") or "") if isinstance(response, dict) else ""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def verbatim_reply(response: Any, *, fallback: str = "") -> str:
|
|
329
|
+
"""The assistant's reply as the provider returned it: reasoning AND content.
|
|
330
|
+
|
|
331
|
+
Never the emptied record: the runtime's recorded turn for a think-held
|
|
332
|
+
call is `content: ""`, and a model shown that believes its tools ran.
|
|
333
|
+
Reasoning is re-wrapped in `<think>` (the tag every thinking family this
|
|
334
|
+
framework serves uses); an unparsed envelope that the provider reported in
|
|
335
|
+
metadata instead of content is appended so the model sees it too.
|
|
336
|
+
"""
|
|
337
|
+
reasoning = extract_reasoning_text(response).strip()
|
|
338
|
+
content = str(response.get("content") or "").strip() if isinstance(response, dict) else ""
|
|
339
|
+
parts: List[str] = []
|
|
340
|
+
if reasoning:
|
|
341
|
+
parts.append(f"<think>\n{reasoning}\n</think>")
|
|
342
|
+
if content:
|
|
343
|
+
parts.append(content)
|
|
344
|
+
unparsed = _metadata(response).get("unparsed_tool_call")
|
|
345
|
+
if isinstance(unparsed, dict):
|
|
346
|
+
utext = str(unparsed.get("text") or "").strip()
|
|
347
|
+
if utext and utext not in reasoning and utext not in content:
|
|
348
|
+
parts.append(utext)
|
|
349
|
+
out = "\n\n".join(parts).strip()
|
|
350
|
+
return out or str(fallback or "").strip()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def quote_reply(reply: str) -> str:
|
|
354
|
+
"""Fence `reply` so it survives as quoted DATA inside a user message."""
|
|
355
|
+
longest = max((len(m) for m in re.findall(r"`+", reply or "")), default=0)
|
|
356
|
+
fence = "`" * max(3, longest + 1)
|
|
357
|
+
return f"{fence}text\n{reply}\n{fence}"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def reprompt_text(
|
|
361
|
+
reason: str,
|
|
362
|
+
*,
|
|
363
|
+
tools_offered: bool,
|
|
364
|
+
suppressed: bool = False,
|
|
365
|
+
reply: str = "",
|
|
366
|
+
) -> str:
|
|
367
|
+
"""The corrective user message, with the failed reply QUOTED inside it.
|
|
368
|
+
|
|
369
|
+
The reply rides here, not (only) in the assistant history turn: Qwen3.5 /
|
|
370
|
+
Qwen3.6 chat templates strip `<think>…</think>` from every assistant turn
|
|
371
|
+
before the last user message, so a think-held call carried by the history
|
|
372
|
+
turn reaches the model as an EMPTY reply — the XP "as recorded" case that
|
|
373
|
+
made the model believe its tools had run (5/5). User content is rendered
|
|
374
|
+
verbatim by every template.
|
|
375
|
+
"""
|
|
376
|
+
quoted = f"Your previous reply was, verbatim:\n{quote_reply(reply)}\n\n" if reply else ""
|
|
377
|
+
if suppressed and reason == REASON_UNRUNNABLE:
|
|
378
|
+
# Visit lanes: host-voiced, no loop vocabulary (c2447 lane honesty).
|
|
379
|
+
# (The announcement check only runs there on an EXPLICIT
|
|
380
|
+
# `check_plan=true`, which asks for the task-lane wording.)
|
|
381
|
+
quoted = f"This is what came through:\n{quote_reply(reply)}\n\n" if reply else ""
|
|
382
|
+
return quoted + (
|
|
383
|
+
"It didn't come through as words — part of it was in a form that can't be carried "
|
|
384
|
+
"out here, so nothing happened. Say it again: do what you meant to do now, or "
|
|
385
|
+
"reply in plain words."
|
|
386
|
+
)
|
|
387
|
+
if not tools_offered:
|
|
388
|
+
return quoted + (
|
|
389
|
+
"That reply contained tool-call markup, but no tools are available here, so nothing "
|
|
390
|
+
"ran. Reply again with a direct answer, without tool-call markup."
|
|
391
|
+
)
|
|
392
|
+
if reason == REASON_UNRUNNABLE:
|
|
393
|
+
return quoted + (
|
|
394
|
+
"That reply placed tool calls where they cannot run (inside your thinking, with an "
|
|
395
|
+
"unknown tool name, or cut off), so none ran. Reply again: call the tools now in the "
|
|
396
|
+
"required format as your visible reply, or answer directly."
|
|
397
|
+
)
|
|
398
|
+
return quoted + (
|
|
399
|
+
"That reply announced tool calls but none ran: it contained no tool call. Reply again: "
|
|
400
|
+
"call the tools now in the required format as your visible reply, or answer directly."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def no_call_error_text(reason: str, detail: str, *, reprompted: bool = True) -> str:
|
|
405
|
+
"""The visible error that ends a step whose no-call reply could not be recovered."""
|
|
406
|
+
what = (
|
|
407
|
+
"tool calls it could not run"
|
|
408
|
+
if reason == REASON_UNRUNNABLE
|
|
409
|
+
else "tool use without calling any tool"
|
|
410
|
+
)
|
|
411
|
+
tail = f" ({detail})" if detail else ""
|
|
412
|
+
how = (
|
|
413
|
+
"and it did the same again after one re-prompt"
|
|
414
|
+
if reprompted
|
|
415
|
+
else "and no iteration was left to re-prompt it"
|
|
416
|
+
)
|
|
417
|
+
return (
|
|
418
|
+
f"Error: the model's reply announced {what}{tail}, {how}. "
|
|
419
|
+
"Nothing ran, and the reply was not used as an answer."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def no_call_stop_reason(no_call: Dict[str, Any], *, iterations: int) -> Dict[str, Any]:
|
|
424
|
+
"""Host-facing `stop_reason` for a turn ended by an unrecovered no-call reply.
|
|
425
|
+
|
|
426
|
+
One wording for all three loops (hosts render it; they never re-derive it).
|
|
427
|
+
`reprompted: False` means the reply came on the last iteration, so
|
|
428
|
+
`budget_exhausted` is True as well.
|
|
429
|
+
"""
|
|
430
|
+
unrunnable = str(no_call.get("reason") or "") == REASON_UNRUNNABLE
|
|
431
|
+
reprompted = bool(no_call.get("reprompted", True))
|
|
432
|
+
detail = str(no_call.get("detail") or "").strip()
|
|
433
|
+
iters_txt = f" after {iterations} iterations" if iterations > 0 else ""
|
|
434
|
+
what = "wrote tool calls that could not run" if unrunnable else "announced tool use without calling any tool"
|
|
435
|
+
again = (
|
|
436
|
+
", and did the same after one re-prompt, so nothing ran."
|
|
437
|
+
if reprompted
|
|
438
|
+
else ", with no iteration left to re-prompt it, so nothing ran."
|
|
439
|
+
)
|
|
440
|
+
return {
|
|
441
|
+
"code": "no_tool_call",
|
|
442
|
+
"finished": False,
|
|
443
|
+
# Not re-prompted = the last iteration: the budget is spent too.
|
|
444
|
+
"budget_exhausted": not reprompted,
|
|
445
|
+
"iterations": iterations,
|
|
446
|
+
"label": (
|
|
447
|
+
f"stopped: tool call could not run{iters_txt}"
|
|
448
|
+
if unrunnable
|
|
449
|
+
else f"stopped: announced tools, made no call{iters_txt}"
|
|
450
|
+
),
|
|
451
|
+
"headline": (
|
|
452
|
+
(f"The agent stopped early{iters_txt}" if reprompted else f"The agent used its last iteration{iters_txt}")
|
|
453
|
+
+ f": the model {what}"
|
|
454
|
+
+ (f" ({detail})" if detail else "")
|
|
455
|
+
+ again
|
|
456
|
+
),
|
|
457
|
+
"remedy": (
|
|
458
|
+
"Check that the tool the model asked for is enabled for this agent, then retry."
|
|
459
|
+
if unrunnable
|
|
460
|
+
else "Retry; if it recurs, use another model or turn thinking off for this agent "
|
|
461
|
+
"(the model announces calls it does not emit)."
|
|
462
|
+
),
|
|
463
|
+
}
|