Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# https://peps.python.org/pep-0440/

[tool.bumpversion]
current_version = "0.4.3"
current_version = "0.4.4"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
Expand Down
6 changes: 0 additions & 6 deletions CHANGELOG.md

This file was deleted.

73 changes: 73 additions & 0 deletions docs/changelog/0.4.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 0.4.1 → 0.4.2 — Front-end Tools & Custom AG-UI Events

## Summary

Two additive features for Agno modules using the AG-UI stream:

- **Front-end tools** — first-class integration of Agno's community tool helpers,
with the `tools=` factory now able to accept an optional `RunContext` so per-run
state can flow into tool construction without mutating the `Agent` instance.
- **Custom AG-UI events** — a new `CustomEvent` type and the matching handler in
`AgUiMixin`, so modules can emit arbitrary client-facing events alongside the
built-in lifecycle ones.

A handful of AG-UI streaming fixes ship in the same release (tool-call dedup,
HITL flow, reasoning update).

No breaking changes; everything is opt-in.

## What changes

### 1. `CustomEvent` and `AgUiMixin` handler

Modules that need to push protocol-level signals to the front (beyond the
generic `TEXT_MESSAGE_*`, `TOOL_CALL_*`, `RUN_*` events) can now emit a
`CustomEvent`:

```python
from digitalkin.models.events import CustomEvent

await context.callbacks.send_message(
CustomEvent(name="my_signal", value={"foo": "bar"})
)
```

`AgUiMixin` ships with the corresponding handler that translates the event into
an AG-UI `CUSTOM` frame on the client stream. Existing modules see no change —
the handler is only invoked when a `CustomEvent` is produced.

### 2. Front-end tools — community Agno integration

Agno's community function helpers (the ones that wrap user-declared tool
definitions into `Function` objects) are now integrated into the SDK's tools
factory. Two practical consequences:

- The factory accepts an optional `run_context` argument. If provided, it is
threaded through tool construction, so tools that need per-run state
(storage handles, cost trackers, the AG-UI input itself) can be built
inline instead of through `dependencies`-based indirection.
- The factory no longer assumes the `Agent` instance is the only source of
truth for tools; combined with `cache_callables=False`, this makes it
straightforward to wire dynamic frontend-tool catalogues.

### 3. AG-UI streaming fixes

- **Tool-call dedup**: in stream sessions where the LLM re-emits the same
`tool_call_id` (Agno retry / re-plan paths), the adapter no longer
forwards duplicate `TOOL_CALL_START` / `TOOL_CALL_END` events to the
front. Only the first occurrence is emitted; later duplicates are
silently dropped.
- **HITL tool flow**: the resume path correctly threads `tool_call_id`s
through `acontinue_run`, fixing a case where pending tool results were
injected on the wrong `ToolExecution` instance after a storage
round-trip.
- **Reasoning update**: the reasoning sequence is closed deterministically
by the next non-reasoning event, fixing a class of orphaned
`REASONING_MESSAGE_CONTENT` events that the front could not render.

## Migration

None required. To opt into custom events, import `CustomEvent` from
`digitalkin.models.events` and emit via `context.callbacks.send_message`. To
opt into the run-context-aware factory, pass `run_context=...` to
`make_tools_factory(...)` at agent construction.
43 changes: 43 additions & 0 deletions docs/changelog/0.4.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 0.4.2 → 0.4.3 — Live Tool-module Schema Refetching

## Summary

`BaseModule` now clears `resolved_tools` on startup so that any tool module
whose schema changed between mission runs is re-discovered on the next
run, without needing a setup-version bump. Until this release, a redeployed
or upgraded tool module's new schema was only picked up once the consuming
setup's version was bumped — a manual step that was easy to forget and
caused stale tool catalogues to linger in production.

No public API change; consumers see the new behaviour automatically on the
next module boot.

## What changes

### `BaseModule` — clear `resolved_tools` on startup

`SetupModel.resolved_tools` is a persisted cache keyed by `setup_id`,
populated by `ToolReference.resolve` and read at every mission run. Its
invalidation contract until now was tied to the setup-version bump: a new
setup version dropped the persisted entry, any older version reused it
verbatim.

