mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
a629627afa07e8536cc7b9c76e3d6d810461f2f7
94
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
002dc8d76d |
fix(subagent): dedup tool specs before sending to provider (#2485)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9d0cce77ce |
feat(embeddings): rate-limit cloud embedding requests to the backend's hard 60/min cap (#2461)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d7b27b94fc |
fix(memory): run memory_tree on TRUNCATE journal instead of WAL (#2455)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6137b67811 |
fix(memory_tree,sync_status,scripts): IMMEDIATE-tx ingest, reembed skip-persistence, sidecar-based sync-status accounting, Windows dev-script PATH (#2349)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: sanil-23 <sanil@alphahuman.xyz> |
||
|
|
48c4da4e2a |
fix(cron): classify agent job errors into actionable user messages (#2279) (#2340)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3e2ba6648d |
fix(notifications): render <openhuman-link> tags in notification bodies (#2279) (#2339)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
ec74c7346b |
fix(channels): clear stale OAuth Connecting badges across auth modes (#2128)
## Summary - Centralises OAuth deep-link → channel-badge transitions behind a new `useOAuthConnectionListener` hook so every channel panel handles both `oauth:success` and `oauth:error` consistently. - Adds a `clearOtherPendingForChannel` reducer so starting a connect flow on one auth mode drops any sibling auth mode that's still mid-`connecting` on the same channel. - Wires `DiscordConfig` and `TelegramConfig` onto the shared hook; future channels with an OAuth auth mode inherit correct pending-state transitions automatically. - Covers the new reducer (4 cases) and hook (8 cases) with Vitest. ## Problem OAuth badges on the channel connection panels could get pinned at `Connecting` indefinitely (issue #2128): - `DiscordConfig` had a per-component `oauth:success` listener but no `oauth:error` listener — failed OAuth attempts never transitioned the badge out of `connecting`. - `TelegramConfig` had neither — completed *and* failed OAuth attempts left the badge pinned. - Both panels set `connecting` on the chosen auth mode but never cancelled any sibling auth mode that was already pending. Triggering a second OAuth method on Discord (`OAuth Sign-in` then `Login with OpenHuman`, or the reverse) left both methods badged `Connecting` simultaneously. This is the exact repro from the issue. The same shape was visible across GitHub/GitLab style multi-method panels because the underlying state model (`channelConnections`, keyed by `(channel, authMode)`) had no notion of mutual exclusion. ## Solution **Shared listener hook** — [`app/src/hooks/useOAuthConnectionListener.ts`](app/src/hooks/useOAuthConnectionListener.ts) subscribes to both `oauth:success` and `oauth:error` window events (dispatched from `utils/desktopDeepLinkListener.ts`), filters by `toolkit` / `provider` case-insensitively, and dispatches the matching slice action. Per-channel panels mount it once with `{ channel, authMode }`; cleanup on unmount is deterministic. New channels with an OAuth auth mode inherit the behaviour without copying any logic. **Pending-state cancellation reducer** — `clearOtherPendingForChannel({ channel, exceptAuthMode })` in `channelConnectionsSlice.ts` walks the auth-mode map for one channel and transitions every `connecting` row (except the exception) to `disconnected` with `lastError: undefined`. Cancelled rows go to `disconnected` rather than `error` so the UI doesn't surface a misleading failure — the user explicitly switched methods, they didn't experience an error. **Per-panel wiring** — `DiscordConfig` and `TelegramConfig` each: 1. Mount `useOAuthConnectionListener({ channel: <name>, authMode: 'oauth' })` at the top of the component (replacing the bespoke effect on Discord; net-new on Telegram). 2. Dispatch `clearOtherPendingForChannel` at the start of `handleConnect` *before* setting their own auth mode to `connecting`. **Tradeoffs** - The cancellation transition is `disconnected`, not a new `cancelled` state. Adding a dedicated state would expand the `ChannelConnectionStatus` union across many call sites for marginal UX value. - The deep-link CustomEvent payload (`{ integrationId, toolkit }` for success, `{ provider, errorCode, message }` for error) is unchanged, so no symmetric change in the Tauri-side handler is needed. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) — 12 new Vitest cases (4 reducer + 8 hook) covering success, error, mismatched channel, mismatched provider, missing error message, custom capabilities, unsubscribe on unmount, and three sibling-cancellation shapes. - [x] **Diff coverage ≥ 80%** — frontend-only change; `pnpm test:coverage` locally over the new files reaches 100% on changed lines (every branch in the hook + reducer is exercised by the suite). - [x] Coverage matrix updated — `N/A: behaviour-only fix on existing surfaces (channel connection pending state)`. - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` — `N/A: no feature ID changes`. - [x] No new external network dependencies introduced — purely in-app state plumbing. - [x] Manual smoke checklist updated if this touches release-cut surfaces — `N/A: no release-cut surface touched (channels panel is part of the always-shipped settings UX)`. - [x] Linked issue closed via `Closes #NNN` in the `## Related` section — see below. ## Impact - **Desktop only** — no mobile/web/CLI impact. The deep-link event source (`desktopDeepLinkListener.ts`) is Tauri-gated; the hook is a no-op outside Tauri because no deep-link events fire. - **No persistence shape change** — `channelConnections` slice schema (`SCHEMA_VERSION = 1`) is unchanged. The new reducer only mutates existing rows; no migration needed. - **No security implications** — the listener filters strictly by channel identifier and never reads tokens. Existing `[DeepLink][oauth:*]` logs remain the canonical diagnostic surface; the hook adds its own `channels:oauth-listener` debug namespace per the project's verbose-diagnostics rule. ## Related - Closes: #2128 - Follow-up PR(s)/TODOs: none ## Provider coverage The issue body mentions Discord, GitHub, and GitLab. The Channels page in this codebase only exposes three multi-method channel-config panels today: `DiscordConfig.tsx`, `TelegramConfig.tsx`, and `WebChannelConfig.tsx` (the last is not OAuth-driven). There is no `GitHubConfig.tsx` / `GitLabConfig.tsx` — verified via `find app/src -name "*Config.tsx"`. GitHub OAuth does appear elsewhere in the app, but on different state slices that this PR's `channelConnections`-bound hook does not (and should not) touch: | Surface | File(s) | State path | This PR applies? | |---|---|---|---| | App-level sign-in | `BootCheckGate.tsx`, OAuth callback | `deepLinkAuth` slice | No — different slice. App-level OAuth's hot-instance issue is the family fixed by #2228 / #2229. | | Skill OAuth install | `InstallSkillDialog.tsx`, `services/api/skillsApi.ts` | skills-domain state | No — different surface. | | Composio integration | `components/composio/TriggerToggles.tsx`, `composio/providerConfigs.tsx` | Composio integration state | No — different surface. | | **Channel config** (this PR) | `DiscordConfig.tsx`, `TelegramConfig.tsx` | `channelConnections` slice | **Yes — wired.** | So this PR's `useOAuthConnectionListener` covers every multi-method OAuth panel that actually exists on the Channels surface. The shared hook is also the right shape for any future `GitHubConfig.tsx` / `GitLabConfig.tsx` channel panels — wiring them in becomes a one-line `useOAuthConnectionListener({ channelId, capabilities, ... })` import. If the stale-`Connecting` symptom also surfaces in the app-level / skills / Composio OAuth flows, those are separate fixes against different state slices and out of scope for this PR — I'm happy to file follow-up issues if any are observed. --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A - URL: N/A ### Commit & Branch - Branch: `fix/2128-oauth-badge-pending-state` - Commit SHA: `2d93f7c0` ### Validation Run - [x] `pnpm --filter openhuman-app format:check` — `All matched files use Prettier code style!` on the 6 changed files - [x] `pnpm typecheck` — clean (`tsc --noEmit`) - [x] Focused tests: `pnpm --filter openhuman-app exec vitest run --config test/vitest.config.ts src/store/__tests__/channelConnectionsSlice.test.ts src/hooks/__tests__/useOAuthConnectionListener.test.tsx src/components/channels/__tests__/DiscordConfig.test.tsx src/components/channels/__tests__/TelegramConfig.test.tsx` → 4 files, 27 tests pass - [x] Rust fmt/check (if changed): `N/A: no Rust changes` - [x] Tauri fmt/check (if changed): `N/A: no Tauri shell changes` ### Validation Blocked - `command:` `git push` pre-push hook (`app:lint:commands-tokens`) - `error:` `lint:commands-tokens requires ripgrep` — `rg` not installed on the dev environment - `impact:` zero — the check greps a directory I did not modify (`src/components/commands/`). Pushed with `--no-verify` per the CLAUDE.md guidance for environment-related hook failures unrelated to the diff. Maintainers can re-run on CI to validate. ### Behavior Changes - Intended behavior change: OAuth badges on channel panels transition out of `connecting` when the OAuth flow completes *or* fails, and starting a new method cancels the previous method's `connecting` row. - User-visible effect: the reported bug (multiple methods stuck on `Connecting` simultaneously, Telegram OAuth never clearing) goes away. No new UI elements; only badge state transitions are affected. ### Parity Contract - Legacy behavior preserved: existing `connected` and `error` transitions are unchanged; `disconnectChannelConnection`, `upsertChannelConnection`, `setChannelConnectionStatus` are all untouched. The Discord `oauth:success` path still produces the same final state (`status: 'connected'`, `capabilities: ['read', 'write']`); the inline effect was just refactored behind the shared hook. - Guard/fallback/dispatch parity checks: hook only reacts when the event's `toolkit` (success) or `provider` (error) field matches the subscribed channel — siblings on other channels, and mismatched dispatches, are no-ops. ### Duplicate / Superseded PR Handling - Duplicate PR(s): none found. #2170 cross-references #2128 in passing but its title and body close #2141 (channel selector error-status aggregation, a different surface). - Canonical PR: this one. - Resolution: N/A. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Reusable OAuth connection listener to handle OAuth success/error deep-link flows for Discord and Telegram. * New action to clear other pending/connecting auth methods for a channel. * **Bug Fixes** * Prevents multiple auth methods from remaining "connecting"; switching stops in-flight polling and clears sibling pending modes. * OAuth errors now record meaningful messages and listeners unsubscribe on unmount. * **Tests** * Added tests covering the OAuth listener and pending-clearing reducer behaviors. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2256?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
1e3ecc55ee |
feat(composio,agent): async sync + gated-tools surface + UI-only scope elevation + force-delegate capability questions (#2348)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cc498d1727 |
fix(onboarding): capture completeAndExit rejection in Sentry (#2081) (#2327)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
997be4eef2 |
fix(composio): trim API key in ComposioTool constructor (#2323) (#2338)
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c9a77c8757 |
fix(telegram): stop duplicate operator-approval prompts for allowed users (#1948) (#2240)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
71526ea4ab |
fix(linux): restore tauri-cef pin so AppImage stops bundling libm.so.6 (#2154) (#2236)
Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai> |
||
|
|
fd657e94c6 |
fix(docker): chown workspace volume before dropping privileges so core can start (#2065) (#2235)
Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai> |
||
|
|
1dbff180a3 | fix(memory): accept ISO-8601 / missing modified_at + default provider (#2237) | ||
|
|
cf5dd2d7c7 | fix(voice): bound dictation WS audio buffer (partial fix for #1924) (#2238) | ||
|
|
b8fbb364fb | fix(observability): classify provider config-rejection 4xx as expected user-state (#2079) (#2239) | ||
|
|
1f98614869 |
feat(inference): gate local Ollama models by memory-layer context-window minimum (#2122)
Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai> |
||
|
|
d4dacd62ae |
feat(learning): explicit user-preference tool + always-on pinned-facet prompt injection (#2150)
Co-authored-by: Gray Cyrus <cyrus@tinyhumans.ai> |
||
|
|
de0b254898 |
fix(core): skip memory init on config failure instead of fabricating a default workspace (OPENHUMAN-CORE-48) (#2120)
Co-authored-by: Gray Cyrus <cyrus@tinyhumans.ai> |
||
|
|
81a0faef9d |
feat(memory): #1574 Stage 2 — per-(row,model) embedding storage live (cutover + migration + re-embed backfill + switch UX) (#2153)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
341ce42ff1 |
feat(memory): activate chat memory pipeline — Archivist wiring, LLM recaps, cross-thread STM recall, segment-granular tree, unified compaction (#2175)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e052aadfe9 |
feat(ai): unified per-workload provider routing + chat-provider factory (#1710) (#1858)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
3e148f66cb |
feat(composio): bring-your-own Composio direct mode (#1710) (#1825)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9ab8af28f3 | fix(socket): round event-payload log truncation to UTF-8 boundary (#1818) | ||
|
|
9eee92e336 | fix(security): round command-log truncation to UTF-8 boundary (#1817) | ||
|
|
acdc818de8 |
fix(voice): atomic install-start guard for Whisper/Piper install RPCs (#1787)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3165306bd1 |
feat(voice): fully-local STT + TTS via Whisper/Piper provider factory (#1755)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd30a4b029 |
fix(memory_tree, e2e tests ): deterministic query_topic ordering + robust CEF cleanup (#1751)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a7a262ba9c |
chore(migrations): phase out PROFILE.md from disk on schema_version=1 (#1734)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c3abafdf11 |
fix(agent): refresh delegation surface on mid-session Composio connect/revoke (#1687)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
83d5adba66 |
feat(local_ai): bind owned ollama serve lifecycle to openhuman (#1622) (#1638)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
256f004c36 |
feat(local_ai): unify memory embeddings — cloud Voyage default + live local toggle + dev infra (#1640)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bddfbb12c8 | fix(chat): stop duplicating assistant replies on multi-segment turns (#1648) | ||
|
|
acebeb12b6 |
fix(tauri): hide main window via OS API on Windows close (#1607) (#1621)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ea9a897eea |
fix(scheduler_gate): ignore SIGNED_OUT override when gate is uninit (unblocks Rust Core test suite) (#1552)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
f82f2d524d |
fix(socket): suppress retry-storm Sentry noise + empty-token guard (OPENHUMAN-TAURI-8M) (#1568)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
4be66bafd1 |
feat(local_ai): Ollama precondition gate + install UX hardening (#1475) (#1569)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
ad11a08e54 |
fix(sentry): drop dev-server fetch noise from Tauri shell events (OPENHUMAN-TAURI-V) (#1545)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
75097e66d5 |
fix(rpc): rewrite legacy method names server-side before dispatch (OPENHUMAN-TAURI-BQ) (#1544)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
84d86da70e |
fix(ipc): guard isTauri() on __TAURI_INTERNALS__.invoke (OPENHUMAN-REACT-S) (#1556)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
3d4e4d278c |
fix(webview_apis): always bind ephemeral port, ignore stale PORT_ENV (OPENHUMAN-TAURI-82) (#1543)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
7ce319362b |
fix(windows): wire CEF keyboard input routing on cold launch (#1528)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f0606ba703 |
fix(windows): silence conhost flashes from core-side spawns (#731/#1338 follow-up) (#1498)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
12ef69b89f | feat(learning): ambient personalization cache for #566 (#1460) | ||
|
|
b8922c2e28 | feat(memory): self-identity tagging via Composio identity registry (#1365) (#1381) | ||
|
|
fd5a644338 | feat(memory_tree/jobs): JobOutcome::Defer + mark_deferred (#1256) (#1345) | ||
|
|
34188130a0 |
feat(subconscious): memory-context-aware reflection threads + dedupe gates (#623) (#1344)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b7198063de | fix(welcome): include bearer token in core RPC test-connection probe (#1301) | ||
|
|
0c546fcffe |
feat(dev): Windows dev environment for pnpm dev:app:win (#1302)
|
||
|
|
fff1339532 |
feat(memory): per-source memory sync status (#1136) (#1250)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
65032870f2 |
feat(scheduler_gate): throttling + power-aware execution for local LLM (#1073) (#1252)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
86740e2699 |
fix(memory/tree): emit summary children as Obsidian wikilinks (#1210)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
62516ba418 |
feat(memory_tree): cloud-default LLM, queue priority, entity filter, Memory tab UI (#1198)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b90dad121d |
fix(composio/gmail): phase out html2md, prefer text/plain MIME part (#1170)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
677e941ac1 |
chore(memory_tree): drop body-read timeouts on Ollama HTTP calls (#1171)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b8113e96a5 |
feat(memory_tree): switch embed model to bge-m3 (1024-dim, 8K context) (#1174)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
44988bdb80 |
feat(composio/gmail): sync into memory tree (Slack-parity) (#1056)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
151451f66e |
fix(channels): preserve overflow segments instead of dropping them (#1041) (#1051)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0768d18873 |
feat(orchestrator): wire memory-tree retrieval tools with periodic prefetch (#1027)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fd0398d5fa |
feat(memory): MD-on-disk content + participant bucketing + async pipeline foundation (#1008)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
e3c46d1c3d |
fix(whatsapp): upgrade WhatsApp Web channel to upstream whatsapp-rust 0.5 (#916)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
238a9a5ad9 |
feat(slack): backfill ingestion + LLM summariser + tree fanout gate (#934)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c0f6e39b3e |
feat(memory_tree): Phase 4 retrieval tools for hierarchical memory (#710) (#831)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
629b4ccb07 |
test(memory_tree): unit tests for retrieval RPC handlers (#710) (#839)
- Add JSON-RPC handlers for source, global, and topic-based memory retrieval. - Integrate Ollama embeddings for semantic reranking of chunks and summaries. - Implement 15 unit tests for RPC handlers covering parameter parsing and PII redaction. - Redact PII from logs by removing raw source, entity, and node identifiers. - Fix BFS traversal for drill-down and deduplicate results in topic queries. - Add configuration for embedding endpoints, models, and strictness modes. Co-authored-by: sanil-23 <sanil@vezures.xyz> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
32b5afd1d9 |
feat(memory): topic trees (lazy hotness-driven materialisation) (#709) (#799)
Phase 3c of the memory architecture (umbrella #711). Adds per-entity topic trees spawned lazily when an entity's hotness crosses a threshold, so Phase 4 retrieval can resolve "what did Alice say about Phoenix?" without scanning the full chunk pool. ## What's in this commit New module `src/openhuman/memory/tree/topic_tree/`: - `types.rs` — `EntityIndexStats` (hotness input), `HotnessCounters` (the persisted row) and the `TOPIC_CREATION_THRESHOLD=10.0`, `TOPIC_ARCHIVE_THRESHOLD=2.0`, `TOPIC_RECHECK_EVERY=100` constants. - `hotness.rs` — pure arithmetic scorer plus deterministic `hotness_at(entity_id, stats, now_ms)` variant for tests, and a piecewise-linear `recency_decay` helper. - `store.rs` — SQLite helpers (`get`, `get_or_fresh`, `upsert`, `distinct_sources_for`, `count`) for the new `mem_tree_entity_hotness` table. - `registry.rs` — `get_or_create_topic_tree`, `force_create_topic_tree`, `list_topic_trees`, `archive_topic_tree`. Race-recovers on UNIQUE violations via `is_unique_violation`, mirroring `source_tree::registry`. - `curator.rs` — `maybe_spawn_topic_tree` (per-ingest tick) and `force_recompute` (admin path). Implements the `TOPIC_RECHECK_EVERY`-gated recompute: refresh `distinct_sources` from the entity index, compute hotness, spawn + backfill if threshold crossed. - `backfill.rs` — `backfill_topic_tree` walks the entity index and routes every historic leaf (ts-ASC ordered) through `source_tree::bucket_seal::append_leaf`. Capped at 500 leaves. Skips summary-node hits so topic trees only ever ingest raw leaves. Missing chunks log a warn and are skipped, never failing the spawn. - `routing.rs` — `route_leaf_to_topic_trees` fans a kept leaf out to every active matching topic tree and ticks the curator for each entity. Archived trees are skipped but counters still bump. Wired from ingest (`tree/ingest.rs::append_leaves_to_tree`) AFTER the source-tree `append_leaf` succeeds — non-fatal on error, logged at warn level so routing issues never poison the ingest hot path. ## Hotness scoring (pure) ``` hotness = ln(mentions + 1) // dampened volume + 0.5 * distinct_sources // cross-source bonus + recency_decay(last_seen) // 1.0 @ day 0 → 0 @ day 30 + graph_centrality // Phase 4+; None → 0 + 2.0 * query_hits // retrieval feedback; Phase 4+ ``` `graph_centrality` and `query_hits_30d` columns are persisted but the increment code paths are deferred to later phases, as planned. ## Schema (additive, idempotent) New table `mem_tree_entity_hotness` (keyed on `entity_id`) added to the Phase 1 `SCHEMA` constant in `tree/store.rs` so it migrates through the same `with_connection` path as the existing tables. `CREATE TABLE IF NOT EXISTS` keeps it re-run-safe. An ancillary index on `last_hotness` supports future sweep queries. ## Routing The ingest path is the only non-admin caller. After `append_leaf` puts a leaf in the source tree, `route_leaf_to_topic_trees` runs with the chunk's canonical entity list. For each entity: 1. If an active topic tree exists, append the leaf to it (reusing `source_tree::bucket_seal::append_leaf` with `entities=[entity_id]`). 2. Tick `maybe_spawn_topic_tree` — may bump counters or, on cadence, recompute hotness and spawn + backfill a new tree. Per-entity errors are caught and logged; a top-level failure is demoted to a warn in the ingest caller so the source-tree append always wins. ## Reuses from Phase 3a - `source_tree::bucket_seal::append_leaf` — same `&Tree` API works for `TreeKind::Topic` end-to-end. - `source_tree::summariser::{Summariser, InertSummariser}` — honest stub emits empty entity/topic vecs on summary nodes. - `mem_tree_trees` / `mem_tree_summaries` / `mem_tree_buffers` schema, discriminated by `kind = 'topic'` and `scope = <entity canonical id>`. - Registry race recovery via `is_unique_violation` (catches UNIQUE on `insert_tree`, re-queries on collision). - Idempotent append (duplicate `item_ids` in the L0 buffer are no-ops), so backfill is safe to re-run. ## Tests 40 new tests (10 hotness, 3 types, 6 store, 7 registry, 4 curator, 4 backfill, 5 routing, 1 integration). `memory::tree` suite: 201 passing, 0 regressions (Phase 3a baseline was 161 → +40 new). Coverage highlights: - Hotness pure math (zero-entity, spike-over-threshold, old-but-widely- cited retains signal, query-hit boost, recency decay edges). - Curator: first-ingest-just-bumps, no-spawn-below-threshold, spawn-fires-exactly-once-when-crossed, cadence gating. - Backfill: appends-all-entity-leaves, skips missing chunks, idempotent, skips summary nodes. - Routing: empty-entities-noop, appends-to-existing-tree, archived-tree- skipped, multi-entity-fan-out, end-to-end-integration-materialisation. - Registry: idempotent get-or-create, UNIQUE race recovery, archive flips status (not deletion), kind/scope cleanly separated from source. ## Deliberate non-goals (deferred) - No JSON-RPC surface (out of scope for Phase 3c core). - No archive cron sweep — `archive_topic_tree` primitive only. - `graph_centrality` / `query_hits_30d` increments deferred to later phases (columns exist, reads work, writes are Phase 4+). ## Stacked on #789 Base is `feat/709-summary-trees`. Merge #789 first, then rebase this branch. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
48897af9b3 |
feat(memory): global activity digest tree (#709) (#798)
Phase 3b of the memory architecture (umbrella #711). Adds a singleton cross-source `global` tree whose purpose is time-windowed recap ("what did I do in the last 7 days?") via a time-axis-aligned hierarchy: L0 = day, L1 = week (7 dailies), L2 = month (4 weeklies), L3 = year (12 monthlies). ## What's in this PR - `global_tree/registry.rs` — singleton `get_or_create_global_tree` with the same UNIQUE-race recovery pattern Phase 3a uses for source trees (scope is the literal `"global"`). - `global_tree/digest.rs` — `end_of_day_digest(config, day, summariser)` walks every active source tree, picks one representative contribution per tree (latest L1+ intersecting the day, fallback to root), folds them with the Summariser trait into one L0 daily node, inserts it into `mem_tree_summaries`, and triggers the L0→L1→L2→L3 cascade. Idempotent on re-run (returns `DigestOutcome::Skipped` when an L0 already exists for the day). - `global_tree/seal.rs` — count-based cascade-seal with thresholds 7 (weekly), 4 (monthly), 12 (yearly). Transactional append with idempotency on (tree_id, level, item_id) to survive partial retries. - `global_tree/recap.rs` — `recap(config, window)` maps the window to a level (<2d→L0, <14d→L1, <60d→L2, else L3), fetches covering summaries, and falls back downward when the chosen level has no sealed material yet (reports `level_used` so callers can surface "best available"). Empty tree returns `None`. - `source_tree/store.rs` — adds `list_trees_by_kind` used by the digest to enumerate source trees. - `tree/mod.rs` — exports the new `global_tree` module. ## Reuses from Phase 3a - `source_tree::store` CRUD for `mem_tree_trees` / `mem_tree_summaries` / `mem_tree_buffers` (no new schema tables). - `source_tree::summariser::{Summariser, SummaryContext, SummaryInput, InertSummariser}` — honest-stub entity/topic semantics kept as-is. - `source_tree::registry::new_summary_id` for id generation. - `source_tree::types::{Tree, SummaryNode, Buffer, TreeKind::Global, TreeStatus}` — `TreeKind::Global` was already declared in Phase 3a. - Entity backfill via `tree::score::store::index_summary_entity_ids_tx` so Phase 4 retrieval can resolve "summaries mentioning X" through the same inverted index as leaves. ## Design decision: seal.rs kept as parallel impl `source_tree::bucket_seal::cascade_all_from` uses a token-budget seal policy (`TOKEN_BUDGET`), not pluggable. The global tree needs count-based thresholds per level. Rather than refactoring the stable Phase 3a seal pipeline to accept a policy (regression risk), we keep `global_tree/seal.rs` as a parallel count-based implementation that routes through the same `source_tree::store` primitives. The shape of the transaction is intentionally identical so future consolidation is straightforward when we're ready to touch the Phase 3a seal path. ## Tests 15 new tests; 176 total `memory::tree::*` passing (was 161). Coverage: - registry idempotency, ID prefix, UNIQUE-race recovery - digest empty-day, populated-day, rerun idempotency, 7-day weekly cascade - seal below-threshold, weekly-threshold, append idempotency - recap level selection across all four bands, empty-tree, L0 fallback, L1 when sealed ## Stacked on #789 Base is `feat/709-summary-trees` (Phase 3a). Merge #789 first, then rebase this branch. PR body should flag the stack order. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
37a73df0c0 |
feat(memory): source trees + bucket-seal foundation (#709) (#789)
* feat(memory): LLM-based NER + importance signal with cheap-signals short-circuit (#708)
Adds an Ollama-backed entity extractor and an LLM-derived importance
score as a new signal in the admission gate. Builds on merged Phase 2
(#733) without changing its surface — additive only.
Why LLM (not GLiNER): zero new deps (reuses existing reqwest + async
infra), reuses the same Ollama setup openhuman uses for embeddings,
better per-entity quality scales with model choice, no native ONNX
runtime to ship. Latency cost (~100-300ms per chunk on a small model
like qwen2.5:0.5b) is amortised by the short-circuit.
The admission gate stays a hybrid - cheap deterministic signals
always run; the LLM is one signal among many, never the sole arbiter.
Backwards-compatible: with default SignalWeights the LLM weight is 0.0
and behaviour matches pre-LLM Phase 2 exactly.
What's new:
- src/openhuman/memory/tree/score/extract/llm.rs: LlmEntityExtractor
implementing the existing EntityExtractor trait. Posts a structured
JSON request to an Ollama-compatible /api/chat endpoint asking for
NER + an importance rating in one call. Span recovery via string
search; hallucinated entities (surface not in source text) dropped.
Soft fallback: HTTP failures log a warn and return empty extraction.
- ExtractedEntities gains llm_importance (Option<f32>) +
llm_importance_reason (Option<String>); merge() takes max importance.
- ScoreSignals gains llm_importance (f32, defaults to 0.0).
- SignalWeights gains llm_importance (default 0.0; with_llm_enabled()
helper sets it to 2.0 - comparable to metadata/source weights).
- signals::combine_cheap_only(): variant that excludes the LLM signal,
used for the short-circuit decision in score_chunk.
- ScoringConfig gains llm_extractor (Option<Arc<dyn EntityExtractor>>),
definite_keep_threshold (default 0.85), definite_drop_threshold
(default 0.15). New with_llm_extractor() constructor.
- score_chunk() pipeline:
1. Always-on regex extraction
2. Cheap signals + combine_cheap_only -> cheap_total
3. If cheap_total >= definite_keep OR <= definite_drop: skip LLM
4. Else (borderline band): run LLM extractor, merge results, recompute
5. Final combine + admission gate against drop_threshold
- mem_tree_score table gains nullable llm_importance + llm_importance_reason
columns via idempotent ALTER TABLE migration.
- ScoreRow + upsert/get persist the new column.
What's not changed:
- Existing JSON-RPC surface
- Default behaviour (LLM weight=0, no llm_extractor configured = same
outputs as before)
- mem_tree_chunks / mem_tree_entity_index schemas
Tests added:
- LLM extractor: prompt construction, JSON parsing, hallucination
drop, importance clamping, strict vs lenient unknown-kind handling
- Score pipeline: short-circuit on definite_keep/definite_drop skips
LLM call (verified via FakeLlm call counter); borderline band
consults LLM once; LLM failure falls back gracefully without erroring
LLM-NER work follows up on #708. Parent: #711.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(memory): address CI failures and CodeRabbit review on LLM-NER (#708)
CI was red on PR #775 due to 10 E0063 errors — existing test-only struct
literals for ExtractedEntities / ScoreSignals / ScoreRow weren't updated
when the new optional llm_importance fields landed. Local cargo check
on lib-only passed; cargo check --tests caught them, as does CI.
CodeRabbit also flagged four semantic issues:
1. LlmEntityExtractor::extract() returned Err on HTTP transport, non-2xx
status, and JSON parse failures. The soft-fallback contract (documented
in the module header) only worked because score_chunk catches errors;
any other caller got a hard error. Now split into extract_or_empty()
which logs a warn on each failure mode and returns
ExtractedEntities::default() unconditionally. The public extract()
stays on the async-trait surface but never returns Err.
2. find_char_span() always searched from byte offset 0, so when the LLM
returned the same surface twice (explicitly asked for duplicates in
the prompt), both entities got (start=0, end=len) — same span, not
distinct mentions. Added find_char_span_from(haystack, needle,
byte_from, char_from) that resumes from a cursor. into_extracted_entities
now tracks a per-surface (byte_after, char_after) cursor in a
HashMap<String, (usize, u32)>, so duplicate mentions get distinct
non-overlapping spans and over-claim (LLM returns 3 "Alice" when
source has 2) drops the excess entries with a debug log.
3. combine() always included w.llm_importance in the denominator even
when llm_importance=0.0 because the LLM didn't run. That artificially
lowered the total on short-circuit paths. score_chunk now branches on
llm_consulted: uses combine() when LLM ran (importance actually
contributes), combine_cheap_only() when LLM was skipped or failed
(denominator excludes the LLM weight). Observable in two new tests:
short_circuit_reports_cheap_only_total verifies r.total ==
combine_cheap_only(r.signals, weights) and strictly exceeds
combine(r.signals, weights) when llm_importance=0; llm_consulted_
reports_full_total verifies the opposite path.
4. Same issue from a different angle (CodeRabbit flagged both). Fixed
by the same llm_consulted branch.
Test-literal fixes:
- signals/ops.rs: ScoreSignals literals pick up llm_importance field;
make_entities helper uses ..Default::default() on ExtractedEntities.
- extract/types.rs: three test ExtractedEntities literals gain
llm_importance: None, llm_importance_reason: None.
- resolver.rs: canonicalise_batch_preserves_spans literal gains the
two fields.
- score/store.rs: sample_row gains signals.llm_importance and
llm_importance_reason.
New tests (7 added):
- find_char_span_from_advances_past_prior_match
- find_char_span_from_returns_none_after_exhaustion
- find_char_span_from_preserves_utf8
- find_char_span_from_rejects_non_char_boundary
- into_extracted_entities_gives_distinct_spans_to_duplicate_mentions
- into_extracted_entities_drops_extra_duplicate_when_source_only_has_one
- extract_soft_fallback_on_unreachable_endpoint (transport failure →
empty extraction, not Err)
- short_circuit_reports_cheap_only_total
- llm_consulted_reports_full_total
cargo check --lib clean; cargo fmt clean. The integration-test rmeta
errors visible locally are stale-metadata artifacts from incremental
builds with prior struct shapes; CI's clean build resolves them and
tests/*.rs files do not construct any of the touched types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(memory): source trees + bucket-seal foundation (#709)
Phase 3a of the memory architecture (#711 umbrella). Lifts admitted leaves
into a per-source hierarchy so Phase 4 retrieval has a tree to walk.
## What's in this PR
* **Schema** — three new tables in `chunks.db` alongside Phase 1/2:
`mem_tree_trees`, `mem_tree_summaries`, `mem_tree_buffers`. Plus a
`parent_summary_id` backlink column on `mem_tree_chunks`. All migrations
are additive and idempotent (`ALTER TABLE ADD COLUMN`, same pattern as
Phase 2).
* **`source_tree/` module** — isolated subdir; does not touch the legacy
`openhuman::memory` layer.
- `types.rs` — `Tree`, `SummaryNode`, `Buffer`, `TreeKind` (Source/Topic/Global),
`TreeStatus`. Constants: `TOKEN_BUDGET = 10_000`, `DEFAULT_FLUSH_AGE_SECS = 7d`.
- `store.rs` — CRUD for all three tables via the shared `with_connection`
entry point. Writes are transactional; summary insert is idempotent
on primary key (INSERT OR IGNORE).
- `registry.rs` — `get_or_create_source_tree(scope)` — idempotent lookup
keyed on `(kind, scope)`.
- `bucket_seal.rs` — `append_leaf` + cascade: push a leaf into L0, seal
when `token_sum >= TOKEN_BUDGET`, recurse upward. One seal = one
transaction; children get a parent backlink (leaves via
`parent_summary_id`, summaries via `parent_id`). Tree's `max_level`
and `root_id` bump on root split.
- `flush.rs` — `flush_stale_buffers(max_age)` force-seals any buffer
whose `oldest_at` is older than `max_age`. Primitive only; wiring
into a scheduler is out of scope.
- `summariser/` — `Summariser` trait + `InertSummariser` fallback
(concatenate children, union entities preserving first-seen order,
hard-truncate to budget). Trait is async; Ollama implementation slots
in later without breaking callers.
* **Ingest wiring** — `tree/ingest.rs::persist` now calls `append_leaf`
once per kept chunk after chunks + scores land. Failures are logged at
warn level and do not fail the ingest — leaves are already persisted
and the next flush can still rebuild the tree.
## Concurrency / LLM
The async summariser call happens OUTSIDE any DB transaction, so a slow
LLM never holds SQLite locks. Blocking DB calls inside `append_leaf` are
acceptable for Phase 3a because the Inert summariser does no real I/O;
when a networked summariser lands, wrap the DB calls in
`tokio::task::spawn_blocking`.
## Tests (23 new, all green)
* `types` — enum round-trips, buffer staleness predicates
* `store` — tree insert + unique scope, summary insert + idempotence,
buffer upsert/clear, stale-buffer ordering
* `registry` — `get_or_create` idempotence, distinct scopes yield distinct
trees, id prefix format
* `summariser/inert` — provenance-prefixed concat, entity union with
order preservation, budget truncation, empty-content skip
* `bucket_seal` — append-below-budget is buffered only (no seal);
12k-token cross-over produces an L1 summary with correct `child_ids`,
L0 cleared, L1 carries the new summary id, tree `max_level`/`root_id`
updated, leaf → parent backlink populated
* `flush` — stale buffer force-seals under budget; recent buffer is a no-op
Broader `memory::tree` suite: 160 tests pass (23 new + 137 existing Phase
1/2), no regressions.
## Stacked on #775
This PR applies on top of `feat/708-memory-llm-ner` (PR #775, Phase 2
LLM-NER follow-up). Merge #775 first; this branch will rebase cleanly
onto main.
## Out of scope (tracked separately)
* **3b — Global activity digest tree** — time-aligned cross-source recap
* **3c — Topic trees** — hotness-driven per-entity materialisation
* **Phase 4 (#710)** — retrieval / query / gate
Closes part of #709 (source-tree foundation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(memory): cargo fmt on source_tree module (#709)
CI rustfmt caught a few multi-line function signatures and import
groups that rustfmt wants collapsed / reordered. No behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(memory): address CodeRabbit review on source-tree (#709)
Three correctness fixes from CodeRabbit on PR #789, plus a design
clarification for the InertSummariser. False-positive findings
(schema-out-of-sync, rustfmt — already in
|
||
|
|
2b53566283 |
feat(memory): LLM-based NER + importance signal with cheap-signals short-circuit (#708) (#775)
* feat(memory): LLM-based NER + importance signal with cheap-signals short-circuit (#708) Adds an Ollama-backed entity extractor and an LLM-derived importance score as a new signal in the admission gate. Builds on merged Phase 2 (#733) without changing its surface — additive only. Why LLM (not GLiNER): zero new deps (reuses existing reqwest + async infra), reuses the same Ollama setup openhuman uses for embeddings, better per-entity quality scales with model choice, no native ONNX runtime to ship. Latency cost (~100-300ms per chunk on a small model like qwen2.5:0.5b) is amortised by the short-circuit. The admission gate stays a hybrid - cheap deterministic signals always run; the LLM is one signal among many, never the sole arbiter. Backwards-compatible: with default SignalWeights the LLM weight is 0.0 and behaviour matches pre-LLM Phase 2 exactly. What's new: - src/openhuman/memory/tree/score/extract/llm.rs: LlmEntityExtractor implementing the existing EntityExtractor trait. Posts a structured JSON request to an Ollama-compatible /api/chat endpoint asking for NER + an importance rating in one call. Span recovery via string search; hallucinated entities (surface not in source text) dropped. Soft fallback: HTTP failures log a warn and return empty extraction. - ExtractedEntities gains llm_importance (Option<f32>) + llm_importance_reason (Option<String>); merge() takes max importance. - ScoreSignals gains llm_importance (f32, defaults to 0.0). - SignalWeights gains llm_importance (default 0.0; with_llm_enabled() helper sets it to 2.0 - comparable to metadata/source weights). - signals::combine_cheap_only(): variant that excludes the LLM signal, used for the short-circuit decision in score_chunk. - ScoringConfig gains llm_extractor (Option<Arc<dyn EntityExtractor>>), definite_keep_threshold (default 0.85), definite_drop_threshold (default 0.15). New with_llm_extractor() constructor. - score_chunk() pipeline: 1. Always-on regex extraction 2. Cheap signals + combine_cheap_only -> cheap_total 3. If cheap_total >= definite_keep OR <= definite_drop: skip LLM 4. Else (borderline band): run LLM extractor, merge results, recompute 5. Final combine + admission gate against drop_threshold - mem_tree_score table gains nullable llm_importance + llm_importance_reason columns via idempotent ALTER TABLE migration. - ScoreRow + upsert/get persist the new column. What's not changed: - Existing JSON-RPC surface - Default behaviour (LLM weight=0, no llm_extractor configured = same outputs as before) - mem_tree_chunks / mem_tree_entity_index schemas Tests added: - LLM extractor: prompt construction, JSON parsing, hallucination drop, importance clamping, strict vs lenient unknown-kind handling - Score pipeline: short-circuit on definite_keep/definite_drop skips LLM call (verified via FakeLlm call counter); borderline band consults LLM once; LLM failure falls back gracefully without erroring LLM-NER work follows up on #708. Parent: #711. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(memory): address CI failures and CodeRabbit review on LLM-NER (#708) CI was red on PR #775 due to 10 E0063 errors — existing test-only struct literals for ExtractedEntities / ScoreSignals / ScoreRow weren't updated when the new optional llm_importance fields landed. Local cargo check on lib-only passed; cargo check --tests caught them, as does CI. CodeRabbit also flagged four semantic issues: 1. LlmEntityExtractor::extract() returned Err on HTTP transport, non-2xx status, and JSON parse failures. The soft-fallback contract (documented in the module header) only worked because score_chunk catches errors; any other caller got a hard error. Now split into extract_or_empty() which logs a warn on each failure mode and returns ExtractedEntities::default() unconditionally. The public extract() stays on the async-trait surface but never returns Err. 2. find_char_span() always searched from byte offset 0, so when the LLM returned the same surface twice (explicitly asked for duplicates in the prompt), both entities got (start=0, end=len) — same span, not distinct mentions. Added find_char_span_from(haystack, needle, byte_from, char_from) that resumes from a cursor. into_extracted_entities now tracks a per-surface (byte_after, char_after) cursor in a HashMap<String, (usize, u32)>, so duplicate mentions get distinct non-overlapping spans and over-claim (LLM returns 3 "Alice" when source has 2) drops the excess entries with a debug log. 3. combine() always included w.llm_importance in the denominator even when llm_importance=0.0 because the LLM didn't run. That artificially lowered the total on short-circuit paths. score_chunk now branches on llm_consulted: uses combine() when LLM ran (importance actually contributes), combine_cheap_only() when LLM was skipped or failed (denominator excludes the LLM weight). Observable in two new tests: short_circuit_reports_cheap_only_total verifies r.total == combine_cheap_only(r.signals, weights) and strictly exceeds combine(r.signals, weights) when llm_importance=0; llm_consulted_ reports_full_total verifies the opposite path. 4. Same issue from a different angle (CodeRabbit flagged both). Fixed by the same llm_consulted branch. Test-literal fixes: - signals/ops.rs: ScoreSignals literals pick up llm_importance field; make_entities helper uses ..Default::default() on ExtractedEntities. - extract/types.rs: three test ExtractedEntities literals gain llm_importance: None, llm_importance_reason: None. - resolver.rs: canonicalise_batch_preserves_spans literal gains the two fields. - score/store.rs: sample_row gains signals.llm_importance and llm_importance_reason. New tests (7 added): - find_char_span_from_advances_past_prior_match - find_char_span_from_returns_none_after_exhaustion - find_char_span_from_preserves_utf8 - find_char_span_from_rejects_non_char_boundary - into_extracted_entities_gives_distinct_spans_to_duplicate_mentions - into_extracted_entities_drops_extra_duplicate_when_source_only_has_one - extract_soft_fallback_on_unreachable_endpoint (transport failure → empty extraction, not Err) - short_circuit_reports_cheap_only_total - llm_consulted_reports_full_total cargo check --lib clean; cargo fmt clean. The integration-test rmeta errors visible locally are stale-metadata artifacts from incremental builds with prior struct shapes; CI's clean build resolves them and tests/*.rs files do not construct any of the touched types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5fff5568d3 |
feat(memory): Phase 2 memory tree - preprocessing, scoring, admission gate (#708) (#733)
* feat(memory): phase 1 memory tree - multi-source ingestion & canonical chunks (#707) Adds an isolated memory tree layer under src/openhuman/memory/tree/ implementing Phase 1 of the new memory architecture (umbrella #711). Zero edits to existing memory/*.rs files - the new layer coexists with the legacy TinyHumans-backed client. - Source adapters: chat / email / document -> canonical Markdown - Token-bounded chunker with deterministic SHA-256 chunk IDs - SQLite persistence at <workspace>/memory_tree/chunks.db with full provenance metadata (source_kind, source_id, owner, timestamps, tags, time_range) and back-pointer to raw source - Unified JSON-RPC ingest (dispatches on source_kind + JSON payload): openhuman.memory_tree_ingest, _list_chunks, _get_chunk - DataSource enum covering the 8 providers from m.excalidraw step 1 (Discord/Telegram/Whatsapp/Gmail/OtherEmail/Notion/MeetingNotes/DriveDocs) - ~40 unit tests (chunk ID stability, UTF-8-safe splitting, canonicalisation idempotence, store round-trip, filter behavior) Additive only: new tables in a new DB file, new JSON-RPC namespace, no existing behavior changes. Feeds #708 (scoring), #709 (summary trees), #710 (query tools). Closes #707. Parent: #711. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(memory): phase 2 memory tree - preprocessing, scoring, admission gate (#708) Adds the scoring / admission layer between Phase 1's chunker and store. Stacked on feat/707-memory-ingestion (PR #732) - depends on Phase 1's chunk substrate. - Pluggable EntityExtractor trait + CompositeExtractor chain - RegexEntityExtractor: mechanical entities (emails, URLs, @handles, #hashtags) - Always on, deterministic, zero deps, UTF-8-safe char spans - Five weighted signals: token count, unique-word ratio, metadata weight, source weight (per-DataSource), interaction (reply/sent/mention/dm tags), entity density - Exact-match entity canonicalisation (email lowercased, @ and # stripped) - Admission gate drops chunks below configurable threshold (default 0.3) - Score rationale persists for EVERY chunk (kept or dropped) for debugging - Entities indexed for KEPT chunks only - Two new SQLite tables added to the memory_tree DB: - mem_tree_score: per-chunk score rationale with all signal values - mem_tree_entity_index: inverted index entity_id -> node_id - Idempotent ALTER TABLE migration adds embedding BLOB column to mem_tree_chunks (used in Phase 3 retrieval, wired but not populated here) - Ingest pipeline converted to async to accommodate the extractor trait; blocking SQLite work isolated on spawn_blocking; JSON-RPC surface unchanged (same memory_tree_ingest / list / get methods) - Phase 2 deliberately ships without GLiNER/semantic NER - per-chunk semantic entities land later behind a cargo feature flag; the composite extractor interface keeps that drop-in trivial Additive only: new tables, new columns, new module. Existing Phase 1 behavior unchanged except that low-signal chunks are now dropped before reaching mem_tree_chunks. Raise score_drop_threshold to 0 to disable the gate and restore Phase-1-identical behavior. Closes #708. Parent: #711. Depends on: #707 (#732). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix memory tree scoring persistence issues * Fix memory tree scoring robustness issues from PR review - ingest: fail fast if scorer returns fewer/more results than chunks (silent zip truncation would drop chunks or their score rationale) - score::persist_score{,_tx}: clear stale entity-index rows before re-indexing a re-scored chunk, since INSERT OR REPLACE never deletes rows whose entity_id is no longer in the new extraction - score::store::lookup_entity: clamp limit to i64::MAX before casting to prevent a large usize wrapping into a negative LIMIT Adds clear_entity_index_drops_stale_rows regression test. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
0ce1253fe1 |
feat(memory): Phase 1 memory tree - multi-source ingestion & canonical chunks (#707) (#732)
* feat(memory): phase 1 memory tree - multi-source ingestion & canonical chunks (#707) Adds an isolated memory tree layer under src/openhuman/memory/tree/ implementing Phase 1 of the new memory architecture (umbrella #711). Zero edits to existing memory/*.rs files - the new layer coexists with the legacy TinyHumans-backed client. - Source adapters: chat / email / document -> canonical Markdown - Token-bounded chunker with deterministic SHA-256 chunk IDs - SQLite persistence at <workspace>/memory_tree/chunks.db with full provenance metadata (source_kind, source_id, owner, timestamps, tags, time_range) and back-pointer to raw source - Unified JSON-RPC ingest (dispatches on source_kind + JSON payload): openhuman.memory_tree_ingest, _list_chunks, _get_chunk - DataSource enum covering the 8 providers from m.excalidraw step 1 (Discord/Telegram/Whatsapp/Gmail/OtherEmail/Notion/MeetingNotes/DriveDocs) - ~40 unit tests (chunk ID stability, UTF-8-safe splitting, canonicalisation idempotence, store round-trip, filter behavior) Additive only: new tables in a new DB file, new JSON-RPC namespace, no existing behavior changes. Feeds #708 (scoring), #709 (summary trees), #710 (query tools). Closes #707. Parent: #711. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(memory): enhance memory tree functionality and tests - Added support for "memory_tree" namespace description in `namespace_description`. - Implemented clamping for zero token budget in `chunk_markdown` to prevent empty leading chunks. - Introduced detailed logging for document ingestion, including title length. - Updated output schemas in `schemas.rs` for improved clarity. - Enhanced chunk listing with clamping limits and ordering by sequence in `list_chunks`. - Normalized source references in canonicalization functions to drop blank values. - Added comprehensive tests for new features and edge cases in chunking and canonicalization. These changes improve the robustness and usability of the memory tree layer, aligning with ongoing development efforts for Phase 1 of the memory architecture. * fix(memory): address follow-up CodeRabbit comments --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> |
||
|
|
8d4934e634 |
feat(agent): progressive-disclosure handoff + token-based summarizer threshold (#574) (#586)
* feat(agent): summarizer sub-agent compresses oversized tool results Issue #574. Before this change, tool results larger than a few KB landed verbatim in orchestrator history, burning context budget on raw JSON/HTML/file dumps. The only guardrail was tool_result_budget_bytes which hard-truncated mid-payload, dropping everything past the cut. This PR introduces a dedicated `summarizer` sub-agent (model.hint = "summarization") that the runtime automatically dispatches whenever a tool result exceeds summarizer_payload_threshold_bytes (default 100 KB). The summarizer compresses the payload per an extraction contract that preserves identifiers and key facts, and the compressed summary replaces the raw payload before it enters agent history. Payloads above summarizer_max_payload_bytes (default 5 MB) skip summarization and fall through to the existing truncation path — paying for an LLM call on a 5 MB blob is counterproductive. Scoped to the orchestrator session only. Welcome, skills_agent, researcher, planner, and every other typed sub-agent get None and their tool results are untouched. Gated in build_session_agent_inner by checking agent_id == "orchestrator". Instrumented at both tool-loop paths: - run_tool_call_loop in tool_loop.rs (event-bus/channels path) - Agent::execute_tool_call in session/turn.rs (web channel path via run_single) Both paths call into the same `PayloadSummarizer::maybe_summarize` trait so the threshold check, circuit breaker (3 consecutive failures disables for the session), and sub-agent dispatch policy live in exactly one place (agent/harness/payload_summarizer.rs). The summarizer sub-agent is runtime-dispatched only — it is NOT exposed as a delegation tool to the orchestrator's LLM. It is listed in the orchestrator's `subagents = [...]` for explicit registration, but `collect_orchestrator_tools` filters out the `summarizer` id and never synthesises a `delegate_summarizer` tool. Files: * NEW: src/openhuman/agent/agents/summarizer/{agent.toml,prompt.md} * NEW: src/openhuman/agent/harness/payload_summarizer.rs (trait + SubagentPayloadSummarizer impl + circuit breaker + tests) * src/openhuman/agent/agents/mod.rs — register summarizer in BUILTINS * src/openhuman/agent/agents/orchestrator/agent.toml — list summarizer in subagents (runtime-only, no LLM delegation tool) * src/openhuman/agent/harness/mod.rs — declare module * src/openhuman/agent/harness/builtin_definitions.rs — expect summarizer in `expected_builtin_ids_are_present` * src/openhuman/agent/harness/tool_loop.rs — thread Option<&dyn PayloadSummarizer> into run_tool_call_loop + agent_turn, intercept at the tool-execution success site, plus a new integration test using a MockSummarizer * src/openhuman/agent/harness/tests.rs — thread None through the existing run_tool_call_loop callsites * src/openhuman/agent/harness/session/turn.rs — same interception in Agent::execute_tool_call * src/openhuman/agent/harness/session/types.rs — payload_summarizer field on Agent and AgentBuilder * src/openhuman/agent/harness/session/builder.rs — .payload_summarizer() setter + orchestrator-only construction in build_session_agent_inner * src/openhuman/agent/bus.rs — pass None for the bus path * src/openhuman/config/schema/context.rs — summarizer_payload_threshold_bytes + summarizer_max_payload_bytes on ContextConfig * src/openhuman/tools/orchestrator_tools.rs — filter out the summarizer id from delegation-tool synthesis Tests: unit tests in payload_summarizer pin the pass-through rules (below threshold, above max cap, breaker tripped) and the prompt construction. Integration test in tool_loop::tests uses a MockSummarizer to verify the interception wires through end-to-end. Closes tinyhumansai/openhuman#574. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(tools): csv_export tool + skills_agent oversized output handling When a Composio tool returns a large payload inside skills_agent, the agent now has two prompt-driven paths: Path A — user wants an answer derived from the data: skills_agent extracts the answer in its next iteration and returns a targeted response (no file I/O needed). Path B — user wants the actual raw dataset: skills_agent calls csv_export (tabular data) or file_write (non-tabular) to persist the output to workspace/exports/, then returns a summary + file path. Changes: * NEW: src/openhuman/tools/impl/filesystem/csv_export.rs — CsvExportTool: parses JSON array, formats as CSV, writes to workspace/exports/{filename}. Handles missing keys (empty cells), nested values (JSON-serialised), and optional column ordering. Sandboxed via SecurityPolicy. * src/openhuman/tools/impl/filesystem/mod.rs — wire module * src/openhuman/tools/ops.rs — register CsvExportTool * src/openhuman/agent/harness/definition.rs — new extra_tools field on AgentDefinition, allowing named system tools to bypass category_filter * src/openhuman/agent/harness/subagent_runner.rs — inject extra_tools into allowed_indices after category filtering * src/openhuman/agent/agents/skills_agent/agent.toml — add file_write + csv_export via extra_tools * src/openhuman/agent/agents/skills_agent/prompt.md — new "Handling Oversized Tool Results" section with Path A (extract answer) vs Path B (export file) decision tree, intent-detection heuristics, and examples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(agent): progressive-disclosure handoff for skills_agent + token-based summarizer threshold (#574) Two layered fixes for #574 plus supporting changes. ## Summarizer thresholds in tokens, not bytes Renamed `ContextConfig::summarizer_payload_threshold_bytes` -> `summarizer_payload_threshold_tokens` (default 500 000, was 100 KB in bytes) and `summarizer_max_payload_bytes` -> `summarizer_max_payload_tokens` (default 2 000 000). Token count is estimated at ~4 chars/token via a local helper that mirrors `tree_summarizer::types::estimate_tokens`. Serde aliases on both fields keep existing config.toml files parsing. `payload_summarizer.rs` now compares token estimates for the threshold and cap, with both `tokens=` and `bytes=` surfaced in log events. ## Progressive-disclosure handoff for skills_agent `skills_agent` typed subagents (when spawned with a Composio toolkit) now receive a per-spawn `ResultHandoffCache` and a new built-in `extract_from_result` tool. When a per-action tool returns more than `HANDOFF_OVERSIZE_THRESHOLD_TOKENS` (20 000 tokens), the raw payload is stashed in the cache and a compact placeholder replaces it in history: [oversized tool output: 187432 bytes - stashed as result_id="res_1"] Preview (first 1500 chars): ... If the preview does not answer your task, call: extract_from_result(result_id="res_1", query="<specific question>") The subagent can answer from the preview, call `extract_from_result` with a targeted query, or re-call the original tool with tighter filters. `extract_from_result` dispatches the `summarizer` subagent against the cached payload with the query as the extraction contract. For payloads over `SUMMARIZER_CHUNK_CHAR_BUDGET` (60 000 chars) the extractor runs a chunked map-reduce: - split at blank-line / newline boundaries - map each chunk to a per-chunk partial via bounded concurrency (`buffer_unordered(3)`) to avoid storming the provider gateway - reduce partials in one more summarizer turn, or fall back to the concatenation if the reduce stage fails Falls back gracefully when the summarizer definition is missing from the registry (placeholder + preview still work; just no extractor). ## Text-mode dispatcher for skills_agent + tighter tool filter Provider-side grammar decoders (Fireworks etc.) cap tool-schema rules at 65 535 (uint16_t). Large Composio toolkits - Gmail, Notion, GitHub, Salesforce, HubSpot, Google Workspace - ship per-action JSON schemas dense enough that even a handful of them exceed the cap. - `subagent_runner::run_inner_loop` now detects `skills_agent` and omits `tools: [...]` from the API request, instead appending an `<tool_call>{...}</tool_call>` XML protocol block (with compact per-tool parameter summaries) into the system prompt. Tool calls are parsed out of the model's free-form response via the shared `parse_tool_calls` helper. Mirrors XmlToolDispatcher behaviour. - `top_k_for_toolkit()` picks 12 for HEAVY_SCHEMA_TOOLKITS (the six listed above plus googledocs, googlesheets, microsoftteams) vs the default 25 for lighter toolkits. ## Skills_agent agent.toml / prompt changes - Dropped `csv_export` from `skills_agent.extra_tools` (kept `file_write`) - the export tool was triggering superfluous file writes after extraction had already answered the query. - `builtin_definitions::skills_agent_has_extra_tools_for_export` test inverted to assert `csv_export` is NOT present. - `prompt.md` re-aligned: Path B now references only `file_write`. ## Wiring - `session/builder.rs`: threshold rename wiring (`_tokens`). - `subagent_runner.rs::run_inner_loop`: new `handoff_cache` parameter (threaded through both call sites; fork-mode passes `None`). ## Tests All green: - `payload_summarizer::tests`: 6/6 (thresholds in tokens) - `tool_loop::tests`: 10/10 (MockSummarizer integration) - `subagent_runner::tests`: 19/19 - `builtin_definitions::tests`: 5/5 (csv_export removal) - `csv_export::tests`: 7/7 (implementation kept, wiring removed) Closes #574. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(agent): drop reduce LLM call and pre-clean tool outputs in chunked extraction Two optimisations to the oversize-handoff / extract_from_result pipeline: 1. Concat chunk summaries in order; drop reduce stage. The reduce-stage summarizer call was the slowest single turn of the pipeline and a single point of failure when the upstream provider hung — a hung reduce burned minutes before timing out. Map summaries are now sorted by original chunk index and concatenated with a plain-separator join. For listing/extraction queries this is equivalent; for top-N / global-ordering queries the caller can post-process. Observed: 8m24s → ~70s on a Notion SEARCH run. 2. Generic cleaner before the oversize check. Strips <script>/<style>/<svg> blocks, HTML comments, data:...;base64,... inline URIs, remaining HTML tags, and collapses whitespace. Applied on every tool output so results that are mostly markup drop below the 20k-token threshold and skip the extract pipeline entirely; the ones that remain oversized get chunked on clean bytes. Debug log emits before/after bytes + saved_pct per call. No changes to the extract_from_result tool surface or placeholder format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): rearm 2-min silence timer on inference progress events The client-side safety timer in Conversations.tsx was a flat 120s deadline from the send, not a silence window. Long agent turns (chained tool calls, chunked extraction fan-outs, subagent spawns) would trip the timeout even though the backend was actively making progress — showing "No response from the assistant after 2 minutes" while the server was still working. The effect now watches inferenceStatusByThread and rearms the timer on every status change for the sending thread. Any tool_call / tool_result / iteration_start / subagent_{spawned,done} event resets the 120s window. When status is cleared (chat_done / chat_error), the timer is dropped. A truly silent server still fails after 120s as before. Tracks the sending thread id in a ref (sendingThreadIdRef) so switching threads mid-turn doesn't move the timer's reference point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a78506f6a3 |
fix(agent): use canonical assistant history format in subagent_runner (#606)
build_native_assistant_payload in subagent_runner.rs serialised the
assistant tool-call message using {"text": …, "tool_calls": [{
"type":"function","function":{…}}]} — two mismatches vs the format
parse::build_native_assistant_history produces and the backend
jinja template expects:
1. "text" instead of "content" — the downstream convert_messages
parser looks for "content" to detect assistant-with-tool-calls
messages; "text" is not recognised, so the message is treated
as a plain assistant message.
2. Nested {"type":"function","function":{"name":…,"arguments":…}}
wrappers instead of flat {"id":…,"name":…,"arguments":…}.
Result: every sub-agent that made a tool call and entered iteration
1+ hit a 400 from the backend: "jinja template rendering failed.
Message has tool role, but there was no previous assistant message
with a tool call!" — the backend saw a role=tool message without a
recognised assistant-with-tool-calls predecessor.
The fix replaces the broken inline copy with a direct call to
parse::build_native_assistant_history (the same serialiser
tool_loop.rs uses successfully for the orchestrator's tool calls).
The dead build_native_assistant_payload function is removed.
Introduced in PR #474 (
|
||
|
|
19aa50a429 |
fix(agent): thread agent_id into build_session_agent_inner (#584)
build_session_agent_inner took an agent_id: &str parameter but
never threaded it into the Agent::builder() chain — a `let _ =
agent_id;` silenced the unused-variable warning. AgentBuilder::build()
fell back to the legacy "main" default at builder.rs:310-312, so
every session built via Agent::from_config_for_agent carried
agent_definition_name="main" regardless of the id the caller asked
for.
Only two ids reach this path in production: "orchestrator" (legacy
Agent::from_config wrapper, cron, local_ai, escalation) and
"welcome" (channels/providers/web.rs routing after #525/#526, and
welcome_proactive). Orchestrator is benign — "main" was already
an alias for orchestrator everywhere downstream (see debug_dump.rs:249).
Welcome is the visible bug — agent_definition_name feeds three
surfaces that are all silently wrong pre-fix:
1. Transcript filename — welcome sessions land as main_*.md
instead of welcome_*.md.
2. Transcript metadata — the <!-- session_transcript --> block
stamps `agent: main` instead of `agent: welcome`.
3. Transcript resume cross-contamination —
try_load_session_transcript calls
find_latest_transcript(workspace_dir, "main") which scans all
dated session dirs for the latest main_*.md. It finds the
last orchestrator session and resumes from it, loading its
system prompt + user messages + assistant tool-call history
into the welcome agent's `history` as a resume prefix before
run_single runs. The written transcript mixes yesterday's
orchestrator email thread with today's welcome message under
the same main_0.md filename. Reproduced live 2026-04-16: a
welcome_proactive session loaded 5 messages from yesterday's
15042026/main_0.md and wrote 6 messages back that included a
spawn_subagent(skills_agent, toolkit=gmail) tool-call the
welcome agent never made.
Prompt rendering is NOT affected. context/prompt.rs only reads
ctx.agent_id for the is_skill_executor branch at prompt.rs:655, so
welcome/main/orchestrator all fall through the same delegator path.
The welcome persona prompt is rendered by the stamped
SystemPromptBuilder::for_subagent(target_def.system_prompt) built
at session-build time, independent of agent_definition_name. The
pre-fix main_0.md on disk contains `# Welcome Agent\n\nYou are the
Welcome agent...` as its system prompt body — correct content,
wrong filename and metadata.
skills_agent and other typed sub-agents are unaffected — they go
through subagent_runner, which constructs its prompt directly and
writes transcripts under the explicit id it receives. The
pre-existing sessions/15042026/skills_agent_0.md on disk with
`agent: skills_agent` confirms this code path has always been
correct.
Changes:
* src/openhuman/agent/harness/session/builder.rs:
- Add .agent_definition_name(agent_id.to_string()) to the
builder chain in build_session_agent_inner.
- Delete the `let _ = agent_id;` suppression.
- Replace the misleading 8-line comment block at the call site
(which claimed event_channel was for transport identity and
therefore stamping agent_id wasn't needed — conflating two
unrelated fields) with an accurate description of the three
load-bearing surfaces.
- Expand the docstring on AgentBuilder::agent_definition_name
to document the surfaces and the latent prompt-section
foot-gun the fix closes for future code.
- New log::debug! call at the stamping site for grep-friendly
runtime traces ([agent::builder] stamping
agent_definition_name=<id> onto session agent).
* src/openhuman/agent/harness/session/runtime.rs:
- Add pub fn agent_definition_name(&self) -> &str accessor
on Agent so tests and runtime callers can introspect the
stamped id without reaching into the pub(super) field.
* src/openhuman/agent/harness/session/tests.rs:
- Add build_minimal_agent_with_definition_name helper.
- agent_builder_threads_agent_definition_name_when_set —
parameterised over welcome/skills_agent/orchestrator/
trigger_triage, asserts the setter threads the id through.
- agent_builder_falls_back_to_main_when_definition_name_unset
— pins the legacy fallback contract direct builder users rely
on.
Tests: 2 passed; 0 failed; 3477 filtered out; finished in 0.21s.
cargo check --lib clean; cargo fmt clean.
Verified live against G:/projects/vezures/.openhuman on 2026-04-16:
- Pre-fix welcome_proactive run wrote sessions/16042026/main_0.md
with `agent: main` in the metadata header.
- Post-fix welcome_proactive run wrote sessions/16042026/welcome_0.md
with `agent: welcome` in the metadata header.
- Post-fix web-chat dispatch to orchestrator wrote
sessions/16042026/orchestrator_0.md (moved off the historical
main_*.md alias — behaviorally unchanged for orchestrator).
- The new [agent::builder] stamping debug line fires on both
welcome and orchestrator paths.
Does not touch: subagent_runner, spawn_subagent / dispatch_subagent,
any agent/agents/*/agent.toml, any context::prompt code, or the
AgentBuilder::build() fallback itself.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
d545c193b9 |
feat(agent): fuzzy-filter skills_agent toolkit actions by task prompt (#579)
* feat(agent): fuzzy-filter skills_agent toolkit actions by task prompt Narrow large Composio toolkits (e.g. github ~500 actions) down to the handful relevant to a given delegation prompt before registering them as native tools on a spawned skills_agent. Falls back to the full catalogue when the filter yields fewer than MIN_CONFIDENT_HITS hits to avoid starving the sub-agent on under-specified prompts. Filter is only invoked when both `definition.id == "skills_agent"` and a `toolkit=` argument is present, so orchestrator and other sub-agents are unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(composio): mock toolkits route in ops integration test --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ea088d9f15 |
feat(prompt): unified delegation guide + dynamic per-toolkit tool registration (#447) (#570)
* feat(prompt): unified delegation guide + dynamic per-toolkit tool registration (#447) Cuts main agent prompt token cost ~98% on accounts with connected integrations and replaces the generic Composio dispatcher with native per-action tool calling inside skills_agent. ## Why Issue #447: prompt payloads were ballooning because main/orchestrator re-shipped a full prose enumeration of every connected toolkit's actions on every turn (~13.7k tokens for one gmail integration alone), and skills_agent had no way to scope tools to a single toolkit so it inherited a flat dispatcher (composio_execute) that the LLM couldn't reliably call without first round-tripping composio_list_tools. ## What ### main / orchestrator prompt - Replace `ConnectedIntegrationsSection`'s per-action prose dump with a unified Delegation Guide: one bullet per integration (toolkit + one-line description + spawn snippet). Tokens added per integration: ~30. Savings vs old per-action listing: ~5k+ tokens per connected toolkit. - Drop every "Composio" reference from delegating-agent prose so the surface stays provider-neutral. ### skills_agent - Add a `toolkit` argument to `spawn_subagent` with three pre-flight validation modes resolved against the parent's connected integrations list before any LLM call: * missing toolkit -> ToolResult::error (model retries) * unknown toolkit -> ToolResult::error (model retries) * in allowlist, not connected -> ToolResult::success (model surfaces "authorize in Settings -> Integrations" to user; not flagged as a tool failure in the agent loop or web channel) - New `ComposioActionTool` (`src/openhuman/composio/action_tool.rs`) implements the `Tool` trait per Composio action. The sub-agent runner constructs ~N of these at spawn time from the cached integration overview and injects them via a new `extra_tools` parameter through `run_typed_mode` -> `run_inner_loop`. - `filter_tool_indices` drops every skill-category parent tool when `is_skills_agent_with_toolkit` is true so the only skill-category entries the sub-agent sees are the freshly-built per-action tools. This eliminates apify_*, composio_list_*, composio_authorize, and composio_execute from the toolkit-scoped surface. - Sub-agent renderer takes the parent's actual `tool_call_format` instead of hardcoding PFormat. Native dispatchers no longer carry a prose `## Tools` section at all (schemas already travel through the request body's `tools` field) — eliminates a ~30k-token duplication that was blowing past the model's context window. ### integration overview fetch - `fetch_connected_integrations_uncached` now merges Composio's toolkit allowlist with the user's active connections and returns one `ConnectedIntegration` per allowlisted toolkit with a `connected: bool` flag. Unconnected entries carry no schemas, just the toolkit name + description, so the orchestrator can mention them without trying to invoke them. - `ConnectedIntegrationTool` preserves the action's full JSON parameter schema so `ComposioActionTool` can advertise it through native function-calling. ### plumbing - `ParentExecutionContext` carries a `ComposioClient` and the parent's resolved `tool_call_format`, populated alongside `connected_integrations` in `Session::fetch_connected_integrations`. Triage and test paths pass `None` / `PFormat` defaults. - `dispatch_subagent` (the `SkillDelegationTool` path) plumbs its pre-bound skill_id through `toolkit_override` instead of the broken `skill_filter_override` (which used `{skill}__` prefix matching that never matched Composio's `TOOLKIT_*` naming). - `web::run_chat_task` failures now log the underlying error at WARN so debugging an in-flight failure no longer requires turning on TRACE for socket events. - `scripts/stage-core-sidecar.mjs` queries `cargo metadata` for the real target directory instead of assuming `<repo>/target` so the staging step works under workspace-level `target-dir` overrides (the vezures-workspace shared `.cargo-target` setup). ## Verified end-to-end Two RPC tests against a freshly-rebuilt sidecar (gmail connected, notion / slack / etc. allowlisted but not connected): | Test | Behavior | Tokens | Iterations | |---|---|---|---| | Connected (gmail, "fetch 5 unread emails") | spawn_subagent -> 62 dynamic gmail tools registered -> 1 GMAIL_FETCH_EMAILS call with smart args -> markdown table response | 42,911 input | 1 | | Not-connected (notion, "create a page") | main answers directly without spawning, tells user to authorize in Settings -> Integrations | 3,319 input | 1 | Both flows complete cleanly. The connected path proves dynamic per-action tool registration is working with full schema validation; the unconnected path proves the unified delegation guide + ToolResult::success return for not-connected toolkits keeps the model on a graceful path without polluting the chat with error styling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(prompt): update test callers + assertions for new renderer signature Follow-up to #447. The main patch changed three signatures that test callers hadn't been updated for, and flipped one assertion that was validating the now-removed prose schema duplication. - `SubagentRunOptions` test constructors in subagent_runner.rs (2 sites) now pass `toolkit_override: None`. - `ConnectedIntegration` test constructor in orchestrator_tools.rs now passes `connected: true` (the default for test integrations — they're treated as authorized so delegation logic still runs). - 12 `render_subagent_system_prompt` test callers in prompt.rs now pass `&[]` for the new `extra_tools` slice and `ToolCallFormat::PFormat` for the new `tool_call_format` argument. - `render_subagent_system_prompt_honors_identity_safety_and_skills_flags` used to assert `rendered.contains("Parameters:")` on the Json dispatcher branch — that was valid in the old world where the prose `## Tools` section dumped full JSON schemas for Json/Native formats. The main patch deliberately removes that dump (it was the ~30k-token duplication of the native `tools` field), so the test now asserts the opposite: no `## Tools` header and no `Parameters:` line are emitted for Native/Json dispatchers. The schemas still travel through the provider request's `tools` field. Also picks up `rustfmt` rewraps in action_tool.rs and ops.rs from a background linter run — pure whitespace, no semantic change. Verified green against the full `cargo test --lib` suite for every test touched by this PR: - openhuman::context::prompt::tests (26 passed) - openhuman::agent::harness::subagent_runner::tests (19 passed) - openhuman::composio::ops::tests (2 passed) - openhuman::tools::impl::agent::tests (0 scoped) The 7 remaining failures in `cargo test --lib` are pre-existing Windows-path/filesystem flakes in subsystems this PR doesn't touch (self_healing polyfill path separator, cron scheduler shell spawning, local_ai::paths absolute-path detection, security::policy sandbox path handling, composio::trigger_history jsonl archive, and a real pre-existing `Option::unwrap()` panic in browser::screenshot). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(harness): add composio_client + tool_call_format to integration test stub Follow-up to #447. The `tests/agent_harness_public.rs` integration test constructs a `ParentExecutionContext` in `stub_parent_context` that was missing the two fields added in the main patch (`composio_client` and `tool_call_format`). `cargo test --lib` doesn't compile integration tests, which is why this slipped past local verification — it only surfaced on Linux CI. - `composio_client: None` — the stub parent has no composio client because these tests don't exercise the integration-overview path. - `tool_call_format: ToolCallFormat::PFormat` — default legacy format; none of the tests in this file exercise the sub-agent renderer's format branching, so PFormat is the safe pick. Verified locally that `cargo test --no-run` compiles all integration test targets including `agent_harness_public`. (The `agent_memory_loader_public` link error in the local run is a pre-existing Windows `libucrt`/`fgets` C-runtime issue unrelated to this PR — won't reproduce on Linux CI.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(prompt,composio): address CodeRabbit review on #570 (#447) Four fixes from the CodeRabbit review of PR #570, ranked by impact: ## `context/prompt.rs` — Json dispatcher needs prose tool catalog The initial patch gated the prose `## Tools` section behind `matches!(tool_call_format, ToolCallFormat::PFormat)`, which conflated Json with Native. Only `ToolCallFormat::Native` uses the provider's native function-calling channel where schemas travel via the request body's `tools` field. `ToolCallFormat::Json` is a prompt-driven format (the model wraps JSON tool calls in `<tool_call>` tags) and relies on the prose catalog the same way PFormat does — without it the model sees protocol instructions but no visible tool names or schemas. Inverted the guard to `!matches!(tool_call_format, Native)` so PFormat and Json both render the `## Tools` section with their respective per-entry shapes (compact `Call as:` signature for PFormat, inline `Parameters:` JSON schema for Json). Updated the `render_subagent_system_prompt_honors_identity_safety_and_skills_flags` test: the Json assertion now expects `Parameters:` to be present again (reverting commit 2's flip), and a new Native-branch assertion guards against the ~54k-token schema duplication the original PR fixed. ## `composio/ops.rs` — don't cache degraded snapshots All three backend calls in `fetch_connected_integrations_uncached` (`list_toolkits`, `list_connections`, `list_tools`) previously returned `Some(Vec::new())` or fell through to an empty inner vec on transient errors. The outer `fetch_connected_integrations` caches whatever the uncached path returns, so a single transient 5xx would silently hide every integration, mark connected toolkits as disconnected, or register dynamic Composio tools with zero callable actions — until the cache is invalidated or the process restarts. Changed all three branches to return `None`, which signals the caller to NOT cache the result and retry on the next call. ## `composio/ops.rs` — prefix match needs a delimiter `starts_with(&slug.to_uppercase())` false-matches when two toolkit slugs share a text prefix (e.g. `git` vs `github`). The current allowlist doesn't trigger this, but adding a new toolkit could silently leak actions between integrations. Anchored the prefix with an underscore so `GMAIL_SEND_EMAIL` matches `gmail_`, not just `gmail`. ## `scripts/stage-core-sidecar.mjs` — resolve CARGO_TARGET_DIR vs repo root `resolve(process.env.CARGO_TARGET_DIR)` uses the process's current working directory, but the `cargo build` spawn below runs with `cwd: root`. For an absolute env var this is identical, but a relative `CARGO_TARGET_DIR` and a cwd outside the repo would make the two paths disagree and the binary lookup would miss. Defensive fix: `resolve(root, process.env.CARGO_TARGET_DIR)` so both paths anchor to the same base. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7685e877ee |
feat(agent): welcome->orchestrator routing + per-agent tool scoping (#525, #526) (#544)
* feat(agent): add subagents + delegate_name fields to AgentDefinition (#525, #526) Introduces the schema change needed to make agent definitions the single source of truth for both direct tools and delegation targets: - `subagents: Vec<SubagentEntry>` — declarative list of agents this agent can spawn, expanding at build time into synthesised delegate_* tools on the LLM's function-calling surface. Supports two TOML shapes via `#[serde(untagged)]`: * Bare string (`"researcher"`) → `SubagentEntry::AgentId` * Inline table (`{ skills = "*" }`) → `SubagentEntry::Skills(SkillsWildcard)` The `Skills` variant expands dynamically to one delegate_{toolkit} tool per connected Composio toolkit at runtime. - `delegate_name: Option<String>` — optional override for the tool name this agent is exposed as when another agent lists it in `subagents`. Defaults to `delegate_{id}` when absent, lets the researcher agent be exposed as `research`, code_executor as `run_code`, etc. Schema only — no runtime behavior change yet. Follow-up commits wire the field into `collect_orchestrator_tools`, the dispatch path, and the debug dump. TOML placement note: `subagents = [...]` must appear before the `[tools]` table header in agent TOMLs. Once a table section opens, every subsequent top-level key is consumed by that table, so placing `subagents` after `[tools]` parses it as `tools.subagents` and fails deserializing ToolScope. The test doc-comment records this constraint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(agent): drive orchestrator delegation tools from TOML subagents (#525, #526) Completes the half-built migration from a hardcoded ARCHETYPE_TOOLS table + orphan skill_delegation.rs to a TOML-driven delegation surface where each agent declares its subagents and the tool list is synthesised from the registry at agent-build time. Rewrites `collect_orchestrator_tools` to take the orchestrator's AgentDefinition, the global AgentDefinitionRegistry, and the slice of connected Composio integrations, then iterates the definition's `subagents` field: * `SubagentEntry::AgentId` → one ArchetypeDelegationTool. The tool's `name()` comes from the target agent's `delegate_name` override (or `delegate_{id}` fallback) and its `description()` is the target's `when_to_use`. Editing an agent's TOML when_to_use now immediately updates the tool schema the orchestrator LLM sees — zero drift, no hardcoded description strings to keep in sync. * `SubagentEntry::Skills { skills = "*" }` → one SkillDelegationTool per connected Composio toolkit. Each routes to the generic skills_agent with `skill_filter` pre-populated. Empty integrations list (CLI path, or user not yet connected) produces zero tools rather than phantom `delegate_*` entries for unconnected toolkits. Also in this commit: - Wire the orphan `skill_delegation.rs` into `impl/agent/mod.rs` via `mod skill_delegation;` + `pub use SkillDelegationTool;`. The file has existed since earlier work but was never declared in the module tree, so it compiled as dead code. - Delete the legacy `MAIN_AGENT_TOOL_ALLOWLIST` constant and the `main_agent_tools` filter in `tools/ops.rs`. They were documented as "no longer the primary source of truth" since the from_config builder switched to `collect_orchestrator_tools`, and grep confirms no external callers remain. Clean deletion. - Delete the hardcoded `ARCHETYPE_TOOLS` const in `tools/impl/agent/mod.rs`. The 4-entry table has been replaced by the orchestrator TOML's `subagents` list (which covers those 4 plus archivist plus the skills wildcard), and the re-export in `tools/mod.rs` is removed accordingly. - Update `agents/orchestrator/agent.toml`: add the `subagents` field listing researcher / planner / code_executor / critic / archivist / { skills = "*" }. Keep `spawn_subagent` in `[tools] named` as an advanced fallback so power users can still spawn custom workspace- override agent ids that aren't in the declarative subagents list. - Add `delegate_name = "..."` to the 5 archetype TOMLs so the orchestrator LLM sees natural tool names (`research`, `plan`, `run_code`, `review_code`, `archive_session`) rather than the `delegate_<agent_id>` fallback. - Update `agent/harness/session/builder.rs` (line ~461) to call the new `collect_orchestrator_tools` signature. Looks up the orchestrator definition from the global registry; passes an empty integrations slice because the builder is synchronous and cannot await Composio's async fetch. The channel-dispatch path will populate integrations in a later commit — the CLI/REPL path ships without per-toolkit delegation tools, which is acceptable regression since CLI users still reach Composio via `composio_execute` and the retained `spawn_subagent` fallback. Tests: * 5 new unit tests in `orchestrator_tools.rs` cover the baseline AgentId + Skills wildcard expansion, empty-integrations edge case, unknown-id graceful skip, non-delegating agent with empty subagents, and the slug sanitiser for tool-name-safe Composio toolkit names. * Runs clean alongside all existing agent-module tests (323 pass; one pre-existing Windows-path failure in `self_healing::tests:: tool_maker_prompt_includes_command` is unrelated to this PR and fails identically on the upstream baseline). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(agent): plumb per-agent tool scoping through bus + tool loop (#525, #526) Adds the parameter plumbing for agent-aware tool filtering without changing any runtime behaviour. Every existing call site continues to pass `None` / empty extras, so the LLM still sees the full unfiltered registry — the actual routing logic that populates these fields lands in commit 4b (dispatch.rs onboarding-flag → target_agent_id). Why two parameters and not one filter: Tools in this codebase are `Box<dyn Tool>` — owned trait objects with no Clone impl, stored in a shared `Arc<Vec<Box<dyn Tool>>>`. We can't cheaply build a per-turn filtered subset of the global registry, and we can't mutate the Arc to remove entries. Two new parameters work around this without touching the global registry's lifetime model: * `visible_tool_names: Option<&HashSet<String>>` — whitelist filter applied at the iteration site inside `run_tool_call_loop`. When `Some(set)`, only tools whose `name()` is in the set contribute to the function-calling schema and are eligible for execution; every other tool in the combined registry is hidden from the model and rejected if the model emits a call for it. `None` preserves the legacy "everything visible" behaviour. * `extra_tools: &[Box<dyn Tool>]` — per-turn synthesised tools spliced alongside `tools_registry`. The dispatch path will use this to surface delegation tools (`research`, `delegate_gmail`, …) that are built fresh each turn from the active agent's `subagents` field and the current Composio integration list — tools that don't exist in the global startup-time registry because they depend on per-user runtime state. Empty slice for agents that don't delegate. Inside the loop, `tool_specs` is built from `tools_registry.iter().chain(extra_tools.iter()).filter(is_visible)`, and the tool-execution lookup uses the same chain + filter so the function-calling schema and the execution surface stay in sync. Files touched in this commit: src/openhuman/agent/harness/tool_loop.rs - Add `visible_tool_names` and `extra_tools` parameters to `run_tool_call_loop`. Build `tool_specs` from chained iteration with the visibility filter applied. Replace the `find_tool` call at the execution site with an inline chain+filter lookup so hallucinated calls to filtered-out tools surface as "unknown tool" errors. Drop the now-unused `find_tool` import. - Update the legacy `agent_turn` wrapper to pass `None, &[]`, preserving its existing unfiltered behaviour. - Update all 9 in-file test sites to pass `None, &[]`. src/openhuman/agent/harness/tests.rs - Update all 3 `run_tool_call_loop` test sites to pass `None, &[]`. src/openhuman/agent/bus.rs - Add `target_agent_id: Option<String>`, `visible_tool_names: Option<HashSet<String>>`, and `extra_tools: Vec<Box<dyn Tool>>` fields to `AgentTurnRequest`, with rustdoc explaining each. - Destructure the new fields in the `agent.run_turn` handler; thread `visible_tool_names.as_ref()` and `&extra_tools` through to `run_tool_call_loop`. Augment the dispatch trace with target_agent / extra_tool_count / visible_tool_count / filter_active so production logs show whether scoping is active. - Update the in-test `test_request()` helper to populate the new fields with safe defaults. src/openhuman/agent/triage/evaluator.rs - Update the triage `AgentTurnRequest` initializer to set `target_agent_id = Some("trigger_triage")` (for tracing) with `visible_tool_names: None` + `extra_tools: Vec::new()` because the classifier intentionally runs against an empty registry and emits a structured JSON decision rather than calling tools. src/openhuman/channels/runtime/dispatch.rs - Update the channel-message `AgentTurnRequest` initializer to set the three new fields to safe defaults (`None` / `None` / empty vec). Commit 4b will replace these with the real onboarding-flag based routing. src/openhuman/tools/impl/agent/mod.rs - Bug fix: `dispatch_subagent` previously took `_skill_filter: Option<&str>` but discarded the value, hardcoding `SubagentRunOptions::skill_filter_override = None`. That meant `SkillDelegationTool::execute()` synthesising `dispatch_subagent("skills_agent", ..., Some("gmail"))` never actually narrowed `skills_agent`'s tool list — so even with the orchestrator's view scoped, the spawned `skills_agent` subagent would still see the full Composio catalog. Drop the underscore, propagate `skill_filter` into `skill_filter_override`, and add a tracing log line to make this path observable. This is the downstream half of the #526 leak that commit 3's orchestrator- side scoping alone wouldn't have caught. Tests: 8/8 `tool_loop` tests pass, 3/3 harness `tests.rs` cases pass, 323/324 agent module tests pass overall (the one failure is the same pre-existing Windows-path bug in `self_healing::tool_maker_prompt_ includes_command` that fails identically on the upstream baseline). No existing test expectations were changed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(dispatch): route channel messages to welcome/orchestrator by onboarding flag (#525) Wires the per-agent tool scoping plumbing from commit 4a into the channel-message dispatch path. Each incoming channel message now picks the active agent — `welcome` pre-onboarding, `orchestrator` post — based on `Config::onboarding_completed`, loads the matching definition from the global `AgentDefinitionRegistry`, synthesises any `delegate_*` tools the agent declares in its `subagents` field, and passes everything through to `agent.run_turn` on the bus. This is the half of #525 that makes the welcome agent actually run for new users — the welcome definition has existed since upstream PR #522 but had no caller; nothing in dispatch consulted the onboarding flag, so every channel message ran through the same generic tool loop with the full registry exposed. Changes: src/openhuman/channels/runtime/dispatch.rs - New `AgentScoping` struct carrying the three new `AgentTurnRequest` fields (`target_agent_id`, `visible_tool_names`, `extra_tools`) plus an `unscoped()` constructor for safe-fallback paths. - New async `resolve_target_agent(channel)` helper: * fresh `Config::load_or_init().await` per turn (no cache — the loader reads from disk every call, verified at `config/schema/load.rs:409`, so the welcome→orchestrator handoff is observed on the next message after `complete_onboarding(complete)` flips the flag, with no need for an explicit handoff event); * picks `"welcome"` or `"orchestrator"` based on the flag and emits a structured `[dispatch::routing] selected target agent` info trace recording the choice + the flag value, satisfying the #525 acceptance criterion `"agent-selection logs clearly record why each agent was selected at onboarding boundaries"`; * looks up the definition in `AgentDefinitionRegistry::global()`, gracefully falling back to `AgentScoping::unscoped()` (= legacy behaviour, no filter, no extras) if the registry isn't initialised or the definition isn't found, so a routing miss never fails the user message; * for agents with a non-empty `subagents` field, awaits `composio::fetch_connected_integrations(&config)` and runs `orchestrator_tools::collect_orchestrator_tools` to materialise per-turn delegation tools (`research`, `plan`, `delegate_gmail`, …). Agents with empty `subagents` get an empty extras vec. - New `build_visible_tool_set(definition, &extra_tools)` helper that returns `Some(union)` for `ToolScope::Named` agents (their named list ∪ the names of the synthesised delegation tools) and `None` for `ToolScope::Wildcard` agents to preserve the unfiltered semantics — so agents like `skills_agent` and `morning_briefing` that already work via `wildcard + category_filter` keep their existing behaviour without this layer interfering. - `process_channel_message` calls `resolve_target_agent` once per turn, drops the placeholder defaults from commit 4a, and feeds the real `target_agent_id`/`visible_tool_names`/`extra_tools` into `AgentTurnRequest`. - New imports: `AgentDefinition`, `AgentDefinitionRegistry`, `ToolScope`, `Config`, `fetch_connected_integrations`, `orchestrator_tools`, `Tool`, `HashSet`. End-to-end behaviour after this commit: 1. New user, `onboarding_completed=false`: dispatch picks `welcome`, loads its 2-tool TOML scope, builds `visible_tool_names = {complete_onboarding, memory_recall}`, no extras, hands off to the bus. Bus handler applies the filter → welcome's LLM sees exactly 2 tools. 2. Welcome agent guides the user through setup, eventually calls `complete_onboarding(action="complete")` → flag persists to disk via `config.save()`. 3. Next user message: dispatch reads the flag fresh, picks `orchestrator`, fetches connected Composio integrations, expands `subagents = ["researcher", "planner", "code_executor", "critic", "archivist", { skills = "*" }]` into delegate_research / delegate_plan / delegate_run_code / delegate_review_code / delegate_archive_session + one delegate_<toolkit> per connected integration. visible_tool_names is the union with the 4 direct tools from orchestrator's `[tools] named` list. LLM sees the scoped delegation surface, not the full 1000+ Composio catalog. #526's runtime leak is now fixed end-to-end: the orchestrator's LLM prompt only contains the tools its TOML allows, and the SkillDelegationTool path narrows skills_agent to a single toolkit via the `skill_filter` propagation fix from commit 4a. No agent at any layer sees more than its definition declares. Tests: 599/599 channel module tests pass — including `runtime_dispatch::dispatch_routes_through_agent_run_turn_bus_handler` and the telegram integration variant, which exercise the full bus roundtrip with the new fields populated. No existing assertions were modified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(debug_dump): apply orchestrator definition filter to main dump (#526) Replaces the explicit `empty_filter: HashSet::new()` in `render_main_agent_dump` with a real visibility whitelist derived from the orchestrator's `AgentDefinition`. The "main" dump path now mirrors the runtime dispatch path from commits 4a/4b — same definition, same delegation tool synthesis, same filter — so `openhuman agent dump-prompt main` shows what the LLM actually sees in production instead of the unfiltered global registry. Before this commit, `dump-prompt main` always rendered every tool in the registry regardless of which agent was supposed to run, which is exactly the symptom #526 reports: "agent prompt tool scopes leak full GitHub tool catalog". The runtime fix in commit 4b stops the leak in production but the dump path was the user's primary observability tool for inspecting prompts, so leaving it unscoped would mask future regressions and confuse debug sessions. Changes in `src/openhuman/context/debug_dump.rs`: * `dump_agent_prompt` now loads `AgentDefinitionRegistry` once at the top (previously only loaded inside the sub-agent branch). When the request is for "main" or "orchestrator_main", it looks up the "orchestrator" definition from the registry and passes it into `render_main_agent_dump` along with the registry itself. Missing orchestrator entry → structured error listing known agents instead of silently rendering an unfiltered prompt. * `render_main_agent_dump` signature gains `registry: &AgentDefinitionRegistry` and `orchestrator_def: &AgentDefinition`. Inside, it: - calls `collect_orchestrator_tools(orchestrator_def, registry, connected_integrations)` to synthesise the same per-turn delegation tools (`research`, `plan`, `delegate_<toolkit>`, …) that dispatch generates; - extends `prompt_tools` with the synthesised extras so they contribute to the rendered tool catalogue; - builds `visible_filter: HashSet<String>` from `orchestrator_def.tools` (the `[tools] named` list) ∪ the names of the synthesised extras, falling back to an empty HashSet when the orchestrator definition uses `ToolScope::Wildcard` (which the prompt builder treats as "no filter, every tool visible") so dump consumers that supply a wildcard orchestrator (custom workspace overrides, tests) retain the legacy unscoped behaviour; - replaces `visible_tool_names: &empty_filter` in the `PromptContext` with `&visible_filter`; - filters the returned `tool_names` and `skill_tool_count` by the same predicate so the `DumpedPrompt` summary fields match what the prompt text actually contains. Tests: * Replaces the previous `render_main_agent_dump_includes_tool_ instructions_and_skill_count` test with two more focused cases: 1. `render_main_agent_dump_wildcard_scope_shows_full_tool_set` — regression guard for the legacy wildcard path. Builds a wildcard-scoped orchestrator definition, asserts every tool from `tools_vec` survives, and checks the standard system- prompt skeleton (Tools section, Tool Use Protocol, cache boundary) still renders. 2. `render_main_agent_dump_named_scope_filters_to_whitelist` — the #526 regression guard. Builds an orchestrator with `ToolScope::Named(["query_memory", "ask_user_clarification"])` and a `tools_vec` containing `shell`, `query_memory`, and `GMAIL_SEND_EMAIL`. Asserts the dump's `tool_names` is exactly `["query_memory"]` — `shell` and `GMAIL_SEND_EMAIL` are in the global registry but NOT in the whitelist, so they MUST be excluded. If a future change reintroduces the unfiltered behaviour this test fails immediately. * Adds two test helpers: `wildcard_orchestrator_def()` builds a minimal orchestrator definition with all `omit_*` flags set and `ToolScope::Wildcard`, and `registry_with_orchestrator(orch)` wraps it in an `AgentDefinitionRegistry` so the tests can call `render_main_agent_dump` without going through the full TOML loader. 11/11 debug_dump tests pass. The two new guards plus the 9 existing sub-agent / filter / composio-stub tests all run clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(dispatch): unit tests for build_visible_tool_set + cargo fmt cleanup (#525, #526) Adds focused unit tests for the per-agent scoping helper landed in commit 4b and runs `cargo fmt` across the files touched by this PR. Why scoping unit tests, not full integration tests: `resolve_target_agent` is async and reads `Config::load_or_init().await` which does a real disk read every call (no cache, verified at `config/schema/load.rs:409`). Mocking that requires either spinning up a full workspace under a temp dir with a config.toml containing the right `onboarding_completed` value, or adding a test-only injection point on the public Config API. Both are tractable but invasive enough to belong in their own follow-up PR. The end-to-end dispatch path is already covered by the existing channel integration tests (`dispatch_routes_through_agent_run_turn_bus_handler` etc.) which exercise the full bus roundtrip with the new fields populated, and which still pass after the new resolver landed (it gracefully falls back to `AgentScoping::unscoped()` when no orchestrator definition is registered in the test environment). Pure-function unit tests for `build_visible_tool_set` cover the branching logic that does the actual scoping work: how the named whitelist + extras union is built, how Wildcard scope is preserved, how duplicates are de-duplicated, etc. That's the part most likely to drift in future changes, so it's the part most worth fencing with focused tests. Tests added (all in `src/openhuman/channels/runtime/dispatch.rs` under the new `scoping_tests` module): * `wildcard_scope_yields_none_filter` — `ToolScope::Wildcard` must produce `None` regardless of whether extras are present, so skills_agent / morning_briefing keep their full skill-category catalogue. * `named_scope_without_extras_returns_named_only` — the welcome agent's path: 2 named tools, no delegation, exactly 2 entries in the visibility whitelist. * `named_scope_with_extras_returns_union` — the orchestrator's path: 3 direct named tools + 3 synthesised extras (research, delegate_gmail, delegate_github) → 6 entries. * `empty_named_with_extras_returns_extras_only` — guards a future "delegation-only" agent layout where the agent has no direct tools of its own, just spawns subagents. * `empty_named_with_no_extras_returns_empty_set` — guards the distinction between `None` (no filter, all visible) and `Some(empty)` (filter active, nothing matches). Important because the prompt loop's `is_visible` check treats them differently. * `duplicate_names_across_named_and_extras_are_deduplicated` — the HashSet handles collisions automatically, but the test pins that behaviour so a future migration to `Vec<String>` (which would silently double-count) gets caught. * `agent_scoping_unscoped_has_no_filter_or_extras` — pins the safe-fallback constructor's contract. Used when the registry is uninitialised or the target agent is missing — every field must default to "no scoping" so the channel turn falls back to legacy unfiltered behaviour rather than crashing. Plus `cargo fmt` run across the 6 files modified by this PR. No behavioural changes. Final test status across all commits 2-6 in this PR: * agent::harness::definition: 10/10 ✅ (4 new for Subagents schema) * agent::harness::tool_loop: 8/8 ✅ * agent::harness::tests: 3/3 ✅ * tools::orchestrator_tools: 5/5 ✅ (5 new) * channels::*: 599/599 ✅ (incl. dispatch integration) * channels::runtime::dispatch::scoping_tests: 7/7 ✅ (7 new) * context::debug_dump: 11/11 ✅ (1 replaced + 1 new) * Total agent module: 323/324 (one pre-existing Windows path failure in `self_healing::tool_maker_prompt_includes_command` confirmed identical against upstream/main baseline) Pre-existing Windows-environment test failures NOT caused by this PR and out of scope (all confirmed identical on upstream baseline; CI on Linux is unaffected): * self_healing::tool_maker_prompt_includes_command (PathBuf separator) * cron::scheduler::run_job_command_success / _failure (Unix shell) * composio::trigger_history::archives_triggers_in_daily_jsonl... (path) * local_ai::paths::target_paths_preserve_absolute_overrides (path) * security::policy::checklist_root_path_blocked (POSIX absolute) * security::policy::checklist_workspace_only_blocks_all_absolute (POSIX) * tools::implementations::browser::screenshot::screenshot_command_ contains_output_path (browser binary lookup) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(welcome): upgrade bare-install nudge with concrete integration pitches (#525) Follow-up to #522 that addresses a rough UX edge in the welcome agent prompt: users who arrive with only an API key configured (no channels, no Composio integrations, no web search / browser / local AI) were getting a "gentle suggestion" to connect something without any concrete picture of what they'd actually unlock. The welcome agent would finish onboarding, hand off to orchestrator, and leave the user staring at a functional-but-empty assistant with no roadmap for how to make it useful. This commit rewrites the prompt to handle sparse setups explicitly. No Rust changes — this is prompt.md only, picked up at build time via the existing `include_str!` loader in `agents/mod.rs`. Changes to `src/openhuman/agent/agents/welcome/prompt.md`: * Step 2's "point out what's missing" sub-point is rewritten from a single "gently suggest" line into a four-case decision tree keyed off `check_status` state: - No API key → critical, block completion. - Integrations yes, channels no → note the Tauri-only reach limitation, suggest a messaging platform. - Channels yes, integrations no → degraded assistant, nudge toward Composio. - Nothing beyond the API key → the "bare install" case, gets the new Step 2.5 treatment. * New **Step 2.5: Handling a bare install** section added after Step 2. Spells out what the user DOES have (sandboxed reasoning + coding assistant with memory), what they're MISSING (any external action), and how to structure the message: state the current capability honestly, pitch 2-3 specific integrations with concrete example prompts, point to Settings → Integrations / Channels, and leave room for the user to opt into the coding-only experience if that's what they actually want. For bare-install users the word budget stretches to 250-400 words (up from 200-350) so the concrete pitches and example prompts actually fit without cramming. * New **Integration capability reference** section giving the LLM a menu it can draw from when pitching integrations. Each entry is a one-line "connect X → I can Y" with a concrete example prompt the user could send next: - Gmail: "Summarise the most important emails that came in overnight and flag anything that needs a reply today." - Google Calendar: "What's on my calendar tomorrow, and do I have a 30-minute gap before 2pm?" - GitHub: "List open issues on my main project tagged 'bug' and summarise which ones look newest or most urgent." - Notion: "Pull up my 'Ideas' Notion database and show me the three newest entries." - Slack / Discord / Linear / Jira / etc. with similar shapes. Plus a sub-section for messaging platforms (Telegram / Discord / Slack / iMessage / WhatsApp / Signal / web-fallback) that clarifies which each is best for, and a sub-section for the other capabilities (web search, browser automation, HTTP requests, local AI) that explains what breaks without them. The LLM is told NOT to list everything — just pick 2-3 most likely to matter, defaulting to Gmail + GitHub + one of {Calendar, Notion} as the top-3 pitch when no profile context is available. * Tone guidelines updated to document the stretched word budget for bare installs (200-350 for configured users, 250-400 for bare installs). * "What NOT to do" list updated: - Explicitly allows product-tour-style listing ONLY in the bare-install case (Step 2.5), forbids it elsewhere. - Clarifies that describing what WOULD unlock with integration X is fine and encouraged; claiming a capability the user doesn't have is still forbidden. - Adds a new "Don't gloss over a bare install" entry that pins the rule: API-key-only users get concrete pitches and example prompts, not vague suggestions. Scope note: this commit does NOT change the completion logic. `complete_onboarding(complete)` still accepts API-key-only as the minimum bar — that's a separate design question for the maintainer about whether zero-integration users should be gatekept. This change improves what the welcome agent SAYS to those users, not whether they're allowed to proceed. Tests: all 14 `agent::agents::tests` pass (including `welcome_has_onboarding_and_memory_tools` which validates the welcome agent's declarative shape is unchanged). The prompt.md edit is pure content — no schema changes, no tool additions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(web-channel): route Tauri in-app chat to welcome/orchestrator + Windows fsync fix (#525) Closes the web-channel-side half of the #525 welcome agent routing. Also bundles a small pre-existing Windows compatibility fix for `app_state::ops::sync_parent_dir` that was blocking end-to-end testing of this branch on Windows. ## Web channel routing (primary change) Commit 4b ( |
||
|
|
7b457aa27d |
feat(agent): pure orchestrator pattern with per-skill delegation tools (#496)
* feat(agent): pure orchestrator pattern with per-skill delegation tools (#478) Refactors the main agent from a direct tool-calling model to a pure orchestrator that delegates all work through dynamically generated tools. Architecture changes: - Orchestrator only sees generated tools (notion, gmail, research, run_code, review_code, plan, spawn_subagent) — skill tools are architecturally unreachable from the main agent - Each installed skill auto-generates a delegation tool at build time (SkillDelegationTool) that routes to skills_agent with the correct skill_filter - Static archetype tools (research, run_code, etc.) delegate to their respective sub-agents - visible_tool_specs filters the function-calling schema sent to the provider, enforcing the orchestrator boundary at the API level Prompt changes: - Rewrote AGENTS.md as a lean orchestrator prompt — no more routing tables or agent_id instructions - Orchestrator skips TOOLS.md, MEMORY.md, HEARTBEAT.md (~6k tokens saved per turn) — subagents get tool specs from the registry - Workspace .md files auto-sync via builtin-hash mechanism so prompt updates ship automatically to existing installs Bug fixes: - ModelSpec::Hint now resolves to {hint}-v1 (e.g. agentic-v1) instead of hint:agentic which the backend rejected - validate_skill_filter now uses skill_id from the engine tuple instead of splitting on __ in the raw tool name (which always failed) - Memory context forwarded to subagents via ParentExecutionContext Observability: - Added [agent] tagged logs for tool responses, agent state transitions, and delegation decisions throughout turn.rs See docs/agent-prompt-architecture.excalidraw for the visual diagram. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: rustfmt orchestrator_tools.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review — dispatch guard, fork specs, decouple sync - Enforce visible-tool allowlist at dispatch time (not just schema) - Fork mode uses visible_tool_specs (not full registry) - De-duplicate spawn_subagent when extending orchestrator tools - Raw tool output moved to debug level, info level logs metadata only - Decouple workspace file sync from prompt rendering so skipped files still get synced to disk Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9764a875e8 |
fix(subconscious): seed defaults into per-user workspace + fix Intelligence page stale log (#462)
* fix(subconscious): seed defaults and spawn heartbeat on startup The subconscious engine was only constructed lazily on the first engine-routed RPC (trigger, tasks_add, status). Because handle_tasks_list bypasses the engine and reads the store directly, a fresh install showed an empty Subconscious panel until the user clicked "Run now", even though SubconsciousEngine::new() seeds the 3 default system tasks on construction. Separately, HeartbeatEngine::run() — the periodic tick loop — was never spawned in production code. The only callers of HeartbeatEngine were tests, so ticks never fired automatically; users had to trigger each evaluation manually. Both issues are fixed together in run_server_inner, following the existing start_if_enabled pattern used by voice, screen_intelligence, and autocomplete: 1. Call get_or_init_engine() at startup to construct the SubconsciousEngine eagerly, which runs seed_default_tasks via from_heartbeat_config. Construction is idempotent via OnceLock; seeding is idempotent by title match, so repeat startups do not duplicate the defaults. 2. Construct HeartbeatEngine with the heartbeat config and workspace_dir, then tokio::spawn heartbeat.run() so the periodic tick loop runs for the process lifetime. The loop re-acquires the shared engine via get_or_init_engine() on each tick. Guarded by config.heartbeat.enabled so users who disable the heartbeat get neither startup seeding nor the background loop. Add engine_construction_seeds_default_tasks integration test that locks in the invariant: constructing SubconsciousEngine on a fresh workspace_dir must leave the 3 default system tasks in the store, with no tick, trigger, or explicit seed call. Also asserts that reconstructing the engine on the same workspace does not duplicate the defaults. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(subconscious): defer engine bootstrap until after login Default system tasks seeded at sidecar startup into the pre-login global workspace (`~/.openhuman/workspace/`) instead of the per-user workspace (`~/.openhuman/users/<id>/workspace/`) the UI reads from after login. The engine singleton is built lazily via `get_or_init_engine()` and cached in a `OnceLock`. `Config::load_or_init` resolves `workspace_dir` from `active_user.toml` — which does not exist until after login. When the engine was constructed on startup it therefore seeded into the global default, then the frozen singleton kept pointing at that path for the rest of the session while RPC handlers like `tasks_list` re-loaded config per call and read from the correct per-user path, silently returning an empty list. Fix: - `subconscious/global.rs`: add `bootstrap_after_login()` (idempotent via `BOOTSTRAPPED: AtomicBool`) which builds the engine against the now-correct per-user workspace and spawns the heartbeat loop. Track the heartbeat `JoinHandle` in a static so it can be aborted cleanly. Add `reset_engine_for_user_switch()` that aborts the heartbeat, clears the engine option, and resets the bootstrap flag. - `core/jsonrpc.rs`: replace the unconditional eager init on startup with a conditional one that only bootstraps if `active_user.toml` already exists (so a user logged in from a previous session still gets the engine up immediately after restart). - `credentials/ops.rs`: call `bootstrap_after_login()` at the end of `verify_and_store_session` so a fresh login triggers seeding against the per-user workspace. Call `reset_engine_for_user_switch()` in `clear_session` so logout tears down the engine + heartbeat loop and a subsequent login rebuilds them against the new user. Verified locally: sidecar restart with no `active_user.toml` logs "bootstrap deferred — waiting for login"; post-login logs "seeded 3 tasks on init" + "heartbeat periodic loop spawned"; and `subconscious.tasks_list` returns the 3 system defaults from the per-user DB. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(subconscious): bound config load + guard frontend poll Two related fixes for the Intelligence page freezing on a stale subconscious activity-log snapshot while ticks kept progressing in the sidecar. Root cause (backend): the subconscious RPC handlers were the only outlier in the entire JSON-RPC surface that called the raw `Config::load_or_init()` instead of the shared `load_config_with_timeout()` wrapper that every other domain schemas.rs uses (cron, webhooks, voice, team, skills, service, referral, doctor, …). `load_or_init` constructs a fresh `SecretStore` and runs a chain of `decrypt_optional_secret` calls on every invocation, which may IPC to the OS keychain — slow, unbounded, no caching. Under the Intelligence page's 3-second poll (4 parallel RPCs × ~7 keychain round-trips each = ~28 keychain calls every 3s), this pileup was enough to pin the frontend's `Promise.all` past the poll interval. Root cause (frontend): `useSubconscious.refresh()` uses `fetchingRef` as an in-flight guard. The ref is only cleared inside the `finally` block that runs after `Promise.all` settles. With no per-RPC timeout on the client side either, a single slow backend call would leave the ref stuck `true`, and every subsequent 3s `setInterval` tick would silently early-return at the top of `refresh`. The poller kept firing, but every call was a no-op — so the UI froze on whatever snapshot it last successfully fetched, even though the backend was still ticking through new decisions. Backend fix (`src/openhuman/subconscious/schemas.rs`): - Replace the local `load_config()` helper body to delegate to `crate::openhuman::config::load_config_with_timeout()`. Matches the 28 other domain schemas.rs files and brings subconscious handlers under the same 30s bound used everywhere else. Frontend fix (`app/src/hooks/useSubconscious.ts`): - Add a `withTimeout` helper (2.5s per-RPC, strictly less than the 3s poll interval) that races each of the 4 parallel RPCs against a timeout and resolves `null` on timeout — matching the existing `.catch(() => null)` contract so downstream setState logic is unchanged. - Clear `fetchingRef.current = false` in the useEffect cleanup so a late-returning request or a React Strict Mode double-mount in dev can't leave the ref stuck `true` for the next mount. Defense in depth: the backend bound prevents a permanent hang and matches repo conventions, while the frontend bound guarantees the 3s poll loop can never be pinned beyond one tick regardless of server-side latency. Verified locally — `cargo check` clean, `tsc --noEmit` clean, all 18 pre-existing warnings in unrelated modules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style(jsonrpc): cargo fmt the startup bootstrap block CI ran `cargo fmt --all -- --check` and flagged the conditional bootstrap block in `run_server_inner` — `let already_logged_in` should fold onto one line, the `.and_then` closure body should inline, the `match ... .await` chain should fold, and the short log!() calls should not break across lines. No behavior change. Fixes three jobs on PR #462 that were all failing at the same `cargo fmt --all -- --check` step (Rust Quality, Rust Tests, Type Check TypeScript — the last one chains cargo fmt after its prettier check). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
fa5f822f95 |
feat(subconscious): stabilize heartbeat + subconscious loop (#392) (#437)
* feat(subconscious): stabilize heartbeat + subconscious loop (#392) - Enable heartbeat by default (enabled=true, inference_enabled=true, 5min interval) - Seed system tasks on engine init, not first tick - SQLite-backed task/log/escalation persistence - Overlap guard with generation counter — stale ticks are cancelled - Single log entry per task per tick, updated in place (in_progress → act/noop/escalate/failed/cancelled) - Rate-limit retry (429 only) for agentic-v1 cloud model calls - Approval gate: unsolicited write actions on read-only tasks require user approval - Analysis-only mode for agentic-v1 on read-only escalations - Non-blocking status RPC — reads from DB, never blocks on engine mutex - Frontend: system vs user task distinction, toggle switches, expandable activity log - Frontend: 3s auto-poll on Subconscious tab, skill-related escalation navigation - Consecutive failure counter in status (resets on success) - last_tick_at only advances on successful evaluation - Missing LLM evaluation fallback — unevaluated tasks default to noop - Docs: subconscious.md architecture guide, memory-sync-functions.md reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting for subconscious frontend files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: retrigger checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(heartbeat): use disabled config in run_returns_immediately_when_disabled test HeartbeatConfig::default() has enabled: true, so run() entered the infinite loop and never returned — hanging the test (and CI) indefinitely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(subconscious): remove HEARTBEAT.md task import, use SQLite as sole task source Tasks are now managed exclusively in SQLite via the Subconscious UI. HEARTBEAT.md is retained for instructions/context only, not as a task list. Situation report now reads pending tasks from SQLite instead of HEARTBEAT.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt on subconscious engine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
000b40bf43 |
fix(local_ai): Windows Ollama discovery + DirectML GPU acceleration for GLiNER RelEx (#416)
Ollama was not found on Windows because find_system_ollama_binary lacked common Windows install paths (%LOCALAPPDATA%\Programs\Ollama). The server spawn also silently swallowed errors, and the NSIS installer fallback didn't check system paths after install. GLiNER RelEx ONNX sessions were CPU-only — no execution providers were configured. Now offers DirectML (Windows), CoreML (macOS), and CUDA as GPU backends with automatic fallback. Updated the release to v0.5-onnx.2 with a DirectML-enabled onnxruntime.dll. Bundle completeness now requires the platform DLL and verifies checksums to trigger re-download on update. Changes: - Add Windows common paths to find_system_ollama_binary (install.rs) - Log and return spawn errors in start_and_wait_for_server (ollama_admin.rs) - Fall back to find_system_ollama_binary after Windows installer (ollama_admin.rs) - Add platform_execution_providers() with DirectML/CoreML/CUDA (relex.rs) - Require ORT DLL in bundle_complete check (relex.rs) - Verify platform DLL checksums in managed_bundle_complete (relex.rs) - Update release URL and SHA256 hashes for v0.5-onnx.2 (relex.rs) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9b73d00b2e |
fix(agent): inject skill tools into agent registry and unify ToolResult (#341) (#360)
* fix(agent): inject skill tools into agent registry and unify ToolResult (#341) Skill tools (Notion, Gmail, etc.) from the QuickJS runtime were invisible to the agent's tool loop — the LLM could never call them. This change: 1. Adds SkillToolBridge to wrap skill ToolDefinitions as Tool trait impls and inject them into the agent's tool registry via collect_skill_tools(). 2. Unifies ToolResult across built-in and skill tools — both now use the MCP-spec type (content blocks + is_error) from skills::types, eliminating the need for result conversion between the two systems. 3. Adds convenience constructors (ToolResult::success/error/json) and accessor methods (output/text) to simplify all tool implementations. 4. Adds diagnostic logging in the tool loop for tool registry contents and tool lookup results. Verified: agent_chat RPC successfully called Notion list-all-pages tool and returned real workspace data through the agent loop. Closes #341 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tools): address CodeRabbit review feedback on PR #360 - cron_run: preserve execution details (result_output) on failure - delegate: fix mismatched bracket in agent response header format string - run_linter: include lint diagnostics in error output, not just exit code - run_tests: include test output in error result, not just exit code - skill_bridge: prevent namespaced tool-name collisions with dedup + __ delimiter validation; sanitize runtime errors in model-facing output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: apply cargo fmt to skill_bridge.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b8ae9674b3 |
fix(memory): graph query returns namespace data and add sync e2e tests (#344) (#363)
* fix(memory): graph query returns namespace data and add sync e2e tests (#344) The knowledge graph UI showed empty because graph_query(None) only queried the graph_global table, while ingestion writes to graph_namespace. Now graph_query(None) queries both tables via graph_query_all(), merging results. Changes: - Added graph_query_all() in unified graph store to query across all namespaces - MemoryClient::graph_query(None) now uses graph_query_all() instead of graph_query_global() - MemoryWorkspace passes selectedNamespace to the RPC call - Added diagnostic logging in ingestion pipeline (RelEx model availability, extraction counts) - Added debug logging in tauriCommands for unexpected response shapes - Added 2 integration tests proving document sync populates the graph (ignored by default for CI, run with --ignored) Closes #344 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: remove trailing comma for Prettier compliance Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
53b3d67460 |
fix(memory): ingest entities/relations after skill sync (#292) (#313)
* fix(memory): ingest entities/relations after skill sync (#292) After store_skill_sync persists a raw document, call ingest_doc to extract entities and relations into the memory graph. For Notion syncs, individual pages are extracted from the sync blob and ingested separately (keyed by page ID) so GLiNER operates on clean page text rather than a monolithic JSON blob. Other skills ingest the full content as a single document. Closes #292 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: apply cargo fmt to event_loop.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(memory): use page_key for ingestion dedup, strip credentials - Use page_key (page-{id}) as the ingestion document key to match store_skill_sync, ensuring consistent dedup on re-sync - Strip __oauth_credential from content before ingesting in the fallback (non-Notion) branch to prevent credentials in the graph - Add tests for strip_credentials Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
735152cb3f |
feat(memory): surface entity types in recall/query API and UI (#207) (#302)
* fix: patch whisper-rs-sys for Windows MSVC static CRT (/MT) The upstream whisper-rs-sys builds whisper.cpp via CMake which defaults to /MD (dynamic CRT), but Rust and all other C deps use /MT (static CRT). This causes LNK2038/LNK1169 linker errors on Windows. Patch whisper-rs-sys from tinyhumansai/whisper-rs-sys fork which adds config.static_crt(true) and overrides all per-config CMake flags (Debug/Release/MinSizeRel/RelWithDebInfo) from /MD to /MT. Closes #273 * feat: surface entity types in memory recall/query text context Entity types extracted by GLiNER (person, project, organization, etc.) were stored in graph attrs but not rendered in LLM context text. Relations now display as Alice (PERSON) -[OWNS]-> Atlas (PROJECT) instead of Alice -[OWNS]-> Atlas. Closes #207 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(memory): add entity type UI rendering (#207) - New MemoryTextWithEntities component with colour-coded type badges - MemoryWorkspace + MemoryDebugPanel pass structured entity data - MemoryGraphMap shows entity types below node labels - MemoryInsights shows EntityTypeBadge for subject/object types - tauriCommands returns typed MemoryQueryResult with entities - Updated useConsciousItems and tests for new return types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix prettier formatting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix cargo fmt in query.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use local regex instead of mutating module-level lastIndex Avoids react-hooks/immutability ESLint error by using a non-global regex for the .test() check instead of resetting ENTITY_TYPE_RE.lastIndex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sanil jain <jainsanil18@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4027f50c9e |
feat(memory): add bulk namespace clear operation (#205) (#301)
* fix: patch whisper-rs-sys for Windows MSVC static CRT (/MT) The upstream whisper-rs-sys builds whisper.cpp via CMake which defaults to /MD (dynamic CRT), but Rust and all other C deps use /MT (static CRT). This causes LNK2038/LNK1169 linker errors on Windows. Patch whisper-rs-sys from tinyhumansai/whisper-rs-sys fork which adds config.static_crt(true) and overrides all per-config CMake flags (Debug/Release/MinSizeRel/RelWithDebInfo) from /MD to /MT. Closes #273 * feat: add bulk namespace clear operation for memory Add memory_clear_namespace RPC that atomically deletes all documents, vector chunks, KV entries, and graph relations in a namespace. Includes Tauri command wrapper and three tests verifying cleanup, no-op on empty namespaces, and cross-namespace safety. Closes #205 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(memory): add Clear Namespace UI to MemoryDebugPanel (#205) Adds namespace selector, confirmation dialog, success/error feedback, and auto-refresh after clearing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix prettier and cargo fmt formatting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: unwrap RpcOutcome envelope in memoryClearNamespace callCoreRpc returns { result: T }, but memoryClearNamespace was returning the raw envelope instead of unwrapping .result like other callers do. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sanil jain <jainsanil18@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
bd8d3ed15a |
fix(memory): clean up unused JWT token parameter in memory init (#204) (#300)
* fix: patch whisper-rs-sys for Windows MSVC static CRT (/MT) The upstream whisper-rs-sys builds whisper.cpp via CMake which defaults to /MD (dynamic CRT), but Rust and all other C deps use /MT (static CRT). This causes LNK2038/LNK1169 linker errors on Windows. Patch whisper-rs-sys from tinyhumansai/whisper-rs-sys fork which adds config.static_crt(true) and overrides all per-config CMake flags (Debug/Release/MinSizeRel/RelWithDebInfo) from /MD to /MT. Closes #273 * fix: clean up unused JWT token parameter in memory init Memory is local-only (SQLite). The from_token() method accepted a JWT but ignored it, always falling back to new_local(). Remove the dead method, make jwt_token optional in MemoryInitRequest for backward compat, and document the local-only design. Closes #204 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sanil jain <jainsanil18@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
945a5c4a02 |
feat: subconscious loop — local-model background awareness via heartbeat (#268)
* feat: subconscious loop — local-model background awareness via heartbeat Add a subconscious inference layer to the heartbeat engine. On each tick, the engine reads HEARTBEAT.md tasks, builds a delta-based situation report (memory docs, graph relations, skills health, environment), and evaluates them with the local Ollama model. Architecture: - HeartbeatEngine (scheduler) delegates to SubconsciousEngine (brain) - HeartbeatConfig extended with inference_enabled, context_budget_tokens - No separate SubconsciousConfig — all config lives under [heartbeat] - Default HEARTBEAT.md ships with 3 active tasks (email, deadlines, skills) Subconscious module (src/openhuman/subconscious/): - engine.rs: tick logic, local model inference, escalation to cloud model - situation_report.rs: delta assembler (memory, graph, skills, env, tasks) - prompt.rs: task-driven system prompt for local model - decision_log.rs: dedup tracking with 24h TTL and acknowledgment - types.rs: Decision (noop/act/escalate), TickOutput, RecommendedAction - schemas.rs: RPC controllers (subconscious_status, subconscious_trigger) - integration_test.rs: two-tick lifecycle test with fixtures Decision flow: - noop: no changes, skip — no LLM call wasted - act: local model recommends actions → stored in memory KV - escalate: calls cloud model to resolve → concrete actions stored Verified with real Ollama inference (gemma3:4b): - Tick 1: ingested gmail+notion → "act: deadline needs attention" (high) - Tick 2: ingested state changes → "act: deadline moved" (high) - Skills health section populated from live skill registry Closes #145 * feat: add subconscious_actions RPC endpoint New endpoint openhuman.subconscious_actions returns stored action entries from the subconscious KV namespace, sorted by most recent first, with configurable limit (default 20). Response format: { "entries": [ { "tick_at": 1775117975.58, "actions": [...] } ], "count": 1 } The upcoming subconscious page will call this to display notifications and recommended actions to the user. * fix: budget underflow and UTF-8 panic in situation report truncation - Use saturating_add for newline byte to prevent underflow when section exactly fills the remaining budget - Truncate at valid UTF-8 char boundary using char_indices instead of raw byte slicing, which panicked on multibyte characters - Add tests for exact-fit and multibyte truncation * fix: address CodeRabbit review — shared engine, dedup, consistent schema Fixes from CodeRabbit review on PR #268: - #8 Two engine instances: Add global.rs singleton shared between HeartbeatEngine::run() and RPC handlers. Both use get_or_init_engine() so decision log, counters, and last_tick_at are always in sync. - #3 Dedup disabled: tick() now extracts actual document IDs from memory via build_situation_report_with_doc_ids() and passes them to decision_log.record(). filter_unsurfaced() actually filters now. - #5 Decision log not loaded on trigger: tick() loads persisted log from KV on first execution (total_ticks == 0), not only from run(). - #4 Inconsistent action schema: handle_escalation() normalizes agent response into RecommendedAction[] via normalize_escalation_response(). Both act and escalate paths store the same schema. - #7 Key collision: store_actions() uses millisecond timestamp + random suffix instead of second-precision truncation. - #10 No-changes unreachable: tick() checks has_new_data (unsurfaced doc IDs) OR has_memory_changes (report text) instead of naive string matching on environment section. * fix: include document content in situation report, not just titles The local model needs actual content to evaluate HEARTBEAT.md tasks meaningfully. Previously it only saw titles like "Deadline reminder" with no way to know if it's urgent. Now recalls content per namespace (up to 500 chars each, max 10 namespaces) via client.recall_namespace(). The model sees actual email text and page content alongside the task checklist. * fix: timestamp parsing, byte-boundary slicing, and truncation overshoot - schemas.rs: split on first ':' after 'actions:' prefix before parsing timestamp, so keys like 'actions:123456:xyz' parse correctly - situation_report.rs: use truncate_at_char_boundary() for error strings instead of raw byte slice which panics on multibyte characters - situation_report.rs: fix append_section and truncate_at_char_boundary to use char END offset (i + len_utf8) in take_while condition, so multibyte chars that start before but end after the budget are excluded |
||
|
|
d73ccda13d |
fix: patch whisper-rs-sys for Windows MSVC static CRT (/MT) (#276)
The upstream whisper-rs-sys builds whisper.cpp via CMake which defaults to /MD (dynamic CRT), but Rust and all other C deps use /MT (static CRT). This causes LNK2038/LNK1169 linker errors on Windows. Patch whisper-rs-sys from tinyhumansai/whisper-rs-sys fork which adds config.static_crt(true) and overrides all per-config CMake flags (Debug/Release/MinSizeRel/RelWithDebInfo) from /MD to /MT. Closes #273 |
||
|
|
f5b66de2f1 |
fix: unwrap API envelope in memory tauriCommands and fix file path scope (#172)
After the controller registry migration (#138), memory RPC methods return ApiEnvelope responses but the frontend expected flat data. This caused "map is not a function" errors on the Intelligence page. - Unwrap envelope in memoryListDocuments, memoryListNamespaces, aiListMemoryFiles, and aiReadMemoryFile in tauriCommands.ts - Fix resolve_existing_memory_path and resolve_writable_memory_path to scope against workspace root instead of memory/ subdirectory — workspace files (MEMORY.md, SOUL.md) live at the root Closes #170 |
||
|
|
2383d51ea6 |
revert: remove unnecessary prompt and parser changes from #156 (#169)
The actual fix in #156 was adding chat(ChatRequest) to ReliableProvider. The prompt changes in instructions.rs and the bracket tool call parser in parse.rs were added during investigation but are not needed — the model uses native tool calls when ReliableProvider properly delegates to the inner provider. |
||
|
|
305c58a6ec |
fix: reduce agent loop hallucination and improve tool call reliability (#156)
* fix: reduce agent loop hallucination and improve tool call reliability - Strengthen tool-use instructions with explicit anti-hallucination rules: "NEVER narrate tool use without emitting tags", "use exact tool names", "only respond without tool call when no tool is needed" - Wire context guard into tool loop: check utilization before each LLM call, abort on context exhaustion (>95% with circuit breaker tripped) - Add 120-second timeout on tool execution to prevent hangs - Add debug/warn/error logging at all loop boundaries: LLM request, response (with token counts), tool call parsing, tool execution, unknown tools, timeouts, and final response Closes #144 * fix: implement chat(ChatRequest) on ReliableProvider and add bracket tool call parser Root cause: ReliableProvider did not implement the chat(ChatRequest) trait method. The agent loop called provider.chat() which fell through to the default trait implementation — this used chat_with_history() which strips native tool support and sends raw tool-role messages without the required assistant tool_calls, causing the backend Jinja template to reject the request with "Message has tool role, but there was no previous assistant message with a tool call!" Fixes: - Add chat(ChatRequest) to ReliableProvider with full retry/failover logic, matching the existing chat_with_system/chat_with_history implementations. Delegates to inner provider's chat() which properly converts messages to native OpenAI format with tool_calls. - Add [TOOL_CALL]/[/TOOL_CALL] bracket format to the tool call parser (parse.rs) — some models emit this format instead of <tool_call> XML. - Add parse_bracket_tool_call() for the pseudo-syntax format: {tool => "name", args => { --key "value" }} Verified with real staging backend (agentic-v1 model): - Shell tool calls execute successfully - File read tool calls return real content - Knowledge questions return without tool calls - No Jinja template errors Closes #144 |
||
|
|
600dab4336 |
refactor: migrate memory service to controller registry pattern (#138)
Move all 23 memory RPC methods from legacy dispatch (src/rpc/dispatch.rs) to the controller registry pattern with typed schemas. - Create src/openhuman/memory/schemas.rs with 23 ControllerSchema definitions, RegisteredController entries, and handler functions - Wire memory controllers into src/core/all.rs registry builders - Remove all memory.* and ai.* branches from dispatch.rs (only security_policy_info remains) - Update frontend to use openhuman.memory_* method names directly in tauriCommands.ts (no legacy aliases needed) - Move ai.list_memory_files/read/write into memory namespace as openhuman.memory_list_files/read_file/write_file - Update jsonrpc.rs and tauriCommandsMemory test method strings Methods are now accessible via both JSON-RPC (openhuman.memory_*) and CLI (openhuman memory <function>). |
||
|
|
77fd5f9edd |
fix: propagate entity_type into retrieval context for graph visualization (#135)
* feat(memory): connect graph query and doc ingest APIs to frontend Wire up memory.graph.query and memory.doc.ingest RPC endpoints and integrate graph relations into the MemoryWorkspace UI, replacing backend-only entity counts with local graph store data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting in MemoryWorkspace and tauriCommands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add unit tests for memory graph query, doc ingest, and dispatch routing Cover the new graph query and doc ingest APIs added in this PR: - Rust dispatch tests: routing, param validation, unknown method fallthrough - Frontend tauriCommands tests: Tauri guard, RPC forwarding for memoryGraphQuery/memoryDocIngest - MemoryWorkspace component tests: graph relations rendering, evidence badges, empty states Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier and cargo fmt formatting in test files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting in tauriCommandsMemory test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: merge duplicate tauriCommands import in MemoryWorkspace Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting in MemoryWorkspace Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: propagate entity_type from graph relation attrs into retrieval context build_retrieval_context was discarding entity types that were already present in relation attrs.entity_types (populated during ingestion by GLiNER relex). Entities in MemoryRetrievalEntity now carry their type (e.g. PERSON, PROJECT, WORK_ITEM) instead of always being None. Adds unit tests for both typed and untyped paths, plus an ignored GLiNER smoke test (gline_rs_smoke) that verifies the full pipeline from Notion fixture ingestion through graph storage to retrieval context with the real ONNX model. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sanil jain <jainsanil18@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4d3b63857f |
feat(memory): connect graph query and doc ingest APIs (#124)
* feat(memory): connect graph query and doc ingest APIs to frontend Wire up memory.graph.query and memory.doc.ingest RPC endpoints and integrate graph relations into the MemoryWorkspace UI, replacing backend-only entity counts with local graph store data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting in MemoryWorkspace and tauriCommands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add unit tests for memory graph query, doc ingest, and dispatch routing Cover the new graph query and doc ingest APIs added in this PR: - Rust dispatch tests: routing, param validation, unknown method fallthrough - Frontend tauriCommands tests: Tauri guard, RPC forwarding for memoryGraphQuery/memoryDocIngest - MemoryWorkspace component tests: graph relations rendering, evidence badges, empty states Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier and cargo fmt formatting in test files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting in tauriCommandsMemory test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: merge duplicate tauriCommands import in MemoryWorkspace Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting in MemoryWorkspace Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sanil jain <jainsanil18@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |