agentx-dev 3.2.0__tar.gz → 3.3.1__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 (72) hide show
  1. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/CHANGELOG.md +644 -498
  2. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/PKG-INFO +33 -1
  3. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/README.md +1095 -1063
  4. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Runner/AgentRun.py +2821 -2683
  5. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Runner/AsyncAgentRun.py +1053 -1008
  6. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Supervisor.py +1937 -1384
  7. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/__init__.py +2 -1
  8. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev.egg-info/PKG-INFO +33 -1
  9. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/pyproject.toml +70 -70
  10. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/AGENTX.md +0 -0
  11. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/CONTRIBUTING.md +0 -0
  12. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/LICENSE +0 -0
  13. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/MANIFEST.in +0 -0
  14. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Agents/Agent.py +0 -0
  15. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Agents/__init__.py +0 -0
  16. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/AsyncTools.py +0 -0
  17. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/AutoSetup.py +0 -0
  18. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Cache.py +0 -0
  19. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/ChatModel.py +0 -0
  20. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Compiler.py +0 -0
  21. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Config.py +0 -0
  22. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/DefaultTools.py +0 -0
  23. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Embeddings.py +0 -0
  24. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Evals.py +0 -0
  25. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Handoffs.py +0 -0
  26. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Loader.py +0 -0
  27. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/MCP.py +0 -0
  28. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Memory.py +0 -0
  29. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Observability.py +0 -0
  30. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Planner.py +0 -0
  31. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Runner/__init__.py +0 -0
  32. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Runner/promptTemplate.yaml +0 -0
  33. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Session.py +0 -0
  34. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Splitters.py +0 -0
  35. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Streaming.py +0 -0
  36. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/Tools.py +0 -0
  37. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/VectorStores/__init__.py +0 -0
  38. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/VectorStores/chroma_store.py +0 -0
  39. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/VectorStores/pg_store.py +0 -0
  40. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/VectorStores/qdrant_store.py +0 -0
  41. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/WebTools.py +0 -0
  42. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/resources/__init__.py +0 -0
  43. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev/resources/promptTemplate.yaml +0 -0
  44. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev.egg-info/SOURCES.txt +0 -0
  45. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev.egg-info/dependency_links.txt +0 -0
  46. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev.egg-info/requires.txt +0 -0
  47. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/agentx_dev.egg-info/top_level.txt +0 -0
  48. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/README.md +0 -0
  49. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/agentic_rag_demo.py +0 -0
  50. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/async_example.py +0 -0
  51. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/async_quickstart.py +0 -0
  52. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/auto_features_example.py +0 -0
  53. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/caching_example.py +0 -0
  54. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/chatbot_example.py +0 -0
  55. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/complete_example.py +0 -0
  56. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/concurrent_example.py +0 -0
  57. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/concurrent_tool_example.py +0 -0
  58. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/file_agent_demo.py +0 -0
  59. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/function_calling_demo.py +0 -0
  60. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/mcp_demo.py +0 -0
  61. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/mcp_github_triage_demo.py +0 -0
  62. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/observability_example.py +0 -0
  63. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/orchestration_demo.py +0 -0
  64. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/planner_example.py +0 -0
  65. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/robust_link_scraper.py +0 -0
  66. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/supervisor_codebase_analysis_demo.py +0 -0
  67. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/supervisor_example.py +0 -0
  68. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/sync_quickstart.py +0 -0
  69. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/v3_1_1_features_demo.py +0 -0
  70. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/v3_1_comprehensive_demo.py +0 -0
  71. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/examples/v3_1_features_demo.py +0 -0
  72. {agentx_dev-3.2.0 → agentx_dev-3.3.1}/setup.cfg +0 -0