`BaseModule` now clears `resolved_tools` during its own startup, so:

- a freshly booted module re-resolves every `ToolReference` against the
live registry, picking up the current `ModuleInfo` (including any
schema changes from a redeployed tool module);
- the next mission run then re-populates `resolved_tools` with the fresh
catalogue, which subsequent runs in the same module lifecycle continue
to reuse (the per-run cache layer is unchanged).

The end-user effect: redeploying a tool module no longer requires a
downstream setup-version bump for its consumers to see the new schema.
Restarting the consuming module is enough.

## Migration

None. The clear runs at startup automatically. If you previously relied
on bumping setup versions just to flush `resolved_tools`, you can drop
that step from your release process — restarting the module achieves the
same invalidation.
133 changes: 133 additions & 0 deletions docs/changelog/0.4.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 0.4.3 → 0.4.4 — Tool-trigger Filtering Fix (DEV-631)

## Summary

This release fixes a silent cross-agent capability leak in
`ToolReference._resolve_single`. Per-agent trigger filtering no longer
leaks across agents sharing a `setup_id`: the SDK tool cache stops
trimming `ToolModuleInfo.tools` by per-selection triggers; the full
catalogue is always stored, and per-agent filtering happens at the
consumer site.

The cache contract changes shape (full catalogue per `setup_id` instead of
trimmed lists) but the public API is unchanged. Persisted `resolved_tools`
written by `<= 0.4.3` may still hold incorrectly trimmed lists — they are
automatically flushed by the startup clear introduced in 0.4.3, but the
first mission run after upgrading may want a setup-version bump to be
absolutely safe. See the **Migration** section below.

## Bug — capability leak across agents (DEV-631)

### Symptom

In archetype-isaac, a single setup can host a `main_agent`, a `team` with
`N` members and a `workflow` with `M` steps. Each agent independently
selects which trigger protocols of a shared SDK tool (one `setup_id`) it
can call, via a `tools` field carrying a list of
`ToolSelection({setup_id, triggers: {name: bool}})`.

When several agents reference the same `setup_id` with different trigger
booleans, the **first** agent walked by the SDK silently dictated which
tools were loaded for **every** other agent. Example with three agents
sharing one `setup_id` (a Knowledge Graph tool with 5 triggers):

| Agent | Configured triggers | Effective tools (before fix) |
|----------------------|-------------------------------------|------------------------------|
| `main_agent` | all 5 true | all 5 |
| member "search only" | `{search: true, rest: false}` | all 5 |
| member "edit only" | `{edit: true, rest: false}` | all 5 |

…and worse, if `main_agent` had `edit: false`, the "edit only" member would
**lose access to `edit` entirely** — its own trigger config was ignored.

### Root cause

`ToolReference._resolve_single` previously trimmed the resolved
`ToolModuleInfo.tools` in place, based on a **single** `ToolSelection`'s
triggers:

```python
if enabled_triggers := {name for name, enabled in entry.triggers.items() if enabled}:
tool_info.tools = [t for t in tool_info.tools if t.name in enabled_triggers]
```

