You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Audit pass of every ``docs/documentation/*.md`` against the current
source tree, grounded in the Explore-agent report. Fixes:
configuration.md
* ``LLMAgent`` settings table — added ``skills``, ``llm``;
corrected default ``max_tool_hops = 3`` (was
``max_tool_calls = 8`` — fictitious name + wrong default);
removed invented ``parallel_tool_dispatch`` and
``stream_events`` kwargs that don't exist on the class.
* ``Compactor`` settings table — corrected to
``trigger_tokens=4000`` / ``keep_last_turns=4`` (were
``max_context_tokens=3500`` / ``keep_last=6`` / fictitious
``target_tokens=1500``).
* ``create_stt`` and ``create_tts`` examples — removed
non-existent ``backend`` kwarg from ``create_stt`` and
non-existent ``speed`` kwarg from ``create_tts``; added a
note that ``model_size="sherpa"`` routes to the Vietnamese
backend.
* Custom STT / TTS examples — replaced invented
``display_name = "..."`` and ``agent.bind_stt(...)`` with the
real ``_backend_name`` attribute pattern + a note that STT/TTS
instances live in the pipeline, not on the agent.
* Custom LLM example — replaced the non-existent ``chat_stream``
signature with the actual ``complete(messages, *, tools,
tool_choice, stream, stop_event, grammar)`` contract matching
what ``LLMAgent._drive`` calls. Added a note about the
back-compat ``TypeError`` fallback the loop uses.
* VectorMemoryStore example — replaced ``LLM(...,
embedding=True)`` (the EdgeVox ``LLM`` facade doesn't accept
``embedding=`` today) with the canonical
``llama_cpp.Llama(embedding=True)`` + ``llama_embed``
pattern. Same fix in ``memory.md`` and the ``memory_vec``
module docstring.
* Custom workflow example — corrected to the Agent Protocol
that actually exists: ``name``, ``run(task, ctx)``,
``run_stream(task, ctx)``. ``description`` was never a
Protocol requirement.
desktop.md
* On-disk state list — updated to reflect the
SQLiteMemoryStore migration (``memory.db`` + WAL sidecars)
with a note on the transparent ``memory.json`` →
``memory.db`` migration and the new ``game.json`` board
persistence.
memory.md
* Reordered the MemoryStore table so ``SQLiteMemoryStore`` is
marked as the recommended default (was listed second under
``JSONMemoryStore`` despite being the default for RookApp
and what configuration.md recommends).
* Added a ``SessionStore`` subsection covering
``JSONSessionStore`` vs. ``SQLiteSessionStore`` — both
shipped in ``__init__.py`` but previously undocumented.
agents.md
* Added a ``Parallel`` workflow section with a real
morning-briefing example + a pointer to the Pool pattern in
multiagent.md. ``Parallel`` was in the table but had no
usage example anywhere.
multiagent.md
* Added a "Pool + Parallel" subsection showing
``pool.make_ctx()`` threading a shared bus + blackboard
through a ``Parallel`` workflow. Users hit this pattern
whenever they want a bounded fleet of specialists running
concurrently.
memory_vec.py (code)
* Relaxed ``llama_embed``'s type hint from ``LLM`` → ``Any``
since both the EdgeVox facade and a raw ``llama_cpp.Llama``
are accepted by the unwrap logic; updated the docstring to
say so.
Full suite green: 1008 passed, 4 skipped, 209 deselected.
Ruff + ruff-format clean on all touched files.
Copy file name to clipboardExpand all lines: docs/documentation/agents.md
+20Lines changed: 20 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -404,6 +404,26 @@ Workflows compose agents. Every workflow implements the `Agent` protocol so nest
404
404
|`Supervisor.build()`| Same wire-shape as `Router` but forces `required_first_hop` — every turn must dispatch to a worker | "Every message goes to a worker, no chit-chat fallback" |
405
405
|`Orchestrator`| Lead LLM emits a JSON plan; `spawn_subagent` runs each subtask with scoped tools; lead synthesises | Anthropic-style plan → fan-out → synthesise |
406
406
407
+
### Parallel — fan-out to N specialists, one reduced reply
reduce=lambdaresults: "\n\n".join(r.reply for r in results if r.reply),
420
+
)
421
+
422
+
result = morning_briefing.run("what should I know this morning?")
423
+
```
424
+
425
+
Each sub-agent runs on its own worker with its own `Session`, so their tool histories don't cross-contaminate. The shared `LLM` serialises inference via its internal lock — the speedup comes from overlapping non-LLM work (tool dispatch, skill worker threads, I/O waits). Pair with the [Blackboard](/documentation/multiagent#blackboard) pattern when you want an agent pool to self-select rather than be explicitly enumerated.
426
+
407
427
### Router + handoff — the voice-optimized multi-agent pattern
408
428
409
429
Handoff-as-return-value (OpenAI Agents SDK style) saves one LLM hop per delegation vs. smolagents' "sub-agent-as-tool":
hits = store.search_facts("what's safe to cook?", k=5)
137
+
for fact, distance in hits:
138
+
print(f"{distance:.3f}{fact.key}: {fact.value}")
129
139
```
130
140
141
+
``llama_embed`` accepts either the framework's ``LLM`` (if it was built
142
+
with an embedding-capable backend) or a raw ``llama_cpp.Llama``
143
+
instance. For most users the latter is simpler — spinning up a second,
144
+
dedicated embedding model keeps it from fighting the main LLM for
145
+
sampling time.
146
+
131
147
**Disable memory entirely:** don't register `MemoryInjectionHook` on the agent. The agent runs fine without a memory store; each turn just starts fresh.
132
148
133
149
**Expose memory to the LLM itself** (memory-as-tools):
@@ -339,20 +355,23 @@ Every category above is a **Protocol** — a typed shape the framework calls. Yo
339
355
340
356
```python
341
357
from edgevox.stt import BaseSTT
358
+
import numpy as np
342
359
343
360
classMySTT(BaseSTT):
344
-
display_name="MyCustomSTT"
361
+
_backend_name="mystt"# feeds the default ``display_name`` property
agent.bind_stt(MySTT()) # or inject via your own pipeline
354
366
```
355
367
368
+
Drop into a pipeline by passing your instance wherever the default
369
+
`create_stt(...)` result would go — ``PipelineConfig(stt=MySTT(), ...)``
370
+
for the streaming pipeline, or the `stt=` kwarg of whatever higher-level
371
+
factory you're using. STT isn't attached to the `LLMAgent` directly — it
372
+
lives one layer up, producing the text that the agent's `run(...)`
373
+
consumes.
374
+
356
375
### Custom TTS backend
357
376
358
377
```python
@@ -361,7 +380,7 @@ import numpy as np
361
380
362
381
classMyTTS(BaseTTS):
363
382
sample_rate =24_000
364
-
display_name="MyCustomTTS"
383
+
_backend_name="mytts"
365
384
366
385
defsynthesize(self, text: str) -> np.ndarray:
367
386
returnself._model.run(text)
@@ -372,6 +391,8 @@ class MyTTS(BaseTTS):
372
391
yieldself._model.run(sentence)
373
392
```
374
393
394
+
Same injection story as STT: the pipeline owns the TTS instance, not the agent.
395
+
375
396
### Custom memory store
376
397
377
398
Implement `MemoryStore` (see [`memory.md`](/documentation/memory) for the full method list):
@@ -442,37 +463,64 @@ Priority guide: Safety=100, Business=50, Observability=0. The built-ins follow t
442
463
443
464
### Custom workflow
444
465
445
-
Subclass `Agent`— the same Protocol `LLMAgent`implements:
466
+
Implement the `Agent`Protocol — `name: str`, `run(task, ctx) -> AgentResult`, `run_stream(task, ctx) -> Iterator[str]`. `LLMAgent`and every shipped workflow already do this, so your class composes with them.
446
467
447
468
```python
448
-
from edgevox.agents import Agent, AgentContext, AgentResult
469
+
from collections.abc import Iterator
470
+
471
+
from edgevox.agents import AgentContext, AgentResult
472
+
from edgevox.agents.base import Agent
449
473
450
474
classRoundRobin:
451
-
name ="round-robin"
452
-
description ="Alternate between agents on successive calls"
475
+
"""Alternate between sub-agents on successive calls."""
Anywhere the framework takes an `Agent` (workflow child, handoff target, background worker) you can pass this.
465
494
466
495
### Custom LLM backend
467
496
468
-
Match the surface `LLMAgent` calls: `chat_stream(messages, *, stop_event=None, **kwargs)` yielding tokens, plus `count_tokens(text) -> int`. Wrap Anthropic / OpenAI / Ollama / vLLM / your-own-thing, then:
497
+
Match the surface `LLMAgent` calls — a single `complete(...)` method that returns an OpenAI-shaped response dict:
469
498
470
499
```python
500
+
classMyLLM:
501
+
defcomplete(
502
+
self,
503
+
messages: list[dict],
504
+
*,
505
+
tools: list[dict] |None=None,
506
+
tool_choice: str|dict="auto",
507
+
stream: bool=False,
508
+
stop_event: threading.Event |None=None,
509
+
grammar: object|None=None,
510
+
) -> dict:
511
+
# Must return {"choices": [{"message": {"content": str,
512
+
# "tool_calls": list | None}}]}
513
+
...
514
+
515
+
defcount_tokens(self, text: str) -> int:
516
+
# Only used by TokenBudgetHook / Compactor when passed ``ctx.llm``.
517
+
returnlen(text) //4# stub
518
+
471
519
agent = LLMAgent(...)
472
520
agent.bind_llm(MyLLM())
473
521
```
474
522
475
-
The agent loop only cares about the two-method contract — no assumptions about what's under the hood.
523
+
`stop_event` is how barge-in halts generation mid-decode — your backend should poll it in the sampling loop. `grammar` is optional (llama-cpp GBNF or equivalent); backends that can't grammar-constrain can ignore it. The agent loop falls back gracefully for older shims via a `TypeError` catch — so it's safe to implement only the subset you support.
476
524
477
525
---
478
526
@@ -485,15 +533,17 @@ Every knob, by component. Defaults are what you get from a bare `LLMAgent(...)`
485
533
| arg | default | meaning |
486
534
|---|---|---|
487
535
|`name`| required | human-facing identifier |
488
-
|`description`| required | advertised in agent handoffs|
536
+
|`description`| required | advertised to handoff targets + workflows|
489
537
|`instructions`| required | system prompt |
490
-
|`tools`|`None`| list of `@tool` callables or `Tool` objects |
491
-
|`hooks`|`[]`| list of hook objects |
492
-
|`max_tool_calls`|`8`| hops per turn before abort |
538
+
|`tools`|`None`| list of `@tool` callables, `Tool` objects, or a `ToolRegistry`|
539
+
|`skills`|`None`| list of `Skill` objects (cancellable long-running tasks) |
540
+
|`llm`|`None`| pre-bound `LLM` instance; otherwise set via `agent.bind_llm(...)`|
541
+
|`handoffs`|`None`| agents this one can hand off to |
542
+
|`hooks`|`None`| list of hook objects |
543
+
|`max_tool_hops`|`3`| tool-call hops per turn before abort |
|`parallel_tool_dispatch`|`False`| fan out independent tool calls via thread pool |
495
-
|`handoffs`|`[]`| agents this one can hand off to |
496
-
|`stream_events`|`True`| emit per-token / per-event signals via `ctx.on_event`|
545
+
546
+
Parallel tool-call dispatch happens automatically inside `_drive` when the LLM emits multiple `tool_calls` in a single response — no flag needed. Agent events are always published via `ctx.on_event` / the bus; subscribers attach via `ctx.bus.subscribe(...)`.
497
547
498
548
### `MemoryStore` implementations
499
549
@@ -532,22 +582,23 @@ All three honour `max_facts` / `max_episodes` on `render_for_prompt`.
-`memory.db` — `SQLiteMemoryStore` in WAL mode for long-term facts, crash-safe atomic writes. Older installs that wrote `memory.json` are migrated transparently on first launch; the legacy file is renamed to `memory.json.migrated` and left in place as a backup.
132
132
-`notes.md` — `NotesFile` scratchpad the `NotesInjectorHook` reads
133
-
-`sessions/…` — `JSONSessionStore` chat history, restored on next launch
134
-
-`QSettings` — platform-native registry/plist/INI for preferences
133
+
-`sessions.json` — `JSONSessionStore` chat history, restored on next launch
134
+
-`game.json` — board + move history (FEN + SAN), so a crashed match resumes exactly where it was
|`VectorMemoryStore`|`sqlite-vec` extension + injectable `embed_fn`| semantic retrieval — `store.search_facts("what's safe to cook?", k=3)`; opt in via `pip install 'edgevox[memory-vec]'`|
41
41
42
42
All three share the same bi-temporal semantics and render-for-prompt layout, so swapping stores doesn't change what the LLM sees. `VectorMemoryStore` is in the `[memory-vec]` extra; the embedding model is user-supplied via `embed_fn=...` (e.g. `llama_embed(llm)` to reuse a `llama-cpp` instance loaded with `embedding=True`).
43
43
44
44
```python
45
-
fromedgevox.llm.llamacppimportLLM
45
+
fromllama_cppimportLlama
46
46
from edgevox.agents import VectorMemoryStore, llama_embed
for fact, distance in store.search_facts("what's safe to cook?", k=3):
@@ -55,6 +60,26 @@ for fact, distance in store.search_facts("what's safe to cook?", k=3):
55
60
56
61
Write your own backend (Redis, Mongo, remote HTTP, …) by implementing the `MemoryStore` Protocol — the four built-in hooks that consume a store (`MemoryInjectionHook`, `NotesInjectorHook`, `PersistSessionHook`, `ContextCompactionHook`) read through the Protocol, never the concrete class.
57
62
63
+
### `SessionStore` — per-conversation history
64
+
65
+
Distinct from the per-user `MemoryStore`: a `SessionStore` persists an entire `Session` (messages, tool-call history, state dict) keyed by session-id so a user can resume a conversation after a restart. Two implementations ship:
66
+
67
+
| Class | Backing | Use case |
68
+
|---|---|---|
69
+
|`JSONSessionStore`| one JSON file per session | default, human-readable, fine through ~500 turns / 100 sessions |
70
+
|`SQLiteSessionStore`| stdlib `sqlite3` with a single `sessions` table | multi-user services, thousands of sessions, indexed lookup by `updated_at`|
71
+
72
+
Both implement the same three-method `SessionStore` Protocol (`load(id) / save(session) / delete(id)`), so `PersistSessionHook` reads through the Protocol:
73
+
74
+
```python
75
+
from edgevox.agents import PersistSessionHook, SQLiteSessionStore
result = morning.run("morning briefing?", ctx=pool.make_ctx())
102
+
```
103
+
104
+
`pool.make_ctx()` ensures every sub-agent sees the same bus + blackboard, so a background agent listening on `*` will observe the children's events. Combine with `bb.post_request(...)` when you want dynamic membership — the pool's agents pick up requests by capability rather than being enumerated up front.
105
+
85
106
## Debouncing triggers
86
107
87
108
`debounce_trigger(trigger, interval_s=…)` wraps any trigger so it fires at most once per interval — useful for sub-second sensor streams:
0 commit comments