@@ -1,498 +1,644 @@
1
- # Changelog
2
-
3
- All notable changes to `agentx-dev` are documented here. Format loosely
4
- follows [Keep a Changelog](https://keepachangelog.com/); versioning is
5
- [Semver](https://semver.org/).
6
-
7
- ## [3.2.0] - 2026-08-13
8
-
9
- Typed multi-agent pipelines. Specialists can now declare a Pydantic
10
- output schema once and pass validated instances to each other through
11
- the Supervisor, instead of downstream agents re-parsing prose.
12
-
13
- ### Added
14
-
15
- - **`output_schema` on the `AgentRunner` / `AsyncAgentRunner`
16
- constructor.** Declare the runner's output shape once
17
- (`AgentRunner(..., output_schema=QueryIntent)`) instead of passing it
18
- on every call or describing JSON in the prompt. A per-call
19
- `output_schema=` still wins when both are set. `None` keeps the exact
20
- pre-3.2 behaviour: no coercion, `completion.output` stays `None`.
21
-
22
- - **Schema coercion via forced native function calling.** When a schema
23
- is in play, the final answer is converted by forcing a provider-native
24
- tool call against the schema (constrained decoding), not by regexing
25
- JSON out of prose. The ReAct loop itself is untouched: tool selection
26
- and intermediate reasoning run exactly as before, and the coercion
27
- happens once, after the loop finishes. Models without a
28
- `call_with_tools` implementation fall back to the previous text-JSON
29
- parsing, so custom `BaseChatModel` subclasses keep working.
30
- `completion.content` keeps the human-readable answer alongside
31
- `completion.output` in every case.
32
-
33
- - **`SubtaskResult.output`.** The Supervisor now preserves each
34
- specialist's validated Pydantic instance next to its `content` text.
35
- Consumers that only read `content` are unaffected.
36
-
37
- - **Structured specialist-to-specialist handoff.** When an earlier step
38
- produced typed output, `_build_augmented_query` serializes it into the
39
- next specialist's context as a labelled JSON block
40
- (`STRUCTURED OUTPUT (QueryIntent): {...}`) followed by the summary
41
- text, so downstream steps parse fields rather than interpreting
42
- sentences like `INTENT: ... SEARCH_QUERY: ...`.
43
-
44
- - **`vector_search_tool` pipeline options.** New kwargs:
45
- `max_text_chars` (default 500; pass `0` for full untruncated passages,
46
- which a reranker judging evidence actually needs) and
47
- `structured_output` (default False; when True the tool returns a JSON
48
- array of `{id, text, vector_score, metadata}` instead of the
49
- human-formatted list). Defaults preserve existing behaviour byte-for-
50
- byte.
51
-
52
- ### Fixed
53
-
54
- - **Supervisor planning prompt contradicted the execution engine.** The
55
- planner rule said sub-agents "do NOT see previous steps' output" and
56
- discouraged dependency chains, but the dispatcher has threaded prior
57
- findings into every step since `_build_augmented_query` shipped.
58
- The rule now tells the planner that sequential steps receive earlier
59
- results (structured when available) and that chains like
60
- intent -> retrieval -> reranking are a good plan shape, while still
61
- requiring same-specialist steps to merge and banning report-only steps.
62
-
63
- ## [3.1.7] — 2026-07-27
64
-
65
- ### Changed
66
-
67
- - **`use_function_calling` default flipped to auto-detect** on
68
- `AgentRunner` / `AsyncAgentRunner`. The parameter's default type is
69
- now `Optional[bool] = None`; `None` resolves to `True` when the
70
- model class overrides `BaseChatModel.call_with_tools` (both `GPT`
71
- and `Claude` do) and to `False` when it doesn't (or when
72
- `bind_tools_natively=True`). Callers passing `True`/`False`
73
- explicitly are unaffected. Rationale: text-mode ReAct requires the
74
- model to emit strict JSON with any long `action_input` string
75
- properly escaped — a 1200-word markdown draft with unescaped
76
- newlines or quotes reliably breaks `json.loads` and killed the run.
77
- Function-calling mode routes the parser through the SDK's typed
78
- channel so escaping is handled automatically. The historical
79
- default (`False`) was the fragile option; the new default matches
80
- what most users actually want.
81
-
82
- ### Fixed
83
-
84
- - **Malformed parser JSON no longer crashes the run.** When the
85
- text-mode assistant response failed `json.loads` (typically because
86
- a long `action_input` string had unescaped `"`, `\n`, or backticks),
87
- the framework used to raise `JSONDecodeError` and unwind the whole
88
- invocation. The runner now (1) tries a regex-based salvage that
89
- extracts `{Thought, action, action_input}` from the raw text
90
- covering the common "outer envelope valid, inner string broke
91
- escaping" failure, and (2) if salvage fails, feeds a targeted fix
92
- hint back to the model (`"your last response was not valid JSON;
93
- emit …, escape newlines as \n"`) and continues the loop bounded
94
- by `max_iterations`. Exhaustion returns a clear framework message
95
- rather than an uncaught exception. Applied to both sync and async
96
- runners via a shared `_salvage_react_json` helper.
97
- The salvager's action-name regex is intentionally strict
98
- (`[A-Za-z_][A-Za-z0-9_.\- ]{0,79}`) so it can't hallucinate an
99
- "action" out of an unrelated `"key":"value"` pair inside malformed
100
- JSON.
101
-
102
- - **Verbose trace in `bind_tools_natively` mode now prints tool
103
- name + args + response.** Previously native runs showed blank
104
- `[tool.call.start]` / `[tool.call.complete]` pairs (the
105
- observability layer fires them without the trace context), so you
106
- couldn't tell which tool the model actually invoked or what came
107
- back. The runner now prints `[tool] Invoking '<name>' with args:
108
- <input>` and `[tool] Response: <preview>` (or `[tool] Error: ...`
109
- when the dispatch raised) in the post-dispatch loop, matching the
110
- format text-mode and function-calling mode use. Mirrored to the
111
- async runner.
112
-
113
- - **`web_fetch_tool(vector_store=...)` auto-ingests fetched pages into
114
- a vector store** instead of dumping raw HTML into the model's
115
- context. Fixes the TPM-limit trap: when a research agent fetches
116
- four articles in parallel (via ``multi_tool_use.parallel`` or
117
- native binding), the combined bodies can easily exceed 40k tokens
118
- and blow past a 30k TPM ceiling on the very next model call.
119
- New parameters on ``web_fetch_tool``:
120
-
121
- | Kwarg | Default | Effect |
122
- |---|---|---|
123
- | ``vector_store`` | ``None`` | When set, each fetch is HTML-stripped, chunked with ``TextSplitter``, embedded via the store's embeddings, and added with ``{src: url, chunk_index, total_chunks}`` metadata. The tool response becomes a compact summary (URL, byte count, chunk count, 240-char preview) — NOT the raw body. The model then calls ``vector_search`` / ``Rag`` to pull only the passages it needs. |
124
- | ``chunk_size`` | ``1500`` | Characters per chunk when ``vector_store`` is set. Ignored otherwise. |
125
- | ``chunk_overlap`` | ``200`` | Overlap between adjacent chunks so a fact spanning a boundary is still retrievable. Ignored otherwise. |
126
-
127
- Backwards-compatible: the positional ``cache_dir`` signature keeps
128
- working; `web_fetch_tool()` with no ``vector_store`` returns raw
129
- body as before. Ingest and cache_dir compose — enable both and get
130
- disk-cached full bodies AND searchable chunks. HTML stripping is
131
- minimal and dependency-free (regex-based: script/style blocks
132
- dropped whole, then tags stripped, whitespace collapsed) so the
133
- ingest path adds no new install dependency. On JSON/plain-text
134
- responses the stripper is a near no-op.
135
-
136
- The observation returned to the model shows topical coverage --
137
- first, middle, and last chunk previews (up to 3 samples,
138
- deduplicated for short pages) -- so the model can tell what
139
- topics the page actually covers, not just the intro paragraph.
140
- Without this the model would only see the page's opening and
141
- wouldn't know to query for topics discussed later in the same
142
- page. Explicit instruction in the observation ("query with
143
- SPECIFIC keywords from the topics above; do NOT re-fetch; do
144
- NOT ask for the full body") steers the model toward the RAG path
145
- on follow-up turns.
146
-
147
- - **`multi_tool_use.parallel` now reaches its dispatch path.**
148
- When GPT wanted to batch several tool calls into one turn (fetch N
149
- URLs concurrently, run M searches at once), it emitted OpenAI's
150
- synthetic `multi_tool_use.parallel` meta-tool. The registry's
151
- `_dispatch_multi_parallel` / `_adispatch_multi_parallel` handlers
152
- already knew how to unpack it, but the runner loop's known-tools
153
- guardrail rejected the name FIRST as unregistered — dumping the
154
- raw `{"tool_uses": [...]}` payload into the user-facing "final
155
- answer" and never invoking any of the nested calls. Added
156
- `multi_tool_use.parallel` to the recognized action set in both
157
- sync and async runners so the meta-tool flows through to dispatch
158
- and the existing unpackers run. Nested calls with the `functions.`
159
- prefix are normalized before dispatch (same as top-level FC
160
- calls), so the model can emit either shape.
161
-
162
- - **`Permissions.full_access` / `read_only` auto-wrap a bare string.**
163
- Passing `full_access("./workspace")` used to iterate the string
164
- into 11 single-character "subtrees" (Python's `list("./workspace")`)
165
- — every path check silently rejected because no real path could
166
- ever match a `"."` or `"/"` "allowed subtree". The classmethod
167
- now detects a bare string and treats it as `[allowed_paths]`, so
168
- `full_access("./workspace")` does the intuitive thing (equivalent
169
- to `full_access(["./workspace"])` and auto-infers the workspace).
170
- Same fix on `read_only`. List inputs are unchanged.
171
-
172
- - **`Permissions.full_access` now accepts (and auto-infers)
173
- `workspace`.** The classmethod set `allowed_paths` but not
174
- `workspace`, so short paths like `write_file(path="report.md")`
175
- resolved to CWD (outside the sandbox) and raised
176
- `PermissionError: access denied` — a landmine that every caller of
177
- `Permissions.full_access(["./workspace"])` hit sooner or later.
178
- New signature: `full_access(allowed_paths, *, workspace=None)`.
179
- When `workspace` isn't passed AND `allowed_paths` has exactly one
180
- entry, that path is auto-set as the workspace (the "project-scoped
181
- agent whose one allowed subtree IS its workspace" case, which is
182
- 99% of use). Two or more paths stay ambiguous and require an
183
- explicit `workspace=` if short-path resolution is wanted. Pass an
184
- explicit `workspace=` string to override the auto-choice.
185
- Backwards-compatible on the positional signature; adds a keyword
186
- argument that existing callers didn't use.
187
-
188
- ## [3.1.5] — 2026-07-26
189
-
190
- ### Fixed
191
-
192
- - **Text-mode tool results no longer use `role: "function"`.** In text
193
- mode (the default — no `use_function_calling`) the runner fed each tool
194
- observation back to the model as a `role: "function"` message. Newer
195
- OpenAI models reject that role outright (`400 … 'messages[N].role' does
196
- not support 'function' with this model`, e.g. gpt-5.x), and Anthropic
197
- never accepted it — text-mode multi-tool runs on Claude were latently
198
- broken too; older GPT models simply still tolerated the legacy role.
199
- Tool observations now go back as a plain `role: "user"` turn framed as
200
- `Observation: …`, which every provider and model generation accepts and
201
- which matches the ReAct template's own few-shot convention.
202
- Function-calling mode is unchanged (native `role: "tool"` +
203
- `tool_call_id`). The async runner was additionally emitting `function`
204
- unconditionally (even in FC mode); it now uses the same shared helper.
205
-
206
- ### Added
207
-
208
- - **`subtask_success_check` on `Supervisor` / `AsyncSupervisor`.** Opt-in
209
- predicate `(SubtaskResult) -> bool | str` that decides whether a
210
- *returned* (non-raised) sub-task result is actually acceptable — the
211
- "ran fine but produced nothing useful" case a plain retry can't catch
212
- (a scraper that saved 0 links, an extractor that found nothing). Return
213
- `True` to accept, or `False`/a `str` reason to reject; a rejected
214
- result is retried like a raised error, with the reason fed back into the
215
- query, bounded by `max_subtask_retries`. After retries are exhausted the
216
- last result is returned with its `error` set (content preserved). A
217
- check that itself raises is treated as "accept" so a buggy predicate
218
- can't wedge the run. Default `None` keeps the exceptions-only behavior.
219
- New example `examples/robust_link_scraper.py` wires it together with a
220
- scraping `system_addendum` (parse relative+absolute hrefs, fall back to
221
- `sitemap.xml` on JS-rendered sites).
222
-
223
- ## [3.1.4] — 2026-07-26
224
-
225
- ### Fixed
226
-
227
- - **A malformed-JSON tool argument no longer crashes the whole agent
228
- run.** When a model emitted a Python snippet or a Windows path as a
229
- tool-call argument — `re.findall(r'\d+')`, `C:\Users` — the `\d` / `\U`
230
- are illegal JSON escapes, and the OpenAI adapter's eager
231
- `json.loads(call.function.arguments)` raised `JSONDecodeError` and
232
- `raise`d it, unwinding the entire ReAct loop before the agent's own
233
- retry machinery could act. Under a Supervisor this surfaced as a bare
234
- `ERROR: Invalid \escape: line 1 column 598` and the sub-task was
235
- abandoned. Now:
236
- - `_parse_tool_arguments` repairs the common case (backslashes that
237
- don't begin a valid JSON escape are doubled), recovering `\d`,
238
- `\w`, `\s`, etc. with zero extra round-trips. A backslash before a
239
- valid-escape letter (`\b`, `\n`, …) remains ambiguous and is left
240
- as the escape — a documented limit.
241
- - When repair fails, `call_with_tools` returns a dedicated
242
- `invalid_tool_args` result and the loop feeds the error back as a
243
- retryable observation ("your arguments weren't valid JSON — escape
244
- backslashes and resend"), bounded by `max_iterations`, in all
245
- three modes across both `AgentRunner` and `AsyncAgentRunner`.
246
- `Claude` was already immune (its tool inputs arrive pre-parsed).
247
-
248
- - **A tool-call preamble is no longer returned as the final answer.**
249
- Models routinely end a turn with an announcement instead of an action
250
- — "I'll look up your recent scores to get a clear view of your
251
- communication skills. Just a second!" — and every "no tool call
252
- found" branch in both runners was coded as *this text is the answer,
253
- break*. The loop terminated on iteration 1 and the caller got a
254
- promise instead of a result. Three sites per runner were affected:
255
- the native-binding path (`type != "tool_use"`), the
256
- `use_function_calling` path (parser unresolved), and the JSON-text
257
- path (response didn't parse). `max_iterations` never helped, because
258
- the break happened before any iteration was spent.
259
-
260
- The runner now feeds the model one corrective nudge — "your last turn
261
- had no action, so nothing happened; do it, don't announce it" — and
262
- continues the loop. Verified against both `AgentRunner` and
263
- `AsyncAgentRunner` in all three modes.
264
-
265
- ### Added
266
-
267
- - **Proactive "act, don't announce" system-prompt clause.** The reactive
268
- `text_turn_nudges` fix corrects an agent *after* it narrates instead of
269
- acting; this clause heads it off. When (and only when) an agent has
270
- tools, its system prompt now tells it to call the tool rather than
271
- reply "I'll do X / just a second" and stop — and to report what it DID
272
- in past tense. Injected in all three modes across both runners; skipped
273
- for tool-less chat agents, where prose is the correct answer. Sits
274
- before any `system_addendum` so a caller's role instructions still win.
275
-
276
- - **`max_subtask_retries` on `Supervisor` / `AsyncSupervisor`** (default
277
- `1`). A sub-task that raised used to be recorded as an error and the
278
- Supervisor moved straight to synthesis — no second attempt. Now a
279
- failed sub-task is re-dispatched up to this many times, with the prior
280
- error appended to the query so the specialist knows what to fix
281
- ("your previous attempt failed with X — diagnose and try again").
282
- Bounded and informed: only raised exceptions trigger a retry (a
283
- sub-task that returns content is accepted as-is, since the Supervisor
284
- can't tell "terse but correct" from "wrong"), and the error text is
285
- fed back rather than blindly re-running. Set to `0` for the old
286
- quit-on-first-failure behavior. Applies in both sequential and
287
- concurrent async modes.
288
-
289
- - **`text_turn_nudges` on `AgentRunner` / `AsyncAgentRunner`** (default
290
- `1`). Caps the re-prompts described above at one extra LLM call per
291
- run; after the budget is spent the model's text stands as the answer.
292
- Set to `0` for the previous behavior. Automatically skipped when no
293
- tools are registered, since a runner with no tools is a plain chat
294
- call and prose genuinely is the answer there.
295
-
296
- ## [3.1.3] — 2026-07-22
297
-
298
- Docs-only patch. No code changes since 3.1.2. Users on 3.1.2 don't
299
- need to upgrade for functionality; upgrade to pick up the improved
300
- onboarding docs bundled in the sdist.
301
-
302
- ### Documentation
303
-
304
- - **Tools doc rewritten to answer "how do I actually use these?"**
305
- Added §0 `How each built-in tool is registered` as the entry
306
- section. Two registration paths — auto vs manual — laid out in a
307
- table on the first screen. Six runnable subsections covering every
308
- combination:
309
- - §0.1 DefaultTools via `Permissions(...)` (auto)
310
- - §0.2 WebTools via `tools=[web_search_tool(), web_fetch_tool()]`
311
- - §0.3 RAG via `TextSplitter` -> `VectorStore.add_documents` ->
312
- `vector_search_tool(store)`
313
- - §0.4 Handoffs via `handoff_tool` + `HandoffCoordinator`
314
- - §0.5 Custom `StructuredTool` from scratch
315
- - §0.6 Fully-loaded runner combining all of the above
316
- - §0.7 Rules on name collisions, invisible-denied-capabilities,
317
- async-tool behavior
318
- The existing inventory + wrapper / controls / cheat-sheet sections
319
- are unchanged; they now sit after the "how to use them" primer
320
- instead of before it.
321
-
322
- ## [3.1.2] — 2026-07-22
323
-
324
- Patch release. Two independent fixes.
325
-
326
- ### Fixed
327
-
328
- - **`llm_judge` correctly parses YES/NO across providers.** The judge
329
- parser was comparing the reply's first word to the literal string
330
- `"YES"`. GPT-4o answers `"YES,"` (comma-suffixed), which failed the
331
- equality check and marked every genuine PASS as FAIL. Claude replies
332
- `"YES"` without punctuation so the bug hid during local development.
333
- Fixed by matching `\b(YES|NO)\b` (word-boundary regex, case-
334
- insensitive) at the start of the reply. Handles every real shape:
335
- `YES`, `YES.`, `YES!`, `YES, exactly right`, `Yes.`, `yes -- reason`.
336
- Ambiguous replies (`Maybe`, empty string) still fail closed.
337
- - Regression test `test_llm_judge_parses_various_verdict_shapes`
338
- covers 8 YES shapes, 5 NO shapes, and 4 ambiguous replies.
339
-
340
- ### Added
341
-
342
- - **`agentx_dev.Tools` is a one-stop tools namespace.** Users no
343
- longer need to remember which module each tool lives in:
344
-
345
- ```python
346
- from agentx_dev.Tools import (
347
- StandardTool, StructuredTool,
348
- AsyncStandardTool, AsyncStructuredTool,
349
- web_search_tool, web_fetch_tool,
350
- vector_search_tool, handoff_tool,
351
- DefaultTools, Permissions,
352
- )
353
- ```
354
-
355
- Both this form and the pre-existing `from agentx_dev import X`
356
- form coexist. Implementation uses PEP 562 module `__getattr__`
357
- and `__dir__` so re-exports are lazy (no import cost for modules
358
- the caller doesn't touch) and show up in IDE autocomplete +
359
- `dir(agentx_dev.Tools)`.
360
-
361
- ## [3.1.1] — 2026-07-21
362
-
363
- Second batch of 3.1 features + a full docs + brand pass.
364
-
365
- ### Added
366
-
367
- **Streaming through orchestration**
368
- - `Supervisor.stream()` / `AsyncSupervisor.astream()` emit
369
- `plan_start` / `plan` / `dispatch` / `subtask_result` /
370
- `synthesize_start` / `final` / `completion` events.
371
- - `HandoffCoordinator.stream()` / `.astream()` emit `invoke` /
372
- `completion` / `handoff` / `final` / `result` events per hop.
373
- - Legacy `.run()` / `.arun()` refactored to consume the streams (no
374
- code duplication).
375
-
376
- **Prompt optimization — `Compiled`**
377
- - New `agentx_dev.Compiler` module.
378
- - `Compiled(runner_factory, trainset, ...)` iteratively refines a
379
- runner's `system_addendum` against the eval harness. Half of
380
- DSPy's power at a tenth of the surface.
381
-
382
- **Anthropic Batch API**
383
- - `Claude.batch(requests)` submits many prompts at Anthropic's 50%-off
384
- batch rate, polls to completion, returns results in submission order.
385
- - Per-request error dicts on failure; token usage funneled into
386
- `TokenUsage` so cost tracking stays a single source of truth.
387
-
388
- **Vector store adapters — `agentx_dev.VectorStores`**
389
- - `ChromaVectorStore`, `QdrantVectorStore`, `PgVectorStore` — same
390
- public shape as the in-memory `VectorStore` (`add` / `search` /
391
- `delete` / `clear` / `__len__` / `embeddings`).
392
- - `vector_search_tool()` and `SemanticMemory` accept any of them.
393
- - SDK imports lazy; friendly `ImportError` when the underlying SDK
394
- is missing.
395
-
396
- **Trace viewer (`viewer/`)**
397
- - Self-hosted single-page app that reads `FileHook` JSONL and renders
398
- a timeline with type/text filters, summary sidebar, JSON drill-down.
399
- - Works from `file://`, no server required.
400
-
401
- **Docs site (`host/`)**
402
- - Full editorial dark-first design system (JetBrains Mono headings,
403
- Inter body, `#B8FF3E` electric-lime accent).
404
- - Command palette (`Cmd+K`) with keyboard navigation and live search.
405
- - Hero code snippet with hand-tinted syntax highlighting.
406
- - Reading progress bar, breadcrumbs, header anchor links.
407
- - Sidebar sliding active marker, collapsible groups.
408
- - Code copy buttons, language labels.
409
- - Right-rail auto-TOC with `IntersectionObserver` scrollspy.
410
- - Dark/light theme toggle, persisted.
411
- - Cache-busted assets so edits land on refresh without hard-reload.
412
-
413
- **Brand identity (`brand/`)**
414
- - Full brand kit: 5 SVG assets (`mark`, `mono`, `wordmark`, `logo-full`,
415
- `app-icon`), `BRAND.md` strategy doc, rendered brand-kit HTML deck.
416
- - Copy audit dropped "small" (weak) and "LangChain" references from
417
- all marketing surfaces.
418
- - Favicon wired into docs + trace viewer.
419
-
420
- **Test suite (`tests/`)**
421
- - Restored + expanded pytest suite: 127 tests passing (3 skipped for
422
- absent optional SDKs).
423
- - Coverage: parser + all `AgentType` variants, `ToolRegistry`
424
- (dispatch / dup-guard / circuit-breaker / timeout), Permissions
425
- (capability gating + sandbox + traversal), budgets (cost / rate /
426
- retry / non-retryable HTTP), runner loop (streaming + output_schema
427
- + chat history), embeddings + `VectorStore` + `SemanticMemory`,
428
- handoffs (bounded hops + history sanitization), evals harness
429
- (all assertion helpers + JSON case loaders), vector-store adapter
430
- shape conformance.
431
-
432
- **Docs (`docs/`)**
433
- - Full docs tree (34 pages), including new pages for:
434
- vector store adapters, prompt optimization, batch API, trace viewer,
435
- and a **use-cases** landing (13 concrete scenarios with runnable code).
436
- - Rewrote **Tools** page to enumerate every built-in tool with args,
437
- return shape, capability flag, and use-case guidance.
438
- - Rewrote **Agents** page to cover all four orchestration
439
- architectures (Solo / Supervisor / Handoffs / Compiled) with
440
- decision trees, worked examples, and cheat sheet.
441
- - **Agentic RAG chatbot** as use case §13 — multi-query decomposition,
442
- parallel retrieval, self-critique, citations, user memory.
443
-
444
- **Examples**
445
- - `examples/agentic_rag_demo.py` — the runnable version of the
446
- agentic RAG use case. Auto-seeds a KB if none exists, `--demo` flag
447
- runs a 3-turn scripted session proving user-notes recall works.
448
-
449
- **Package**
450
- - `[chroma]`, `[qdrant]`, `[pgvector]`, `[dev]` extras added.
451
- - `[anthropic]` bumped to `>=0.36` (Batch API + prompt cache).
452
-
453
- ### Fixed
454
-
455
- - `AgentRunner._iter_run` in `bind_tools_natively=True` mode uses a
456
- minimal system prompt instead of the AgentType template so the
457
- ReAct `action/action_input` scaffold no longer fights the native
458
- tool interface. Previously produced JSON-blob answers under GPT.
459
- - `HandoffCoordinator._sanitize_history_for_next_agent` strips tool
460
- and function role messages between hops so tool_call_ids from a
461
- previous agent don't leak into the next model's call (OpenAI 400).
462
- - Docs site marker positioning uses double-`requestAnimationFrame` +
463
- `document.fonts.ready` so the sidebar accent bar lands on the
464
- correct row even on a cold font cache.
465
- - Primary hero CTA color uses `#doc .hero-cta a.primary` selector to
466
- outrank `#doc a` link styling (previously rendered lime-on-lime
467
- and was invisible).
468
-
469
- ### Notes
470
-
471
- - Package version bumped from `3.0.6` to `3.1.1`. The 3.1.0 release
472
- did not ship publicly — 3.1.1 is the first 3.1-tagged PyPI release
473
- and includes both batches of features.
474
-
475
- ## [3.1.0] — internal only (commits 52840e7)
476
-
477
- First batch of 3.1 features. Committed but not released to PyPI.
478
- Merged into 3.1.1 for the public release.
479
-
480
- ### Added
481
- - Anthropic prompt caching (`Claude(enable_prompt_cache=True)`).
482
- - Parallel per-turn tool dispatch in `AgentRunner`
483
- (`bind_tools_natively=True`, `parallel_tool_workers`).
484
- - Semantic memory (`SemanticMemory`, embeddings-backed retrieval).
485
- - RAG core (`Embeddings`, `HashEmbeddings`, `OpenAIEmbeddings`,
486
- `VectorStore`, `VectorHit`, `vector_search_tool()`).
487
- - Agent-to-agent handoffs (`HandoffRequest`, `handoff_tool`,
488
- `HandoffCoordinator`, `HandoffResult`).
489
- - Evals harness (`EvalCase`, `EvalRunner`, `EvalReport`, 7 assertion
490
- helpers, JSON case loader, `python -m agentx_dev.Evals run` CLI).
491
- - `TokenUsage.cache_hit_ratio` property.
492
-
493
- ## [3.0.6] — 2026-03 (baseline)
494
-
495
- Security hardening baseline (SSRF guard on `web_fetch`, HMAC-signed
496
- persistent state, scrubbed subprocess env, path sanitizer,
497
- `permissions.json` mode 0o600, ReDoS guard on `grep`,
498
- `invoke`/`ainvoke` accept bare strings and message lists).
1
+ # Changelog
2
+
3
+ All notable changes to `agentx-dev` are documented here. Format loosely
4
+ follows [Keep a Changelog](https://keepachangelog.com/); versioning is
5
+ [Semver](https://semver.org/).
6
+
7
+ ## [3.3.1] - 2026-09-09
8
+
9
+ Correctness fixes in the agent loop, found by driving it with a
10
+ scripted model rather than reading it. No API changes; every fix
11
+ replaces a silent wrong answer or a runaway cost with correct
12
+ behaviour. Nine regression tests ship with them
13
+ (`tests/test_agent_loop_flaws.py`).
14
+
15
+ ### Fixed
16
+
17
+ - **Native binding had no loop-level spiral guard.** The repeat breaker
18
+ sat after the native branch's `continue`, so `bind_tools_natively`
19
+ runs never reached it: a model stuck re-issuing one call burned the
20
+ full `max_iterations` (20 LLM turns where text mode stopped at 3) and
21
+ returned the "hit max_iterations" recap quoting the dup-guard's own
22
+ warning text instead of the data.
23
+
24
+ - **`respond` batched with real tool calls dropped them.** A model that
25
+ emitted "do X, and here is my answer" in one turn had X silently
26
+ discarded -- never dispatched, never in `completion.tool_calls`, no
27
+ error. The batch now runs and the answer defers one turn.
28
+
29
+ - **`completion.history` could not be replayed as `chat_history`.** The
30
+ copy filter kept only truthy `{role, content}`, which dropped every
31
+ tool-calling assistant turn (`content=""`), stripped `tool_call_id`
32
+ off `role="tool"` messages, and replayed a stored system prompt on top
33
+ of the fresh one -- so a follow-up call sent two system messages, zero
34
+ assistant turns, and an orphaned tool message providers reject. Fixed
35
+ in both runners.
36
+
37
+ - **A sync `AgentRunner` silently ignored async tools.** `known_tools`
38
+ unioned only the two sync registry tables while the registry accepted,
39
+ listed and prompt-advertised async ones, so calling one fell through
40
+ to implicit-final and returned `action_input` as the answer with an
41
+ empty `tool_calls` list. Now unions all four, matching the async
42
+ runner; the sync dispatcher already returns a clear `ToolError`, so
43
+ the silent wrong answer became an actionable one.
44
+
45
+ - **`AsyncAgentRunner` regressed the function-calling message shape.**
46
+ It appended its FC turn as raw JSON text with no `tool_calls` block
47
+ and never set `_last_function_call_id`, so observations went back as
48
+ `role="user"` -- text-mode shape while in FC mode, losing provider
49
+ correlation and the cached prefix.
50
+
51
+ - **A `BaseException` from a specialist orphaned its siblings.**
52
+ `CancelledError` and `KeyboardInterrupt` are not `Exception`, so
53
+ `_run_subtask`'s handler never saw them; `t.result()` re-raised and
54
+ left sibling tasks running unowned, with in-flight LLM calls still
55
+ billing. The scheduler now owns its tasks in a `try`/`finally`, which
56
+ also covers a consumer that stops iterating `astream` early.
57
+
58
+ - **The tool cache collided on tool name.** It is a process-wide
59
+ singleton keyed on `(tool_name, args)` with no record of which
60
+ implementation ran, so two runners whose tools merely share a name --
61
+ `search`, `fetch`, `query`, routine across Supervisor specialists --
62
+ served each other's results and the second function never ran. Keys
63
+ now fold in the callable's `module.qualname`, so same-name /
64
+ different-implementation misses while genuinely identical tools still
65
+ share, including across processes for the disk-backed `FileCache`.
66
+
67
+ ### Docs
68
+
69
+ - Streaming documentation described parameters the API rejects.
70
+ `AsyncAgentRunner.astream()` and `HandoffCoordinator.stream()` take no
71
+ `stream_tokens` (only the sync `AgentRunner.stream()` does), and every
72
+ `text_delta` example was built on a default model where
73
+ `use_function_calling` auto-detects to `True` and yields zero deltas.
74
+ `simple_stream` was documented with the wrong signature entirely.
75
+ Added a table naming exactly which stream methods accept what.
76
+
77
+ ### Tests
78
+
79
+ - Repaired 20 stale text-mode tests. `MockModel` defined
80
+ `call_with_tools` unconditionally, so 3.1.7's auto-detect routed every
81
+ text script down the function-calling path and the runner returned
82
+ `""`. The mock now advertises the capability only when scripted for
83
+ it. Suite: 206 passed, 3 skipped.
84
+
85
+ ## [3.3.0] - 2026-08-19
86
+
87
+ Dependency DAGs for the Supervisor. Plans declare which steps consume
88
+ which, and the executor derives ordering, parallelism, AND context
89
+ routing from those edges — unifying the old split where sequential
90
+ mode had threading but no parallelism and concurrent mode had
91
+ parallelism but no threading. Design: docs/design/3.3-depends-on-dag.md.
92
+
93
+ ### Added
94
+
95
+ - **`depends_on` plan steps.** Every plan step now carries an `id`;
96
+ a step that consumes an earlier step's output lists that id in
97
+ `depends_on`. Sync `Supervisor` executes in stable topological
98
+ order; `AsyncSupervisor` runs a completion-driven scheduler where a
99
+ step starts the MOMENT its dependencies finish (not on wave
100
+ barriers) and independent steps run concurrently. In DAG mode each
101
+ step is threaded ONLY its direct dependencies' results — explicit
102
+ routing instead of "everything prior", which also stops the
103
+ per-entry context budget shrinking as plans grow.
104
+
105
+ - **Plan sanitization that never fails a run.** Missing/duplicate ids
106
+ auto-assigned, unknown dependencies dropped (a planner typo degrades
107
+ to a root step, not a dead run), self-deps dropped, cycles broken
108
+ deterministically (back-edge in plan order), spawn steps cannot be
109
+ depended on. If repairs were needed, the plan is re-requested once
110
+ (`max_plan_retries`, default 1) with the repair warnings appended;
111
+ the sanitized original is kept when the retry is no better.
112
+
113
+ - **Failure cascade + `skipped` flag.** A step whose dependency failed
114
+ (after retries / success-check) is skipped, transitively, with
115
+ `SubtaskResult.skipped=True` and an error naming the failed dep.
116
+ Independent branches keep running; synthesis runs over what
117
+ succeeded. Skipped steps never dispatch — no tokens burned
118
+ downstream of garbage.
119
+
120
+ - **`skip_when` conditional execution.** A step may declare
121
+ `{"step": <direct dep id>, "field": <typed output field>, "is": <value>}`;
122
+ evaluated in Python (no LLM call) against the dependency's 3.2
123
+ structured output, dotted paths supported, strictly FAIL-OPEN (any
124
+ doubt → the step runs). Condition-skips do NOT cascade — dependents
125
+ treat them as empty successes ("retrieval unnecessary" is not
126
+ "answering impossible").
127
+
128
+ - **`Specialist` registry entries.** `agents={}` now also accepts
129
+ `Specialist(description, runner, depends_on=[...names...],
130
+ output_schema=..., when_to_use=...)`. The extras render into the
131
+ planning catalog (`typically after:` / `returns: Schema(fields)` /
132
+ `use when:`) so the planner can write real dependency graphs and
133
+ `skip_when` conditions against actual field names. `depends_on`
134
+ here is a planner HINT, never an execution constraint (the same
135
+ specialist can appear twice in one plan; step-ids disambiguate).
136
+ Classic `(description, runner)` tuples keep working — they are
137
+ wrapped internally, and `Specialist` tuple-unpacks for older code.
138
+
139
+ - **`AsyncSupervisor(max_parallel=N)`.** Caps concurrent sub-tasks for
140
+ rate-limited deployments. `sequential=True` is now sugar for
141
+ `max_parallel=1` with all-prior threading.
142
+
143
+ - **`SubtaskResult.step_id` / `.depends_on` / `.skipped`** and a
144
+ `step_id` field on `dispatch` / `subtask_result` stream events.
145
+
146
+ ### Backward compatibility
147
+
148
+ A plan where NO step declares `depends_on` runs with byte-identical
149
+ legacy semantics: sync + async-sequential thread all prior results in
150
+ plan order; async-concurrent runs everything at once with no
151
+ threading. Verified by regression tests against the 3.2 behaviour.
152
+
153
+ ## [3.2.0] - 2026-08-13
154
+
155
+ Typed multi-agent pipelines. Specialists can now declare a Pydantic
156
+ output schema once and pass validated instances to each other through
157
+ the Supervisor, instead of downstream agents re-parsing prose.
158
+
159
+ ### Added
160
+
161
+ - **`output_schema` on the `AgentRunner` / `AsyncAgentRunner`
162
+ constructor.** Declare the runner's output shape once
163
+ (`AgentRunner(..., output_schema=QueryIntent)`) instead of passing it
164
+ on every call or describing JSON in the prompt. A per-call
165
+ `output_schema=` still wins when both are set. `None` keeps the exact
166
+ pre-3.2 behaviour: no coercion, `completion.output` stays `None`.
167
+
168
+ - **Schema coercion via forced native function calling.** When a schema
169
+ is in play, the final answer is converted by forcing a provider-native
170
+ tool call against the schema (constrained decoding), not by regexing
171
+ JSON out of prose. The ReAct loop itself is untouched: tool selection
172
+ and intermediate reasoning run exactly as before, and the coercion
173
+ happens once, after the loop finishes. Models without a
174
+ `call_with_tools` implementation fall back to the previous text-JSON
175
+ parsing, so custom `BaseChatModel` subclasses keep working.
176
+ `completion.content` keeps the human-readable answer alongside
177
+ `completion.output` in every case.
178
+
179
+ - **`SubtaskResult.output`.** The Supervisor now preserves each
180
+ specialist's validated Pydantic instance next to its `content` text.
181
+ Consumers that only read `content` are unaffected.
182
+
183
+ - **Structured specialist-to-specialist handoff.** When an earlier step
184
+ produced typed output, `_build_augmented_query` serializes it into the
185
+ next specialist's context as a labelled JSON block
186
+ (`STRUCTURED OUTPUT (QueryIntent): {...}`) followed by the summary
187
+ text, so downstream steps parse fields rather than interpreting
188
+ sentences like `INTENT: ... SEARCH_QUERY: ...`.
189
+
190
+ - **`vector_search_tool` pipeline options.** New kwargs:
191
+ `max_text_chars` (default 500; pass `0` for full untruncated passages,
192
+ which a reranker judging evidence actually needs) and
193
+ `structured_output` (default False; when True the tool returns a JSON
194
+ array of `{id, text, vector_score, metadata}` instead of the
195
+ human-formatted list). Defaults preserve existing behaviour byte-for-
196
+ byte.
197
+
198
+ ### Fixed
199
+
200
+ - **Supervisor planning prompt contradicted the execution engine.** The
201
+ planner rule said sub-agents "do NOT see previous steps' output" and
202
+ discouraged dependency chains, but the dispatcher has threaded prior
203
+ findings into every step since `_build_augmented_query` shipped.
204
+ The rule now tells the planner that sequential steps receive earlier
205
+ results (structured when available) and that chains like
206
+ intent -> retrieval -> reranking are a good plan shape, while still
207
+ requiring same-specialist steps to merge and banning report-only steps.
208
+
209
+ ## [3.1.7] — 2026-07-27
210
+
211
+ ### Changed
212
+
213
+ - **`use_function_calling` default flipped to auto-detect** on
214
+ `AgentRunner` / `AsyncAgentRunner`. The parameter's default type is
215
+ now `Optional[bool] = None`; `None` resolves to `True` when the
216
+ model class overrides `BaseChatModel.call_with_tools` (both `GPT`
217
+ and `Claude` do) and to `False` when it doesn't (or when
218
+ `bind_tools_natively=True`). Callers passing `True`/`False`
219
+ explicitly are unaffected. Rationale: text-mode ReAct requires the
220
+ model to emit strict JSON with any long `action_input` string
221
+ properly escaped — a 1200-word markdown draft with unescaped
222
+ newlines or quotes reliably breaks `json.loads` and killed the run.
223
+ Function-calling mode routes the parser through the SDK's typed
224
+ channel so escaping is handled automatically. The historical
225
+ default (`False`) was the fragile option; the new default matches
226
+ what most users actually want.
227
+
228
+ ### Fixed
229
+
230
+ - **Malformed parser JSON no longer crashes the run.** When the
231
+ text-mode assistant response failed `json.loads` (typically because
232
+ a long `action_input` string had unescaped `"`, `\n`, or backticks),
233
+ the framework used to raise `JSONDecodeError` and unwind the whole
234
+ invocation. The runner now (1) tries a regex-based salvage that
235
+ extracts `{Thought, action, action_input}` from the raw text
236
+ covering the common "outer envelope valid, inner string broke
237
+ escaping" failure, and (2) if salvage fails, feeds a targeted fix
238
+ hint back to the model (`"your last response was not valid JSON;
239
+ emit …, escape newlines as \n"`) and continues the loop bounded
240
+ by `max_iterations`. Exhaustion returns a clear framework message
241
+ rather than an uncaught exception. Applied to both sync and async
242
+ runners via a shared `_salvage_react_json` helper.
243
+ The salvager's action-name regex is intentionally strict
244
+ (`[A-Za-z_][A-Za-z0-9_.\- ]{0,79}`) so it can't hallucinate an
245
+ "action" out of an unrelated `"key":"value"` pair inside malformed
246
+ JSON.
247
+
248
+ - **Verbose trace in `bind_tools_natively` mode now prints tool
249
+ name + args + response.** Previously native runs showed blank
250
+ `[tool.call.start]` / `[tool.call.complete]` pairs (the
251
+ observability layer fires them without the trace context), so you
252
+ couldn't tell which tool the model actually invoked or what came
253
+ back. The runner now prints `[tool] Invoking '<name>' with args:
254
+ <input>` and `[tool] Response: <preview>` (or `[tool] Error: ...`
255
+ when the dispatch raised) in the post-dispatch loop, matching the
256
+ format text-mode and function-calling mode use. Mirrored to the
257
+ async runner.
258
+
259
+ - **`web_fetch_tool(vector_store=...)` auto-ingests fetched pages into
260
+ a vector store** instead of dumping raw HTML into the model's
261
+ context. Fixes the TPM-limit trap: when a research agent fetches
262
+ four articles in parallel (via ``multi_tool_use.parallel`` or
263
+ native binding), the combined bodies can easily exceed 40k tokens
264
+ and blow past a 30k TPM ceiling on the very next model call.
265
+ New parameters on ``web_fetch_tool``:
266
+
267
+ | Kwarg | Default | Effect |
268
+ |---|---|---|
269
+ | ``vector_store`` | ``None`` | When set, each fetch is HTML-stripped, chunked with ``TextSplitter``, embedded via the store's embeddings, and added with ``{src: url, chunk_index, total_chunks}`` metadata. The tool response becomes a compact summary (URL, byte count, chunk count, 240-char preview) — NOT the raw body. The model then calls ``vector_search`` / ``Rag`` to pull only the passages it needs. |
270
+ | ``chunk_size`` | ``1500`` | Characters per chunk when ``vector_store`` is set. Ignored otherwise. |
271
+ | ``chunk_overlap`` | ``200`` | Overlap between adjacent chunks so a fact spanning a boundary is still retrievable. Ignored otherwise. |
272
+
273
+ Backwards-compatible: the positional ``cache_dir`` signature keeps
274
+ working; `web_fetch_tool()` with no ``vector_store`` returns raw
275
+ body as before. Ingest and cache_dir compose — enable both and get
276
+ disk-cached full bodies AND searchable chunks. HTML stripping is
277
+ minimal and dependency-free (regex-based: script/style blocks
278
+ dropped whole, then tags stripped, whitespace collapsed) so the
279
+ ingest path adds no new install dependency. On JSON/plain-text
280
+ responses the stripper is a near no-op.
281
+
282
+ The observation returned to the model shows topical coverage --
283
+ first, middle, and last chunk previews (up to 3 samples,
284
+ deduplicated for short pages) -- so the model can tell what
285
+ topics the page actually covers, not just the intro paragraph.
286
+ Without this the model would only see the page's opening and
287
+ wouldn't know to query for topics discussed later in the same
288
+ page. Explicit instruction in the observation ("query with
289
+ SPECIFIC keywords from the topics above; do NOT re-fetch; do
290
+ NOT ask for the full body") steers the model toward the RAG path
291
+ on follow-up turns.
292
+
293
+ - **`multi_tool_use.parallel` now reaches its dispatch path.**
294
+ When GPT wanted to batch several tool calls into one turn (fetch N
295
+ URLs concurrently, run M searches at once), it emitted OpenAI's
296
+ synthetic `multi_tool_use.parallel` meta-tool. The registry's
297
+ `_dispatch_multi_parallel` / `_adispatch_multi_parallel` handlers
298
+ already knew how to unpack it, but the runner loop's known-tools
299
+ guardrail rejected the name FIRST as unregistered — dumping the
300
+ raw `{"tool_uses": [...]}` payload into the user-facing "final
301
+ answer" and never invoking any of the nested calls. Added
302
+ `multi_tool_use.parallel` to the recognized action set in both
303
+ sync and async runners so the meta-tool flows through to dispatch
304
+ and the existing unpackers run. Nested calls with the `functions.`
305
+ prefix are normalized before dispatch (same as top-level FC
306
+ calls), so the model can emit either shape.
307
+
308
+ - **`Permissions.full_access` / `read_only` auto-wrap a bare string.**
309
+ Passing `full_access("./workspace")` used to iterate the string
310
+ into 11 single-character "subtrees" (Python's `list("./workspace")`)
311
+ — every path check silently rejected because no real path could
312
+ ever match a `"."` or `"/"` "allowed subtree". The classmethod
313
+ now detects a bare string and treats it as `[allowed_paths]`, so
314
+ `full_access("./workspace")` does the intuitive thing (equivalent
315
+ to `full_access(["./workspace"])` and auto-infers the workspace).
316
+ Same fix on `read_only`. List inputs are unchanged.
317
+
318
+ - **`Permissions.full_access` now accepts (and auto-infers)
319
+ `workspace`.** The classmethod set `allowed_paths` but not
320
+ `workspace`, so short paths like `write_file(path="report.md")`
321
+ resolved to CWD (outside the sandbox) and raised
322
+ `PermissionError: access denied` — a landmine that every caller of
323
+ `Permissions.full_access(["./workspace"])` hit sooner or later.
324
+ New signature: `full_access(allowed_paths, *, workspace=None)`.
325
+ When `workspace` isn't passed AND `allowed_paths` has exactly one
326
+ entry, that path is auto-set as the workspace (the "project-scoped
327
+ agent whose one allowed subtree IS its workspace" case, which is
328
+ 99% of use). Two or more paths stay ambiguous and require an
329
+ explicit `workspace=` if short-path resolution is wanted. Pass an
330
+ explicit `workspace=` string to override the auto-choice.
331
+ Backwards-compatible on the positional signature; adds a keyword
332
+ argument that existing callers didn't use.
333
+
334
+ ## [3.1.5] — 2026-07-26
335
+
336
+ ### Fixed
337
+
338
+ - **Text-mode tool results no longer use `role: "function"`.** In text
339
+ mode (the default — no `use_function_calling`) the runner fed each tool
340
+ observation back to the model as a `role: "function"` message. Newer
341
+ OpenAI models reject that role outright (`400 … 'messages[N].role' does
342
+ not support 'function' with this model`, e.g. gpt-5.x), and Anthropic
343
+ never accepted it — text-mode multi-tool runs on Claude were latently
344
+ broken too; older GPT models simply still tolerated the legacy role.
345
+ Tool observations now go back as a plain `role: "user"` turn framed as
346
+ `Observation: …`, which every provider and model generation accepts and
347
+ which matches the ReAct template's own few-shot convention.
348
+ Function-calling mode is unchanged (native `role: "tool"` +
349
+ `tool_call_id`). The async runner was additionally emitting `function`
350
+ unconditionally (even in FC mode); it now uses the same shared helper.
351
+
352
+ ### Added
353
+
354
+ - **`subtask_success_check` on `Supervisor` / `AsyncSupervisor`.** Opt-in
355
+ predicate `(SubtaskResult) -> bool | str` that decides whether a
356
+ *returned* (non-raised) sub-task result is actually acceptable — the
357
+ "ran fine but produced nothing useful" case a plain retry can't catch
358
+ (a scraper that saved 0 links, an extractor that found nothing). Return
359
+ `True` to accept, or `False`/a `str` reason to reject; a rejected
360
+ result is retried like a raised error, with the reason fed back into the
361
+ query, bounded by `max_subtask_retries`. After retries are exhausted the
362
+ last result is returned with its `error` set (content preserved). A
363
+ check that itself raises is treated as "accept" so a buggy predicate
364
+ can't wedge the run. Default `None` keeps the exceptions-only behavior.
365
+ New example `examples/robust_link_scraper.py` wires it together with a
366
+ scraping `system_addendum` (parse relative+absolute hrefs, fall back to
367
+ `sitemap.xml` on JS-rendered sites).
368
+
369
+ ## [3.1.4] — 2026-07-26
370
+
371
+ ### Fixed
372
+
373
+ - **A malformed-JSON tool argument no longer crashes the whole agent
374
+ run.** When a model emitted a Python snippet or a Windows path as a
375
+ tool-call argument — `re.findall(r'\d+')`, `C:\Users` — the `\d` / `\U`
376
+ are illegal JSON escapes, and the OpenAI adapter's eager
377
+ `json.loads(call.function.arguments)` raised `JSONDecodeError` and
378
+ `raise`d it, unwinding the entire ReAct loop before the agent's own
379
+ retry machinery could act. Under a Supervisor this surfaced as a bare
380
+ `ERROR: Invalid \escape: line 1 column 598` and the sub-task was
381
+ abandoned. Now:
382
+ - `_parse_tool_arguments` repairs the common case (backslashes that
383
+ don't begin a valid JSON escape are doubled), recovering `\d`,
384
+ `\w`, `\s`, etc. with zero extra round-trips. A backslash before a
385
+ valid-escape letter (`\b`, `\n`, …) remains ambiguous and is left
386
+ as the escape — a documented limit.
387
+ - When repair fails, `call_with_tools` returns a dedicated
388
+ `invalid_tool_args` result and the loop feeds the error back as a
389
+ retryable observation ("your arguments weren't valid JSON — escape
390
+ backslashes and resend"), bounded by `max_iterations`, in all
391
+ three modes across both `AgentRunner` and `AsyncAgentRunner`.
392
+ `Claude` was already immune (its tool inputs arrive pre-parsed).
393
+
394
+ - **A tool-call preamble is no longer returned as the final answer.**
395
+ Models routinely end a turn with an announcement instead of an action
396
+ — "I'll look up your recent scores to get a clear view of your
397
+ communication skills. Just a second!" — and every "no tool call
398
+ found" branch in both runners was coded as *this text is the answer,
399
+ break*. The loop terminated on iteration 1 and the caller got a
400
+ promise instead of a result. Three sites per runner were affected:
401
+ the native-binding path (`type != "tool_use"`), the
402
+ `use_function_calling` path (parser unresolved), and the JSON-text
403
+ path (response didn't parse). `max_iterations` never helped, because
404
+ the break happened before any iteration was spent.
405
+
406
+ The runner now feeds the model one corrective nudge — "your last turn
407
+ had no action, so nothing happened; do it, don't announce it" — and
408
+ continues the loop. Verified against both `AgentRunner` and
409
+ `AsyncAgentRunner` in all three modes.
410
+
411
+ ### Added
412
+
413
+ - **Proactive "act, don't announce" system-prompt clause.** The reactive
414
+ `text_turn_nudges` fix corrects an agent *after* it narrates instead of
415
+ acting; this clause heads it off. When (and only when) an agent has
416
+ tools, its system prompt now tells it to call the tool rather than
417
+ reply "I'll do X / just a second" and stop — and to report what it DID
418
+ in past tense. Injected in all three modes across both runners; skipped
419
+ for tool-less chat agents, where prose is the correct answer. Sits
420
+ before any `system_addendum` so a caller's role instructions still win.
421
+
422
+ - **`max_subtask_retries` on `Supervisor` / `AsyncSupervisor`** (default
423
+ `1`). A sub-task that raised used to be recorded as an error and the
424
+ Supervisor moved straight to synthesis — no second attempt. Now a
425
+ failed sub-task is re-dispatched up to this many times, with the prior
426
+ error appended to the query so the specialist knows what to fix
427
+ ("your previous attempt failed with X — diagnose and try again").
428
+ Bounded and informed: only raised exceptions trigger a retry (a
429
+ sub-task that returns content is accepted as-is, since the Supervisor
430
+ can't tell "terse but correct" from "wrong"), and the error text is
431
+ fed back rather than blindly re-running. Set to `0` for the old
432
+ quit-on-first-failure behavior. Applies in both sequential and
433
+ concurrent async modes.
434
+
435
+ - **`text_turn_nudges` on `AgentRunner` / `AsyncAgentRunner`** (default
436
+ `1`). Caps the re-prompts described above at one extra LLM call per
437
+ run; after the budget is spent the model's text stands as the answer.
438
+ Set to `0` for the previous behavior. Automatically skipped when no
439
+ tools are registered, since a runner with no tools is a plain chat
440
+ call and prose genuinely is the answer there.
441
+
442
+ ## [3.1.3] — 2026-07-22
443
+
444
+ Docs-only patch. No code changes since 3.1.2. Users on 3.1.2 don't
445
+ need to upgrade for functionality; upgrade to pick up the improved
446
+ onboarding docs bundled in the sdist.
447
+
448
+ ### Documentation
449
+
450
+ - **Tools doc rewritten to answer "how do I actually use these?"**
451
+ Added §0 `How each built-in tool is registered` as the entry
452
+ section. Two registration paths — auto vs manual — laid out in a
453
+ table on the first screen. Six runnable subsections covering every
454
+ combination:
455
+ - §0.1 DefaultTools via `Permissions(...)` (auto)
456
+ - §0.2 WebTools via `tools=[web_search_tool(), web_fetch_tool()]`
457
+ - §0.3 RAG via `TextSplitter` -> `VectorStore.add_documents` ->
458
+ `vector_search_tool(store)`
459
+ - §0.4 Handoffs via `handoff_tool` + `HandoffCoordinator`
460
+ - §0.5 Custom `StructuredTool` from scratch
461
+ - §0.6 Fully-loaded runner combining all of the above
462
+ - §0.7 Rules on name collisions, invisible-denied-capabilities,
463
+ async-tool behavior
464
+ The existing inventory + wrapper / controls / cheat-sheet sections
465
+ are unchanged; they now sit after the "how to use them" primer
466
+ instead of before it.
467
+
468
+ ## [3.1.2] — 2026-07-22
469
+
470
+ Patch release. Two independent fixes.
471
+
472
+ ### Fixed
473
+
474
+ - **`llm_judge` correctly parses YES/NO across providers.** The judge
475
+ parser was comparing the reply's first word to the literal string
476
+ `"YES"`. GPT-4o answers `"YES,"` (comma-suffixed), which failed the
477
+ equality check and marked every genuine PASS as FAIL. Claude replies
478
+ `"YES"` without punctuation so the bug hid during local development.
479
+ Fixed by matching `\b(YES|NO)\b` (word-boundary regex, case-
480
+ insensitive) at the start of the reply. Handles every real shape:
481
+ `YES`, `YES.`, `YES!`, `YES, exactly right`, `Yes.`, `yes -- reason`.
482
+ Ambiguous replies (`Maybe`, empty string) still fail closed.
483
+ - Regression test `test_llm_judge_parses_various_verdict_shapes`
484
+ covers 8 YES shapes, 5 NO shapes, and 4 ambiguous replies.
485
+
486
+ ### Added
487
+
488
+ - **`agentx_dev.Tools` is a one-stop tools namespace.** Users no
489
+ longer need to remember which module each tool lives in:
490
+
491
+ ```python
492
+ from agentx_dev.Tools import (
493
+ StandardTool, StructuredTool,
494
+ AsyncStandardTool, AsyncStructuredTool,
495
+ web_search_tool, web_fetch_tool,
496
+ vector_search_tool, handoff_tool,
497
+ DefaultTools, Permissions,
498
+ )
499
+ ```
500
+
501
+ Both this form and the pre-existing `from agentx_dev import X`
502
+ form coexist. Implementation uses PEP 562 module `__getattr__`
503
+ and `__dir__` so re-exports are lazy (no import cost for modules
504
+ the caller doesn't touch) and show up in IDE autocomplete +
505
+ `dir(agentx_dev.Tools)`.
506
+
507
+ ## [3.1.1] — 2026-07-21
508
+
509
+ Second batch of 3.1 features + a full docs + brand pass.
510
+
511
+ ### Added
512
+
513
+ **Streaming through orchestration**
514
+ - `Supervisor.stream()` / `AsyncSupervisor.astream()` emit
515
+ `plan_start` / `plan` / `dispatch` / `subtask_result` /
516
+ `synthesize_start` / `final` / `completion` events.
517
+ - `HandoffCoordinator.stream()` / `.astream()` emit `invoke` /
518
+ `completion` / `handoff` / `final` / `result` events per hop.
519
+ - Legacy `.run()` / `.arun()` refactored to consume the streams (no
520
+ code duplication).
521
+
522
+ **Prompt optimization — `Compiled`**
523
+ - New `agentx_dev.Compiler` module.
524
+ - `Compiled(runner_factory, trainset, ...)` iteratively refines a
525
+ runner's `system_addendum` against the eval harness. Half of
526
+ DSPy's power at a tenth of the surface.
527
+
528
+ **Anthropic Batch API**
529
+ - `Claude.batch(requests)` submits many prompts at Anthropic's 50%-off
530
+ batch rate, polls to completion, returns results in submission order.
531
+ - Per-request error dicts on failure; token usage funneled into
532
+ `TokenUsage` so cost tracking stays a single source of truth.
533
+
534
+ **Vector store adapters — `agentx_dev.VectorStores`**
535
+ - `ChromaVectorStore`, `QdrantVectorStore`, `PgVectorStore` — same
536
+ public shape as the in-memory `VectorStore` (`add` / `search` /
537
+ `delete` / `clear` / `__len__` / `embeddings`).
538
+ - `vector_search_tool()` and `SemanticMemory` accept any of them.
539
+ - SDK imports lazy; friendly `ImportError` when the underlying SDK
540
+ is missing.
541
+
542
+ **Trace viewer (`viewer/`)**
543
+ - Self-hosted single-page app that reads `FileHook` JSONL and renders
544
+ a timeline with type/text filters, summary sidebar, JSON drill-down.
545
+ - Works from `file://`, no server required.
546
+
547
+ **Docs site (`host/`)**
548
+ - Full editorial dark-first design system (JetBrains Mono headings,
549
+ Inter body, `#B8FF3E` electric-lime accent).
550
+ - Command palette (`Cmd+K`) with keyboard navigation and live search.
551
+ - Hero code snippet with hand-tinted syntax highlighting.
552
+ - Reading progress bar, breadcrumbs, header anchor links.
553
+ - Sidebar sliding active marker, collapsible groups.
554
+ - Code copy buttons, language labels.
555
+ - Right-rail auto-TOC with `IntersectionObserver` scrollspy.
556
+ - Dark/light theme toggle, persisted.
557
+ - Cache-busted assets so edits land on refresh without hard-reload.
558
+
559
+ **Brand identity (`brand/`)**
560
+ - Full brand kit: 5 SVG assets (`mark`, `mono`, `wordmark`, `logo-full`,
561
+ `app-icon`), `BRAND.md` strategy doc, rendered brand-kit HTML deck.
562
+ - Copy audit dropped "small" (weak) and "LangChain" references from
563
+ all marketing surfaces.
564
+ - Favicon wired into docs + trace viewer.
565
+
566
+ **Test suite (`tests/`)**
567
+ - Restored + expanded pytest suite: 127 tests passing (3 skipped for
568
+ absent optional SDKs).
569
+ - Coverage: parser + all `AgentType` variants, `ToolRegistry`
570
+ (dispatch / dup-guard / circuit-breaker / timeout), Permissions
571
+ (capability gating + sandbox + traversal), budgets (cost / rate /
572
+ retry / non-retryable HTTP), runner loop (streaming + output_schema
573
+ + chat history), embeddings + `VectorStore` + `SemanticMemory`,
574
+ handoffs (bounded hops + history sanitization), evals harness
575
+ (all assertion helpers + JSON case loaders), vector-store adapter
576
+ shape conformance.
577
+
578
+ **Docs (`docs/`)**
579
+ - Full docs tree (34 pages), including new pages for:
580
+ vector store adapters, prompt optimization, batch API, trace viewer,
581
+ and a **use-cases** landing (13 concrete scenarios with runnable code).
582
+ - Rewrote **Tools** page to enumerate every built-in tool with args,
583
+ return shape, capability flag, and use-case guidance.
584
+ - Rewrote **Agents** page to cover all four orchestration
585
+ architectures (Solo / Supervisor / Handoffs / Compiled) with
586
+ decision trees, worked examples, and cheat sheet.
587
+ - **Agentic RAG chatbot** as use case §13 — multi-query decomposition,
588
+ parallel retrieval, self-critique, citations, user memory.
589
+
590
+ **Examples**
591
+ - `examples/agentic_rag_demo.py` — the runnable version of the
592
+ agentic RAG use case. Auto-seeds a KB if none exists, `--demo` flag
593
+ runs a 3-turn scripted session proving user-notes recall works.
594
+
595
+ **Package**
596
+ - `[chroma]`, `[qdrant]`, `[pgvector]`, `[dev]` extras added.
597
+ - `[anthropic]` bumped to `>=0.36` (Batch API + prompt cache).
598
+
599
+ ### Fixed
600
+
601
+ - `AgentRunner._iter_run` in `bind_tools_natively=True` mode uses a
602
+ minimal system prompt instead of the AgentType template so the
603
+ ReAct `action/action_input` scaffold no longer fights the native
604
+ tool interface. Previously produced JSON-blob answers under GPT.
605
+ - `HandoffCoordinator._sanitize_history_for_next_agent` strips tool
606
+ and function role messages between hops so tool_call_ids from a
607
+ previous agent don't leak into the next model's call (OpenAI 400).
608
+ - Docs site marker positioning uses double-`requestAnimationFrame` +
609
+ `document.fonts.ready` so the sidebar accent bar lands on the
610
+ correct row even on a cold font cache.
611
+ - Primary hero CTA color uses `#doc .hero-cta a.primary` selector to
612
+ outrank `#doc a` link styling (previously rendered lime-on-lime
613
+ and was invisible).
614
+
615
+ ### Notes
616
+
617
+ - Package version bumped from `3.0.6` to `3.1.1`. The 3.1.0 release
618
+ did not ship publicly — 3.1.1 is the first 3.1-tagged PyPI release
619
+ and includes both batches of features.
620
+
621
+ ## [3.1.0] — internal only (commits 52840e7)
622
+
623
+ First batch of 3.1 features. Committed but not released to PyPI.
624
+ Merged into 3.1.1 for the public release.
625
+
626
+ ### Added
627
+ - Anthropic prompt caching (`Claude(enable_prompt_cache=True)`).
628
+ - Parallel per-turn tool dispatch in `AgentRunner`
629
+ (`bind_tools_natively=True`, `parallel_tool_workers`).
630
+ - Semantic memory (`SemanticMemory`, embeddings-backed retrieval).
631
+ - RAG core (`Embeddings`, `HashEmbeddings`, `OpenAIEmbeddings`,
632
+ `VectorStore`, `VectorHit`, `vector_search_tool()`).
633
+ - Agent-to-agent handoffs (`HandoffRequest`, `handoff_tool`,
634
+ `HandoffCoordinator`, `HandoffResult`).
635
+ - Evals harness (`EvalCase`, `EvalRunner`, `EvalReport`, 7 assertion
636
+ helpers, JSON case loader, `python -m agentx_dev.Evals run` CLI).
637
+ - `TokenUsage.cache_hit_ratio` property.
638
+
639
+ ## [3.0.6] — 2026-03 (baseline)
640
+
641
+ Security hardening baseline (SSRF guard on `web_fetch`, HMAC-signed
642
+ persistent state, scrubbed subprocess env, path sanitizer,
643
+ `permissions.json` mode 0o600, ReDoS guard on `grep`,
644
+ `invoke`/`ainvoke` accept bare strings and message lists).