`SetupModel._collect_from_tool_ref` keys the resolution cache by `setup_id`
only and short-circuits via `has_uncached`. Subsequent `ToolReference`
instances pointing at the same `setup_id` therefore skipped resolution and
reused the first writer's trimmed object. The trimmed `ToolModuleInfo` then
populated `context.tool_cache.entries`, which downstream toolkit code
(e.g. archetype-isaac's `ModuleToolkit`) reads from. Per-agent
`allowed_tools` filters could only shrink that already-trimmed list,
never re-expand it — so any trigger the first resolver dropped became
permanently invisible to later agents.

`resolved_tools` is also persisted across mission runs (invalidated only on
setup-version bumps prior to 0.4.3, or on module startup since 0.4.3), so a
stale trimmed cache survived runs until the user edited the setup or
restarted the module.

### Fix

`ToolReference._resolve_single` no longer touches `tool_info.tools`. It
returns the full module catalog, unchanged. The full canonical
`ToolModuleInfo` is stored under `tool_cache.entries[setup_id]` and
re-used by every agent that references the setup.

Per-agent filtering is the consumer's responsibility — for example
archetype-isaac builds one `ModuleToolkit(allowed_tools=...)` per agent
with `allowed_tools` derived from that agent's own `ToolSelection.triggers`.
Filtering after the cache, on each toolkit instance, means three agents
on the same `setup_id` with `{search: true}`, `{edit: true}` and
`{everything: true}` each end up with exactly the tools they declared,
even though the underlying cache entry holds the full catalog.

### Architecture (after fix)

```
ToolReference.resolve
└─ _resolve_single(entry) # no triggers filter
└─ registry.discover_by_id # returns full ModuleInfo
└─ module_info_to_tool_module_info → ToolModuleInfo (full tools)
└─ tool_cache.entries[setup_id] = info # shared, untrimmed

Consumer (e.g. archetype-isaac)
└─ ToolkitMixin.create_toolkit_for_selection(setup_id, triggers)
├─ allowed_tools = {name for name, enabled in triggers.items() if enabled}
└─ ModuleToolkit(tool_module_info, allowed_tools=allowed_tools)
└─ filters Function objects per agent, never mutates the cache
```

## Migration

- **No code changes required** in consumers. The `ToolReference`,
`ToolSelection`, `ToolModuleInfo` and `ToolCache` types are unchanged
in shape.
- **One-time data hygiene**: persisted `resolved_tools` entries written
by `<= 0.4.3` may still hold incorrectly trimmed `tools` lists. The
startup clear introduced in 0.4.3 already flushes them on the next
module boot, so a vanilla restart after upgrading is enough. If you
want belt-and-braces certainty (e.g. for shared production setups
whose `resolved_tools` was written before the 0.4.3 startup clear
landed), bump the affected setup's version once to force a clean
re-resolution.
- Archetypes relying on the previous (buggy) behaviour of having the
first agent's triggers silently restrict all peers must instead set
the desired triggers explicitly on each `ToolSelection` — every agent
is now independently honoured.

## Verification

The SDK ships with new regression coverage in
`tests/modules/test_tool_reference.py` (class
`TestSharedSetupIdAcrossAgents`):

- two `ToolReference`s sharing a `setup_id` with disjoint triggers
(`{search: true, edit: false}` and `{search: false, edit: true}`)
both observe the **full** tool catalogue in `tool_cache.entries`;
- a second resolution call does not progressively trim the cache.

archetype-isaac mirrors this with three agents (main, search-only,
edit-only) sharing a single `setup_id`, asserting that each toolkit
holds exactly its agent's enabled triggers while the shared cache
entry remains untouched
(`tests/toolkits/test_toolkit_mixin.py::TestSharedSetupAcrossAgents`).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"grpcio-status==1.78.0",
"pydantic==2.12.5",
]
version = "0.4.3"
version = "0.4.4"

[project.optional-dependencies]
profiling = [
Expand Down
2 changes: 1 addition & 1 deletion src/digitalkin/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
try:
__version__ = version("digitalkin")
except PackageNotFoundError:
__version__ = "0.4.3"
__version__ = "0.4.4"
11 changes: 6 additions & 5 deletions src/digitalkin/models/module/tool_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ async def _resolve_single(
registry: RegistryStrategy,
communication: CommunicationStrategy,
) -> ToolModuleInfo | None:
"""Resolve a single tool selection.
"""Resolve a single tool selection to its complete ``ToolModuleInfo``.

Per-selection trigger filtering is intentionally NOT applied here — the
cache is keyed by ``setup_id`` and shared across agents with disjoint
trigger sets; consumers (e.g. ``ModuleToolkit`` ``allowed_tools``) filter.

Args:
entry: Tool selection to resolve.
Expand All @@ -85,10 +89,7 @@ async def _resolve_single(
info = await registry.discover_by_id(setup.module_id)
if not info:
return None
tool_info = await module_info_to_tool_module_info(info, entry.setup_id, setup.name, communication)
if enabled_triggers := {name for name, enabled in entry.triggers.items() if enabled}:
tool_info.tools = [t for t in tool_info.tools if t.name in enabled_triggers]
return tool_info
return await module_info_to_tool_module_info(info, entry.setup_id, setup.name, communication)


class _ToolReferenceInputSchema:
Expand Down
Loading
Loading