mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
docs: remove stale plan/status docs and clean dangling references (#4508)
This commit is contained in:
+2
-9
@@ -198,7 +198,7 @@ cargo check --manifest-path Cargo.toml
|
||||
cargo check --manifest-path app/src-tauri/Cargo.toml
|
||||
```
|
||||
|
||||
If you only changed docs in a normal local workflow, `pnpm format:check` is usually the only validation you need. AI-authored or remote-agent PRs must still follow [`docs/agent-workflows/codex-pr-checklist.md`](docs/agent-workflows/codex-pr-checklist.md) and report any blocked commands with the exact command and error.
|
||||
If you only changed docs in a normal local workflow, `pnpm format:check` is usually the only validation you need. AI-authored or remote-agent PRs must still fill in the AI Authored PR Metadata section of the PR template and report any blocked commands with the exact command and error.
|
||||
|
||||
### 6. Run tests and checks
|
||||
|
||||
@@ -217,7 +217,7 @@ Merge-gate context:
|
||||
|
||||
- PRs must meet the checks enforced by CI and keep changed-line coverage at or above 80%.
|
||||
- For code changes, run the smallest relevant local checks before you push.
|
||||
- For AI-authored or remote-agent PRs, also follow [`docs/agent-workflows/codex-pr-checklist.md`](docs/agent-workflows/codex-pr-checklist.md).
|
||||
- For AI-authored or remote-agent PRs, also fill in the AI Authored PR Metadata section of the PR template.
|
||||
|
||||
### 7. Local data and user-facing state
|
||||
|
||||
@@ -302,13 +302,6 @@ git checkout -b docs/your-change
|
||||
|
||||
If you are contributing through a coding agent or remote environment, include the metadata required by the PR template and the Codex PR checklist.
|
||||
|
||||
### Contributor rewards
|
||||
|
||||
Maintainers can reward eligible contributors through the automated workflow in
|
||||
[`docs/CONTRIBUTOR-REWARDS.md`](docs/CONTRIBUTOR-REWARDS.md). First merged pull
|
||||
requests are handled automatically, and maintainers can apply the `reward user`
|
||||
label to manually start the same Discord and merch invite flow.
|
||||
|
||||
## Project Conventions
|
||||
|
||||
- Use Redux and existing app state patterns instead of adding new ad hoc browser storage.
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
// TODO(picker-schema): today this subcomponent is wired in based on
|
||||
// the skill id being literally `dev-workflow`. The cleaner long-term
|
||||
// path is to extend `skill.toml`'s `[[inputs]]` with an optional
|
||||
// `picker = "github-issue"` discriminator and route here from that;
|
||||
// see docs/skills-runner-unification.md open question 1.
|
||||
// `picker = "github-issue"` discriminator and route here from that.
|
||||
import createDebug from 'debug';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ import SmartIssuePicker from './SmartIssuePicker';
|
||||
//
|
||||
// TODO(picker-schema): replace this hard-coded set with a schema-level
|
||||
// signal in `skill.toml` — e.g. `[[inputs]] picker = "github-issue"`.
|
||||
// See docs/skills-runner-unification.md open question 1.
|
||||
const SMART_PICKER_SKILL_IDS = new Set(['dev-workflow']);
|
||||
const SMART_PICKER_INPUT_NAMES = new Set(['repo', 'upstream', 'target_branch', 'fork_owner']);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* WorkflowRunnerBody — vitest coverage for the saved-schedules block.
|
||||
*
|
||||
* Phase 2 of the WorkflowRunnerBody / DevWorkflowPanel unification (see
|
||||
* docs/skills-runner-unification.md): this file is seeded with the
|
||||
* Phase 2 of the WorkflowRunnerBody / DevWorkflowPanel unification:
|
||||
* this file is seeded with the
|
||||
* smoke-test for the enable/disable toggle so future Phase 3 chunks
|
||||
* (run-history, active-config card, smart-issue picker gating) drop
|
||||
* additional cases alongside.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* standalone route. The "card" style matches the other sections inside
|
||||
* VoicePanel.
|
||||
*
|
||||
* Plan: docs/superpowers/plans/2026-06-02-global-ptt.md (Task 13).
|
||||
* Spec: docs/superpowers/specs/2026-06-02-global-ptt-design.md.
|
||||
*/
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
/**
|
||||
* E2E: global push-to-talk (PTT) end-to-end flow with mocked STT.
|
||||
*
|
||||
* Task 14 from `docs/superpowers/plans/2026-06-02-global-ptt.md`.
|
||||
*
|
||||
* What this spec exercises (top to bottom):
|
||||
*
|
||||
* UI:
|
||||
@@ -49,7 +47,6 @@
|
||||
* wire). We spy on `__TAURI_INTERNALS__.invoke` before the press to
|
||||
* capture the call payload.
|
||||
*
|
||||
* Plan: docs/superpowers/plans/2026-06-02-global-ptt.md (Task 14).
|
||||
* Spec: docs/superpowers/specs/2026-06-02-global-ptt-design.md.
|
||||
*
|
||||
* Limitations / notes for follow-up sessions:
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
# Agent self-learning
|
||||
|
||||
OpenHuman learns user preferences continuously and surfaces them as ambient defaults in every system prompt. The mechanism is a small **personalization cache** materialized from multiple deterministic + LLM-driven producers, scored by stability, rendered into a user-editable `PROFILE.md`, and injected into prompts through the existing prompt-section pipeline.
|
||||
|
||||
This document covers how preferences are captured, scored, persisted, and surfaced. For the originating issue, see [#566](https://github.com/tinyhumansai/openhuman/issues/566).
|
||||
|
||||
---
|
||||
|
||||
## What gets learned
|
||||
|
||||
Six classes, encoded in the `key` prefix and stored in the existing `user_profile_facets` table:
|
||||
|
||||
| Class | Example facets |
|
||||
|---|---|
|
||||
| `style/*` | `verbosity=terse`, `format=bullets`, `preamble=skip`, `language=english`, `emoji=skip` |
|
||||
| `identity/*` | `name=Sanil`, `timezone=PST`, `role=engineer`, `employer=vezures` |
|
||||
| `tooling/*` | `package_manager=pnpm`, `lang=rust`, `framework=astro`, `runtime=bun` |
|
||||
| `veto/*` | `tool=jest → banned`, `format=nested-bullets → banned` |
|
||||
| `goal/*` | free-form goal sentences (slugified key) |
|
||||
| `channel/preference` | `primary=desktop-chat` |
|
||||
|
||||
Recurring topics, recurring entities, and prior threads are **not** in the cache. They live in the memory tree and are retrieved per-turn by `memory_recall` under the prompt-bias instruction described below.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
Four stages: **capture → identify → score → materialize**.
|
||||
|
||||
```
|
||||
inputs substrate candidates cache
|
||||
───── ───────── ────────── ─────
|
||||
chat turns ──→ episodic_log ──→ Buffer (push) ──┐
|
||||
+ tree │
|
||||
skill syncs ──→ tree (sources) ──→ Buffer (push) ──┤
|
||||
├─→ user_profile_facets
|
||||
channel inbound ──→ tree ──→ Buffer (push) ──┤ (state, stability,
|
||||
│ user_state, evidence)
|
||||
documents ──→ tree ──→ Buffer (push) ──┘ │
|
||||
▼
|
||||
stability_detector::rebuild CacheRebuilt event
|
||||
(every 30 min + event-driven) │
|
||||
▼
|
||||
ProfileMdRenderer
|
||||
│
|
||||
▼
|
||||
PROFILE.md
|
||||
(managed blocks)
|
||||
│
|
||||
▼
|
||||
UserFilesSection
|
||||
+ UserProfileSection
|
||||
+ MemoryAccessSection
|
||||
│
|
||||
▼
|
||||
agent system prompt
|
||||
```
|
||||
|
||||
Chat turns flow into the tree as `source_id="conversations:agent"` (with `tool_calls_json` stripped at canonicalize), so the same `tree_source::summariser` that already runs over Slack/Gmail/Notion now also produces summaries — and structured facet candidates — over chat content.
|
||||
|
||||
---
|
||||
|
||||
## Producers
|
||||
|
||||
Five producers write `LearningCandidate` values into `learning::candidate::global()`:
|
||||
|
||||
| # | Producer | Emits | LLM? |
|
||||
|---|---|---|---|
|
||||
| 1 | `composio::providers::profile::persist_provider_profile` (existing) | Identity from Gmail/Slack/Notion account fields | No |
|
||||
| 2 | `learning::extract::signature::EmailSignatureSubscriber` | Identity from email signatures (last 8 lines) | No |
|
||||
| 3 | `learning::extract::heuristics` (`LengthRatioDetector`, `EditWindowDetector`, `CorrectionRepeatDetector`) | Style + Veto from per-turn rolling state | No |
|
||||
| 4 | `learning::ReflectionHook` (rerouted) | Goal + Style from heuristic cues and LLM-extracted reflections | Optional |
|
||||
| 5 | `tree_source::summariser::llm` (extended schema) | All classes — long-tail extraction over rolling per-source summaries | Yes (existing call, extended output) |
|
||||
|
||||
Producer 5 is the long-tail backbone. Its prompt now asks the model to emit `{ summary, facets[] }`, and `learning::extract::summary_facets` validates each `ParsedFacet` (canonical class, mandatory `evidence_chunks`) before pushing to the buffer. **No new LLM calls** are introduced — the change extends an existing summarization round-trip with ~150–400 output tokens.
|
||||
|
||||
Explicitly **not** producing:
|
||||
|
||||
- Hand-curated regex catalogs for style/identity/tooling — Producer 5 covers them and generalizes to vocabulary the catalog wouldn't predict.
|
||||
- Hand-curated manifest parsers for tooling — same reasoning; Astro, Bun, Deno, uv, mise, etc. all surface through the LLM summarizer.
|
||||
- Free-text NER on chat — recurring entities live in the tree's graph (from structured provider metadata) and surface contextually via `memory_recall`.
|
||||
- `ToolTrackerHook` candidate emission — failure-rate is not a clean preference signal; it stays as substrate for debugging.
|
||||
|
||||
---
|
||||
|
||||
## Identification → candidate buffer
|
||||
|
||||
Every emission carries provenance:
|
||||
|
||||
```rust
|
||||
pub struct LearningCandidate {
|
||||
pub class: FacetClass, // Style | Identity | Tooling | Veto | Goal | Channel
|
||||
pub key: String, // canonical slug, e.g. "verbosity"
|
||||
pub value: String, // canonical value, e.g. "terse"
|
||||
pub cue_family: CueFamily, // Explicit | Structural | Behavioral | Recurrence
|
||||
pub evidence: EvidenceRef, // pointer back into substrate
|
||||
pub initial_confidence: f64, // 0..=1, source-provided hint
|
||||
pub observed_at: f64, // epoch seconds
|
||||
}
|
||||
```
|
||||
|
||||
`EvidenceRef` variants cover every substrate origin (`Episodic`, `EpisodicWindow`, `SourceSummary`, `TreeTopic`, `DocumentChunk`, `EmailMessage`, `Provider`, `ToolCall`, `TreeSourceWeight`). The buffer is a thread-safe bounded ring (default capacity 1024). The stability detector drains it every rebuild.
|
||||
|
||||
---
|
||||
|
||||
## Stability scoring
|
||||
|
||||
```
|
||||
stability(class, key, value) = base × cue × user_state
|
||||
|
||||
base = Σ over evidence:
|
||||
cue_family_weight × exp(-Δt / half_life_for_class) × log(1 + evidence_count_for_family)
|
||||
|
||||
cue = 2.0 if any evidence is Explicit, else 1.0
|
||||
user = ∞ if Pinned, 0 if Forgotten, 1 otherwise
|
||||
```
|
||||
|
||||
**Cue-family weights**:
|
||||
|
||||
| Family | Weight | Rationale |
|
||||
|---|---|---|
|
||||
| `Explicit` | 1.0 | direct user statement — declaration is intent |
|
||||
| `Structural` | 0.9 | provider data / manifest content / signature — data doesn't lie |
|
||||
| `Behavioral` | 0.7 | heuristics + summary mining — must accumulate |
|
||||
| `Recurrence` | 0.6 | tree statistics — emerging |
|
||||
|
||||
**Class half-lives** (the time over which evidence weight halves):
|
||||
|
||||
| Class | Half-life |
|
||||
|---|---|
|
||||
| `identity` | 90 days |
|
||||
| `veto` | 60 days |
|
||||
| `tooling` | 30 days |
|
||||
| `goal` | 30 days |
|
||||
| `style` | 14 days |
|
||||
| `channel` | 7 days |
|
||||
|
||||
**Conflict resolution per `(class, key)`**: active value = `argmax(stability)` over candidate values. Losing values are dropped from the cache; if they re-emerge they reinforce naturally through the same path.
|
||||
|
||||
---
|
||||
|
||||
## States and qualification
|
||||
|
||||
Two state columns govern visibility:
|
||||
|
||||
`state` (driven by stability):
|
||||
|
||||
| State | Range | Effect |
|
||||
|---|---|---|
|
||||
| `Active` | `stability ≥ τ_promote` (1.5) | Renders in `PROFILE.md`, injected into prompt |
|
||||
| `Provisional` | `0.7 ≤ stability < 1.5` | Stored, not rendered |
|
||||
| `Candidate` | `0.4 ≤ stability < 0.7` | Stays in buffer, not yet in cache |
|
||||
| `Dropped` | `stability < 0.4` | Removed |
|
||||
|
||||
`user_state` (overrides scoring):
|
||||
|
||||
| State | Effect |
|
||||
|---|---|
|
||||
| `Auto` | Default; `state` follows scoring |
|
||||
| `Pinned` | Locks `Active`; resists decay |
|
||||
| `Forgotten` | Locks `Dropped`; blocks re-promotion |
|
||||
|
||||
**Class budgets** (top-N selection per class with a shared overflow pool):
|
||||
|
||||
```
|
||||
style 4 · identity 4 · tooling 5 · veto 3 · goal 3 · channel 1 · overflow 5
|
||||
total cap ≈ 25
|
||||
```
|
||||
|
||||
Per-class budgets prevent a tooling-heavy user from drowning out style and identity; the overflow pool lets one class take spare capacity from another when underused.
|
||||
|
||||
---
|
||||
|
||||
## Storage
|
||||
|
||||
Single SQLite table — `user_profile_facets` (existing, extended by an idempotent migration in `memory::store::unified::profile::migrate_profile_schema`):
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_profile (
|
||||
facet_id TEXT PRIMARY KEY,
|
||||
facet_type TEXT NOT NULL, -- legacy enum
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
confidence REAL NOT NULL DEFAULT 0.5,
|
||||
evidence_count INTEGER NOT NULL DEFAULT 1,
|
||||
source_segment_ids TEXT,
|
||||
first_seen_at REAL NOT NULL,
|
||||
last_seen_at REAL NOT NULL,
|
||||
-- learning-cache columns (added by migrate_profile_schema):
|
||||
state TEXT NOT NULL DEFAULT 'active',
|
||||
stability REAL NOT NULL DEFAULT 0.0,
|
||||
user_state TEXT NOT NULL DEFAULT 'auto',
|
||||
evidence_refs_json TEXT,
|
||||
class TEXT,
|
||||
cue_families_json TEXT,
|
||||
UNIQUE(facet_type, key)
|
||||
);
|
||||
CREATE INDEX idx_profile_state ON user_profile(state);
|
||||
CREATE INDEX idx_profile_class ON user_profile(class);
|
||||
```
|
||||
|
||||
The provider-profile path (`composio::providers::profile::persist_provider_profile`) continues to write its existing rows untouched; class is auto-derived for backward-compatibility (provider keys like `skill:gmail:default:email` map to `class=tooling`/`identity`).
|
||||
|
||||
---
|
||||
|
||||
## Surface — `PROFILE.md`
|
||||
|
||||
`{workspace_dir}/PROFILE.md` carries multiple managed blocks. Each block is owned by automation; content outside the markers is user-authored and preserved across rebuilds.
|
||||
|
||||
```markdown
|
||||
# User Profile
|
||||
|
||||
<!-- openhuman:style:start -->
|
||||
## Style
|
||||
|
||||
- **verbosity**: terse
|
||||
- **preamble**: skip *(pinned)*
|
||||
|
||||
<!-- openhuman:style:end -->
|
||||
|
||||
<!-- openhuman:identity:start -->
|
||||
## Identity
|
||||
|
||||
- **name**: Sanil
|
||||
- **timezone**: PST
|
||||
|
||||
<!-- openhuman:identity:end -->
|
||||
|
||||
<!-- openhuman:tooling:start -->
|
||||
## Tooling
|
||||
|
||||
- **lang**: rust
|
||||
- **package_manager**: pnpm
|
||||
|
||||
<!-- openhuman:tooling:end -->
|
||||
|
||||
<!-- openhuman:vetoes:start -->
|
||||
## Vetoes
|
||||
|
||||
- **tool=jest**: banned
|
||||
|
||||
<!-- openhuman:vetoes:end -->
|
||||
|
||||
<!-- openhuman:goals:start -->
|
||||
## Goals
|
||||
|
||||
- ship #566 before #686
|
||||
|
||||
<!-- openhuman:goals:end -->
|
||||
|
||||
<!-- openhuman:connected-accounts:start -->
|
||||
## Connected Accounts
|
||||
|
||||
- gmail (sanil@vezures.xyz)
|
||||
|
||||
<!-- openhuman:connected-accounts:end -->
|
||||
```
|
||||
|
||||
User-authored content between blocks (free-form notes, hand-edited details) is preserved verbatim across rebuilds.
|
||||
|
||||
`ProfileMdRenderer` subscribes to `DomainEvent::CacheRebuilt` and rewrites the cache-derived blocks (`style`, `identity`, `tooling`, `vetoes`, `goals`). The `connected-accounts` block remains owned by the provider sync path.
|
||||
|
||||
---
|
||||
|
||||
## Surface — prompt sections
|
||||
|
||||
Three sections in `agent/prompts/` cooperate:
|
||||
|
||||
- **`UserFilesSection`** — already-existing; injects `PROFILE.md` verbatim into the system prompt every turn.
|
||||
- **`UserProfileSection`** — repointed in Phase 4 to read `FacetCache::list_active()` (gated by `LearningConfig::use_cache_for_user_profile_section`, default true). Renders `- **{class}/{key}**: {value}` bullets.
|
||||
- **`MemoryAccessSection`** — new in Phase 4; static instruction biasing the agent to call `memory_recall` for entities, threads, prior decisions, recurring topics. Adds ~80 tokens to the system prompt.
|
||||
|
||||
The `MemoryAccessSection` covers the **contextual** half of personalization that the cache structurally cannot address: things that need to be retrieved when relevant (recurring people, prior threads) rather than always rendered.
|
||||
|
||||
---
|
||||
|
||||
## Surface — RPC controllers
|
||||
|
||||
Wired through the controller registry (`learning::schemas::all_learning_controller_schemas`):
|
||||
|
||||
| RPC | Purpose |
|
||||
|---|---|
|
||||
| `learning.list_facets { class }` | Enumerate the cache, optionally filtered by class. Returns active + provisional entries with provenance. |
|
||||
| `learning.get_facet { class, key }` | Single-entry lookup. |
|
||||
| `learning.update_facet { class, key, value }` | Overwrite the active value; auto-pins to prevent rebuild from clobbering. |
|
||||
| `learning.pin_facet { class, key }` | `user_state = Pinned`. Locks `Active` regardless of stability score. |
|
||||
| `learning.unpin_facet { class, key }` | `user_state = Auto`. |
|
||||
| `learning.forget_facet { class, key }` | `user_state = Forgotten`. Locks `Dropped`, blocks re-promotion. |
|
||||
| `learning.reset_cache {}` | Clears all `Auto` rows; preserves `Pinned`. Next rebuild repopulates from substrate. |
|
||||
| `learning.rebuild_cache {}` | Manual trigger for the stability rebuild. |
|
||||
| `learning.cache_stats {}` | `{ total, active, provisional, candidate, dropped, by_class }`. |
|
||||
|
||||
The agent itself acts as a conversational user-control surface: asked "what do you know about me?" it can call `list_facets` and cite `evidence_refs_json` for each entry; "forget that I prefer terse" calls `forget_facet("style", "verbosity")`.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
`LearningConfig` in `src/openhuman/config/schema/learning.rs`:
|
||||
|
||||
```rust
|
||||
pub struct LearningConfig {
|
||||
pub enabled: bool, // master switch
|
||||
pub reflection_enabled: bool,
|
||||
pub user_profile_enabled: bool, // legacy hook
|
||||
pub tool_tracking_enabled: bool, // substrate-only
|
||||
pub reflection_source: ReflectionSource, // Local | Cloud
|
||||
pub max_reflections_per_session: usize,
|
||||
pub min_turn_complexity: usize,
|
||||
pub chat_to_tree_enabled: bool, // pipe agent chat into tree
|
||||
pub stability_detector_enabled: bool,
|
||||
pub rebuild_interval_secs: u64, // default 1800 (30 min)
|
||||
pub use_cache_for_user_profile_section: bool, // route prompt-section reads through cache
|
||||
}
|
||||
```
|
||||
|
||||
The summarizer-side facet emission is gated by `LlmSummariserConfig::structured_facet_extraction` (default `true`) so production deployments can disable structured extraction independently of the rest of the learning subsystem.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
`DomainEvent::CacheRebuilt { added, evicted, kept, total_size, rebuilt_at }` is published after every successful rebuild. Subscribers can wire personalization metrics — for example, facet-in-prompt × positive-acceptance rate — to satisfy #566's "measurable personalization improvements" criterion.
|
||||
|
||||
`learning.cache_stats` returns the breakdown by state and class for ad-hoc inspection.
|
||||
|
||||
Tracing prefixes used by new flows (filter your log stream with these):
|
||||
|
||||
- `[learning::candidate]`
|
||||
- `[learning::extract::signature]`
|
||||
- `[learning::extract::heuristics]`
|
||||
- `[learning::extract::summary_facets]`
|
||||
- `[learning::stability_detector]`
|
||||
- `[learning::cache]`
|
||||
- `[learning::profile_md]`
|
||||
- `[archivist]` (chat-into-tree path)
|
||||
- `[memory::tree::ingest]` (`DocumentCanonicalized` emission)
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests live next to their modules (`*_tests.rs` siblings; consistent with the rest of the codebase). End-to-end coverage in `tests/learning_phase4_integration_test.rs`:
|
||||
|
||||
- Push candidates → `stability_detector.rebuild()` → expected `CacheRebuilt` event published
|
||||
- `ProfileMdRenderer` writes the expected managed blocks into `PROFILE.md`
|
||||
- `learning.pin_facet` keeps an entry `Active` despite weak evidence
|
||||
- `learning.forget_facet` removes an entry from `PROFILE.md` and blocks re-promotion
|
||||
- `learning.list_facets` returns the expected shape and class filter
|
||||
|
||||
Run targeted suites:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path Cargo.toml --lib learning::
|
||||
cargo test --manifest-path Cargo.toml --lib memory::store::unified::profile
|
||||
cargo test --manifest-path Cargo.toml --test learning_phase4_integration_test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future work
|
||||
|
||||
- **Validation loop** — track facet-in-prompt × positive-acceptance to calibrate cue-family weights from outcome signal rather than fixed defaults.
|
||||
- **Confirmation prompts** — for high-impact, low-confidence facets (Identity, Veto), surface to the user via the agent or subconscious before promoting to `Active`.
|
||||
- **Cross-corpus topic stitching** — once chat-as-tree-source has accumulated, surface unified hot topics back into the prompt as contextual hints alongside the cache.
|
||||
- **Per-channel persona facets** — tooling and style differ by channel; split when signals diverge meaningfully (e.g., terser in Slack DMs, more structured in email).
|
||||
- **Tool-suggestion telemetry** — distinct from the existing failure stats; needs new `ToolSuggestionAccepted`/`ToolSuggestionRejected` events before it can produce clean veto signal.
|
||||
- **Soft user confirmation in PROFILE.md** — render `Provisional` rows under a quieter sub-heading so the user can promote them by editing without waiting for accumulation.
|
||||
@@ -1,82 +0,0 @@
|
||||
# Contributor Rewards Automation
|
||||
|
||||
OpenHuman uses `.github/workflows/contributor-rewards.yml` to invite eligible
|
||||
contributors into the Discord and merch reward flow.
|
||||
|
||||
## Triggers
|
||||
|
||||
The workflow runs when:
|
||||
|
||||
- a pull request is merged and the PR author has no earlier merged PR in this
|
||||
repository;
|
||||
- a maintainer applies the `reward user` label to a pull request;
|
||||
- a maintainer applies the `reward user` label to an issue;
|
||||
- a maintainer runs the workflow manually to bootstrap the labels.
|
||||
|
||||
The workflow creates these labels if they do not already exist:
|
||||
|
||||
- `reward user` - maintainer-triggered reward invite;
|
||||
- `reward sent` - audit label added after the invite comment is posted.
|
||||
|
||||
## Idempotency
|
||||
|
||||
Each reward comment includes a hidden marker scoped to the GitHub login:
|
||||
|
||||
```html
|
||||
<!-- openhuman-contributor-reward:user=<login> -->
|
||||
```
|
||||
|
||||
Automatic first-merged-PR rewards are skipped when the same login already has a
|
||||
reward marker elsewhere in the repository. Maintainers can intentionally start
|
||||
the flow again by applying `reward user`, but the workflow still skips a target
|
||||
issue or PR that already contains the same marker.
|
||||
|
||||
Bot accounts are skipped.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure these repository variables under
|
||||
**Settings -> Secrets and variables -> Actions -> Variables**:
|
||||
|
||||
| Variable | Required | Purpose |
|
||||
| -------------------------------- | -------- | ------------------------------------------------------------------------ |
|
||||
| `CONTRIBUTOR_REWARD_DISCORD_URL` | No | Public Discord invite URL. Defaults to `https://discord.tinyhumans.ai/`. |
|
||||
| `CONTRIBUTOR_REWARD_MERCH_URL` | No | Public merch claim or redemption URL included in the comment. |
|
||||
| `CONTRIBUTOR_REWARD_MESSAGE` | No | Full custom comment body. Supports tokens listed below. |
|
||||
|
||||
`CONTRIBUTOR_REWARD_MESSAGE` can use these tokens:
|
||||
|
||||
- `{user}` - GitHub mention, for example `@octocat`;
|
||||
- `{login}` - raw GitHub login;
|
||||
- `{discord_url}` - configured Discord URL;
|
||||
- `{merch_url}` - configured merch URL or an empty string;
|
||||
- `{reason}` - trigger reason;
|
||||
- `{target_url}` - issue or PR URL.
|
||||
|
||||
Do not put private Discord invite mechanics, shipping forms, access tokens, or
|
||||
other secrets in repository variables used by this workflow. Anything rendered
|
||||
by the workflow is posted as a public GitHub comment.
|
||||
|
||||
## Security Model
|
||||
|
||||
The workflow uses `pull_request_target` so it can comment on pull requests from
|
||||
forks. It must not check out or execute pull request code. The current workflow
|
||||
only reads the GitHub event payload and calls GitHub APIs through
|
||||
`actions/github-script`.
|
||||
|
||||
Required permissions are limited to:
|
||||
|
||||
- `contents: read`;
|
||||
- `issues: write`;
|
||||
- `pull-requests: read`.
|
||||
|
||||
## Manual Operation
|
||||
|
||||
To reward a contributor manually:
|
||||
|
||||
1. Open the issue or pull request.
|
||||
2. Apply the `reward user` label.
|
||||
3. Wait for the workflow to post the reward comment and add `reward sent`.
|
||||
|
||||
If the labels do not exist yet, run **Actions -> Contributor Rewards -> Run
|
||||
workflow** once on `main`.
|
||||
@@ -1,183 +0,0 @@
|
||||
# Native OS Notification — Testing Status
|
||||
|
||||
Companion to `TAURI_CEF_FINDINGS_AND_CHANGES.md`.
|
||||
This file is a quick-reference checklist: what is done, what is still needed, and how to test.
|
||||
|
||||
---
|
||||
|
||||
## What Is Done
|
||||
|
||||
### tauri-cef (vendored submodule)
|
||||
|
||||
| Change | File |
|
||||
|--------|------|
|
||||
| `NotifyRenderProcessHandler` wired into `TauriApp::render_process_handler` | `vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs` |
|
||||
| `run_cef_helper_process()` uses `NotifyApp` (not `None`) | `vendor/tauri-cef/crates/tauri-runtime-cef/src/lib.rs` |
|
||||
| `notification::unregister(browser_id)` called on browser close | `vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs` |
|
||||
| Dispatch logs added (`[cef-notify] dispatch` / dropped) | `vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs` |
|
||||
| `on_context_created` install logs in runtime shim | `vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs` |
|
||||
| `on_context_created` install logs in helper shim | `vendor/tauri-cef/cef-helper/src/notification.rs` |
|
||||
| Debug markers: `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM`, `__OPENHUMAN_CEF_NOTIFICATION_ORIGIN` | `vendor/tauri-cef/cef-helper/src/notification.rs` |
|
||||
| Manual test entry point: `window.__openhumanFireNotification(title, opts)` | `vendor/tauri-cef/cef-helper/src/notification.rs` |
|
||||
| `ensure-tauri-cli.sh` reinstalls vendored CLI when tauri-cef sources are newer | `scripts/ensure-tauri-cli.sh` |
|
||||
|
||||
### tauri-plugin-notification (vendored to stop init-script conflict)
|
||||
|
||||
| Change | File |
|
||||
|--------|------|
|
||||
| Plugin vendored at `vendor/tauri-plugin-notification/` | `app/src-tauri/Cargo.toml` |
|
||||
| Plugin dependency switched from git rev to local path | `app/src-tauri/Cargo.toml` |
|
||||
| `.js_init_script(...)` call removed from plugin `init()` | `vendor/tauri-plugin-notification/src/lib.rs` |
|
||||
|
||||
**Why this mattered:** Without this change, the plugin injected a JS shim that forwarded `new Notification(...)` to `http://ipc.localhost/plugin:notification|notify`. That IPC always fails with 500 in third-party webviews (Slack), overwriting the CEF shim and blocking all notification delivery.
|
||||
|
||||
### openhuman-cursor app shell
|
||||
|
||||
| Change | File |
|
||||
|--------|------|
|
||||
| Default notification toggle set to `true` | `src/notification_settings/mod.rs` |
|
||||
| `OPENHUMAN_DISABLE_SLACK_SCANNER=1` env bypass for DevTools inspection | `src/webview_accounts/mod.rs` |
|
||||
| Platform-specific OS notification with click detection added | `src/webview_accounts/mod.rs` |
|
||||
| macOS: `mac-notification-sys` + `wait_for_click` + `std::thread::spawn` | `src/webview_accounts/mod.rs` |
|
||||
| Linux: `notify-rust` + `wait_for_action` + `std::thread::spawn` | `src/webview_accounts/mod.rs` |
|
||||
| Windows: fire-and-forget fallback via `NotificationExt` | `src/webview_accounts/mod.rs` |
|
||||
| `notification:click` Tauri event emitted with `{ account_id, provider }` | `src/webview_accounts/mod.rs` |
|
||||
| `[notify-click]` success logs promoted from `debug` to `info` | `src/webview_accounts/mod.rs` |
|
||||
| `mac-notification-sys = "0.6"` added to macOS dependencies | `app/src-tauri/Cargo.toml` |
|
||||
| `notify-rust` added to Linux dependencies | `app/src-tauri/Cargo.toml` |
|
||||
| `NotificationExt` import scoped to `#[cfg(all(feature = "cef", windows))]` | `src/webview_accounts/mod.rs` |
|
||||
| `tokio::task::spawn_blocking` replaced with `std::thread::spawn` (fixes tokio panic from CEF callback thread) | `src/webview_accounts/mod.rs` |
|
||||
| Scanner fallback: per-channel unread baseline, delta-based notification synthesis | `src/slack_scanner/mod.rs` |
|
||||
|
||||
---
|
||||
|
||||
## What Is Still Needed
|
||||
|
||||
### 1. ✅ Slack scanner registry re-enabled
|
||||
|
||||
**File:** `app/src-tauri/src/lib.rs`
|
||||
|
||||
This fix has already been applied in this PR. `ScannerRegistry` is now registered in the Tauri app
|
||||
state, so the scanner-driven fallback notification path is active.
|
||||
|
||||
The following was added to `lib.rs` inside `tauri::Builder::default()...manage(...)`:
|
||||
|
||||
```rust
|
||||
// already applied
|
||||
.manage(Arc::new(slack_scanner::ScannerRegistry::new()))
|
||||
```
|
||||
|
||||
With this change:
|
||||
- The scanner tracks per-channel unread counts
|
||||
- When a channel's unread count increases, the scanner synthesizes a native OS notification
|
||||
- This is the fallback path because Slack's embedded session does not call `new Notification(...)` for real incoming messages
|
||||
|
||||
### 2. Verify end-to-end with a real incoming Slack message
|
||||
|
||||
Once the scanner registry is registered:
|
||||
|
||||
1. Run the app: `cd app && pnpm dev:app`
|
||||
2. Open the Slack webview — wait for Slack to load fully
|
||||
3. Have someone send you a Slack message from another device
|
||||
4. Watch the log:
|
||||
```bash
|
||||
tail -f /tmp/openhuman-dev-app.log | grep --line-buffered "notify-cef\|notify-click\|scanner\|unread"
|
||||
```
|
||||
5. Expected log sequence:
|
||||
```
|
||||
[scanner] unread delta channel=... prev=N new=M
|
||||
[notify-cef][<account_id>] source=... tag=... title_chars=N body_chars=M
|
||||
```
|
||||
6. OS toast should appear
|
||||
7. Click the toast → expected:
|
||||
```
|
||||
[notify-click][<account_id>] clicked provider=slack
|
||||
```
|
||||
8. Slack webview should come into focus (frontend routes `notification:click` → `setActiveAccount` → `activate_main_window`)
|
||||
|
||||
### 3. Verify the CEF shim installs in Slack's page context
|
||||
|
||||
Before relying on real messages, confirm the helper shim is active via DevTools:
|
||||
|
||||
1. Open `brave://inspect`
|
||||
2. Find the Slack page target → click **Inspect**
|
||||
3. In Console, run:
|
||||
```js
|
||||
window.__OPENHUMAN_CEF_NOTIFICATION_SHIM // should be true
|
||||
window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN // should be the Slack URL
|
||||
```
|
||||
4. If both are present, the CEF helper shim installed correctly
|
||||
|
||||
### 4. Verify the manual helper trigger path end-to-end
|
||||
|
||||
With DevTools open on the Slack target:
|
||||
|
||||
```js
|
||||
window.__openhumanFireNotification("Slack CEF test", { body: "Manual trigger" })
|
||||
```
|
||||
|
||||
Expected log:
|
||||
```
|
||||
[cef-notify] dispatch browser_id=N source=Window title="Slack CEF test" origin=...
|
||||
[notify-cef][<account_id>] source=Window tag= silent=false title_chars=14 body_chars=13
|
||||
```
|
||||
|
||||
And an OS notification toast should appear. If no toast appears, the blocker is in `forward_native_notification` in `webview_accounts/mod.rs`.
|
||||
|
||||
### 5. Clean up debug instrumentation (post-verification)
|
||||
|
||||
Once notifications are working reliably, remove:
|
||||
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM` global marker
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN` global marker
|
||||
- `window.__openhumanFireNotification` manual trigger
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_CONSTRUCTOR` saved reference
|
||||
- `[cef-helper-notify]` `eprintln!` calls in `cef-helper/src/notification.rs` (or replace with proper `log::debug!`)
|
||||
|
||||
---
|
||||
|
||||
## How To Run The App Correctly
|
||||
|
||||
The app **must** be started with `dev:app`, not `tauri dev` directly:
|
||||
|
||||
```bash
|
||||
cd app && pnpm dev:app
|
||||
```
|
||||
|
||||
`dev:app` sets `CEF_PATH=$HOME/Library/Caches/tauri-cef` and ensures the vendored `cargo-tauri` is installed. Without it, the app panics at startup in `cef::library_loader` with `No such file or directory`.
|
||||
|
||||
Live log location:
|
||||
|
||||
```bash
|
||||
tail -f /tmp/openhuman-dev-app.log | grep --line-buffered "notify-cef\|notify-click\|scanner\|unread\|cef-notify"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Log Prefixes
|
||||
|
||||
| Prefix | Where | Meaning |
|
||||
|--------|-------|---------|
|
||||
| `[cef-helper-notify] on_context_created` | renderer subprocess | shim callback fired for a new JS context |
|
||||
| `[cef-helper-notify] installed shims` | renderer subprocess | shim installed in that context |
|
||||
| `[cef-helper-notify] execute` | renderer subprocess | `new Notification(...)` called by page JS |
|
||||
| `[cef-notify] dispatch` | browser process | notification IPC received from renderer, handler called |
|
||||
| `[cef-notify] dropped` | browser process | notification IPC received but no handler registered |
|
||||
| `[notify-cef][id]` | `webview_accounts` | notification payload reached app, OS toast being sent |
|
||||
| `[notify-click][id] clicked` | `webview_accounts` | user clicked OS toast, emitting `notification:click` |
|
||||
| `[webview-accounts] slack ScannerRegistry not in app state` | `webview_accounts` | scanner registry is missing — add `.manage(ScannerRegistry::new())` in `lib.rs` |
|
||||
|
||||
---
|
||||
|
||||
## Frontend Side (Already Wired, No Changes Needed)
|
||||
|
||||
`app/src/services/webviewAccountService.ts` already listens for `notification:click`:
|
||||
|
||||
```ts
|
||||
listen('notification:click', ({ payload }) => {
|
||||
dispatch(setActiveAccount(payload.account_id));
|
||||
invoke('activate_main_window');
|
||||
});
|
||||
```
|
||||
|
||||
No frontend changes are needed. The click routing will work once the Rust side emits the event.
|
||||
@@ -1,65 +0,0 @@
|
||||
# Operations and Monitoring
|
||||
|
||||
This document describes the monitoring strategy, alert policies, and incident response procedures for the OpenHuman backend.
|
||||
|
||||
## Uptime Monitoring
|
||||
|
||||
OpenHuman uses external uptime monitors to ensure that critical backend services are available and performing within acceptable thresholds.
|
||||
|
||||
### Critical Endpoints
|
||||
|
||||
The following endpoints are monitored for uptime:
|
||||
|
||||
| Environment | Endpoint | Purpose | Health Signal |
|
||||
|-------------|----------|---------|---------------|
|
||||
| **Production** | `https://api.tinyhumans.ai/health` | Public API liveness | HTTP 200 = healthy; HTTP 503 = one or more components in error state (alert) |
|
||||
| **Staging** | `https://staging-api.tinyhumans.ai/health` | Staging API liveness | HTTP 200 = healthy; HTTP 503 = one or more components in error state (alert) |
|
||||
|
||||
### Monitoring Providers
|
||||
|
||||
1. **Pingdom (Planned — not yet configured)**:
|
||||
- Planned to hit the `/health` endpoints every 1 minute from multiple regions (US, EU, Asia).
|
||||
- Alerts to be triggered after 2 consecutive failures.
|
||||
- No Pingdom configuration currently exists in this repository; no alerts will be sent until it is set up.
|
||||
|
||||
2. **GitHub Actions (Active)**:
|
||||
- Scheduled workflow (`.github/workflows/uptime-monitor.yml`) runs every 5 minutes.
|
||||
- Serves as an independent signal from the deployment pipeline.
|
||||
- On outage detection, automatically creates a labeled GitHub Issue (`bug`, `critical`, `ops`) titled **"CRITICAL: Backend Outage Detected"** and closes it when services recover, providing a durable incident log in the repository.
|
||||
|
||||
## Alerting and Escalation
|
||||
|
||||
### Alert Destinations
|
||||
|
||||
- **Slack/Discord**: Alerts are sent to the configured webhook (e.g. `#ops-alerts`) when the `ALERT_WEBHOOK_URL` GitHub secret is set. Set this secret in the repository settings pointing to your Slack incoming webhook or Discord server webhook URL. Alerts are skipped silently if the secret is not configured.
|
||||
- **Email** *(planned)*: Email alerting to `ops@tinyhumans.ai` is not yet wired into the automated workflow. Until an email integration is added, the `ALERT_WEBHOOK_URL` webhook is the only active notification channel.
|
||||
|
||||
### Escalation Path
|
||||
|
||||
1. **Level 1 (Immediate)**: Notification to `#ops-alerts`. On-call engineer acknowledges.
|
||||
2. **Level 2 (15 minutes)**: Page to the lead backend engineer.
|
||||
3. **Level 3 (30 minutes)**: Escalation to the CTO.
|
||||
|
||||
## Incident Response (Runbook)
|
||||
|
||||
When a monitor fires:
|
||||
|
||||
1. **Verify the outage**: Check the endpoint manually or via `curl -I <endpoint>`.
|
||||
2. **Check Cloud Status**: Check [DigitalOcean Status](https://status.digitalocean.com/) or other upstream providers.
|
||||
3. **Review Logs**: Access runtime logs via the DigitalOcean console or your container runtime (e.g. `docker logs <container>` or Kubernetes pod logs for containers sourced from `ghcr.io`).
|
||||
4. **Determine Scope**: Is it a total outage or degraded performance? Is it specific to a region?
|
||||
5. **Mitigation**: Restart the service via the cloud console or redeploy the last known healthy tag.
|
||||
6. **Communication**: Update the internal status and notify stakeholders if the outage exceeds 5 minutes.
|
||||
|
||||
## Testing Alerts
|
||||
|
||||
To test the GitHub Actions alert pipeline safely without causing a real outage:
|
||||
1. In `.github/workflows/uptime-monitor.yml`, temporarily change one endpoint URL to a non-existent path (e.g., `/health-test-trigger`) and trigger the workflow manually via `workflow_dispatch`.
|
||||
2. Verify that a GitHub Issue is created and an alert is received in the `#ops-alerts` channel.
|
||||
3. Revert the URL change and trigger the workflow again; verify the issue is closed and a recovery notification is sent.
|
||||
|
||||
To test the Pingdom monitor, use Pingdom's built-in test-alert feature from the dashboard rather than changing the monitored URL.
|
||||
|
||||
## Maintenance
|
||||
|
||||
During planned maintenance, monitors should be paused to avoid false positives. This is handled via the provider's "Maintenance Mode" or by disabling the GitHub Action temporarily.
|
||||
@@ -1,86 +0,0 @@
|
||||
# Portfolio Readiness
|
||||
|
||||
This note is for engineering review and portfolio handoff. It does not change
|
||||
the product surface or marketing README.
|
||||
|
||||
## What This Repo Demonstrates
|
||||
|
||||
- A large local-first desktop AI product with TypeScript UI, Rust/Tauri shell,
|
||||
and a Python/Rust-adjacent core surface.
|
||||
- Integration-heavy product architecture: local memory, account connectors,
|
||||
desktop shell commands, native tools, and testable app services.
|
||||
- Real validation breadth: TypeScript compile, ESLint, Vitest, and Rust
|
||||
mock-backed tests all run locally.
|
||||
|
||||
## Current Validation Evidence
|
||||
|
||||
Run from the repo root:
|
||||
|
||||
```bash
|
||||
pnpm run typecheck
|
||||
pnpm run lint
|
||||
pnpm --filter openhuman-app exec vitest run --config test/vitest.config.ts src/pages/__tests__/Conversations.test.tsx src/pages/__tests__/Conversations.render.test.tsx src/components/settings/panels/__tests__/RecoveryPhrasePanel.test.tsx
|
||||
pnpm --filter openhuman-app exec prettier --check .
|
||||
cargo fmt --manifest-path Cargo.toml --all --check
|
||||
cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check
|
||||
cargo check --manifest-path app/src-tauri/Cargo.toml
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Latest clean-branch portfolio-readiness run:
|
||||
|
||||
- `pnpm run typecheck`: passed.
|
||||
- `pnpm run lint`: passed with 35 warnings. The remaining warning family is
|
||||
React compiler `set-state-in-effect`.
|
||||
- Focused Vitest coverage for the touched React areas passed: `3` files and
|
||||
- `24` tests across Conversations and Recovery Phrase panel tests.
|
||||
- `pnpm --filter openhuman-app exec prettier --check .`: passed from `app/`.
|
||||
- Rust format checks for the root and Tauri manifests passed.
|
||||
- `cargo check --manifest-path app/src-tauri/Cargo.toml`: passed with existing
|
||||
Rust warnings.
|
||||
- `git diff --check`: passed.
|
||||
|
||||
## Cleanup Performed
|
||||
|
||||
- Moved mnemonic/recovery-phrase mode resets into the explicit mode switch
|
||||
handlers.
|
||||
- Removed dead sidebar label reset state now that conversation labels use a
|
||||
fixed tab model.
|
||||
- Ignored local `.cocoindex_code/` index output so code-index experiments do
|
||||
not dirty the repo.
|
||||
- Recorded validation evidence in `CODEX_WORKPAD.md`.
|
||||
|
||||
## Remaining Presentation Debt
|
||||
|
||||
- The lint output is not presentation-clean yet because 35 React compiler
|
||||
warnings remain.
|
||||
- Most warnings are synchronous state updates inside effects. Some may be
|
||||
harmless legacy patterns, but they should be either refactored or explicitly
|
||||
accepted as a policy before this repo is used as a polished flagship example.
|
||||
- Vitest currently emits repeated Node `localStorage is not available` warnings;
|
||||
tests pass, but the environment warning should be silenced or documented.
|
||||
|
||||
## Public Claim Boundary
|
||||
|
||||
Safe to claim:
|
||||
|
||||
- The repository has broad local validation across TypeScript, lint, JS tests,
|
||||
and Rust tests.
|
||||
- The current cleanup reduced generic lint noise without changing product
|
||||
behavior.
|
||||
|
||||
Do not claim yet:
|
||||
|
||||
- "Lint-clean" or "warning-free."
|
||||
- Full UI runtime readiness across every Tauri/desktop flow.
|
||||
- That the remaining React compiler warnings have been reviewed and accepted.
|
||||
|
||||
## Next Slice
|
||||
|
||||
Create a narrow lint-policy slice:
|
||||
|
||||
1. Pick one warning family, starting with `react-hooks/set-state-in-effect`.
|
||||
2. Classify warnings into real refactors vs accepted legacy patterns.
|
||||
3. Fix the highest-risk components first.
|
||||
4. Keep `pnpm run typecheck`, `pnpm run lint`, and the relevant Vitest tests
|
||||
green after each group.
|
||||
@@ -1,96 +0,0 @@
|
||||
# Prompt Injection Guard
|
||||
|
||||
This document describes the end-to-end prompt injection detection and enforcement flow added in the OpenHuman core and app.
|
||||
|
||||
## Scope
|
||||
|
||||
- Backend enforcement is authoritative.
|
||||
- Frontend checks are advisory UX only.
|
||||
- Guarding runs before model inference or agent/tool loop execution for user-supplied prompts.
|
||||
|
||||
## Detection Layers
|
||||
|
||||
Detection is implemented in `src/openhuman/prompt_injection/` with layered analysis:
|
||||
|
||||
1. Normalization
|
||||
- Lowercasing and whitespace collapse.
|
||||
- Obfuscation cleanup (zero-width chars, punctuation noise, basic leetspeak substitutions).
|
||||
- Compact-string pass for spaced-out attacks (`i g n o r e ...`).
|
||||
|
||||
2. Pattern rules
|
||||
- Instruction override patterns (`ignore/disregard/forget previous instructions`).
|
||||
- Role hijack patterns (`you are now`, `developer mode`, `jailbreak`).
|
||||
- Prompt/system exfiltration patterns (`reveal system prompt`, `show developer instructions`).
|
||||
- Secret exfiltration patterns (`api key`, `token`, `password`, etc.).
|
||||
- Unsafe tool coercion patterns.
|
||||
|
||||
3. Optional classifier
|
||||
- Enabled with `OPENHUMAN_PROMPT_INJECTION_CLASSIFIER=heuristic`.
|
||||
- Adds score for suspicious combinations (obfuscation + override/exfiltration intent).
|
||||
|
||||
## Verdict Contract
|
||||
|
||||
Detector returns:
|
||||
|
||||
- `verdict`: `allow | block | review`
|
||||
- `score`: normalized `0.0..1.0`
|
||||
- `reasons`: stable reason codes/messages
|
||||
- `action`: enforcement action (`allow`, `block`, `review_blocked`)
|
||||
|
||||
Current threshold policy:
|
||||
|
||||
- `score >= 0.70` -> `block`
|
||||
- `0.45 <= score < 0.70` -> `review`
|
||||
- `< 0.45` -> `allow`
|
||||
|
||||
## Enforcement Flow
|
||||
|
||||
Server-side checks are wired before LLM/tool execution in:
|
||||
|
||||
- `src/openhuman/channels/providers/web.rs` (`start_chat`)
|
||||
- `src/openhuman/local_ai/ops.rs` (`agent_chat`, `agent_chat_simple`, `local_ai_chat`, `local_ai_prompt`, `local_ai_vision_prompt`, `local_ai_summarize`)
|
||||
- `src/openhuman/agent/harness/session/runtime.rs` (`Agent::run_single`)
|
||||
- `src/openhuman/agent/bus.rs` (`agent.run_turn` native bus handler)
|
||||
|
||||
If action is `block` or `review_blocked`, request processing is stopped and no prompt is sent to provider/tool loop.
|
||||
|
||||
## Frontend Advisory UX
|
||||
|
||||
- Advisory pre-submit validation in `app/src/chat/promptInjectionGuard.ts`.
|
||||
- Composer integration in `app/src/pages/Conversations.tsx`.
|
||||
- `block` verdict: advisory warning is shown client-side; backend remains authoritative for final enforcement.
|
||||
- `review` verdict: advisory warning shown; backend still enforces final decision.
|
||||
|
||||
## Logging and Privacy
|
||||
|
||||
Each backend decision logs:
|
||||
|
||||
- `request_id`
|
||||
- `user_id`
|
||||
- `session_id`
|
||||
- `source`
|
||||
- `verdict`
|
||||
- `score`
|
||||
- `reasons` (codes)
|
||||
- `action`
|
||||
- `prompt_hash` (SHA-256)
|
||||
- `prompt_chars`
|
||||
|
||||
Raw prompt text is not logged by this guard.
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit tests:
|
||||
- `src/openhuman/prompt_injection/tests.rs`
|
||||
- `src/openhuman/channels/providers/web_tests.rs`
|
||||
- `src/openhuman/local_ai/ops_tests.rs`
|
||||
- `app/src/chat/__tests__/promptInjectionGuard.test.ts`
|
||||
- Integration test:
|
||||
- `tests/json_rpc_e2e.rs` (`json_rpc_prompt_injection_is_rejected_before_model_call`)
|
||||
|
||||
## Extending Rules
|
||||
|
||||
1. Add/adjust regex rules in `src/openhuman/prompt_injection/detector.rs` (`DETECTION_RULES`).
|
||||
2. Keep reason codes stable for observability and tests.
|
||||
3. Add unit tests for both positive and negative cases (including obfuscated variants).
|
||||
4. If introducing new classifier logic, gate it behind config/env and ensure deterministic fallback behavior when disabled.
|
||||
@@ -1,213 +0,0 @@
|
||||
# OpenHuman Security Audit — Architecture & Data Flow Analysis
|
||||
|
||||
> Date: 2026-05-21
|
||||
> Author: JAYcodr (fork analysis, not an official audit)
|
||||
> Scope: Architecture overview, trust boundaries, credential flow, attack surface
|
||||
|
||||
---
|
||||
|
||||
## 1. System Overview
|
||||
|
||||
OpenHuman is a desktop AI assistant with a **Rust core** running in-process inside a Tauri desktop host, and a **React/TypeScript frontend**. Communication between frontend and core happens via two channels:
|
||||
|
||||
| Channel | Protocol | Auth |
|
||||
|---|---|---|
|
||||
| Primary | Socket.IO (bidirectional streaming) | Session-baked connection auth |
|
||||
| Secondary | HTTP JSON-RPC | Basic Auth (`WWW-Authenticate` realm) |
|
||||
|
||||
**No sidecar binary** — core runs as a tokio task inside the Tauri process (`core_process.rs`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Module Map
|
||||
|
||||
### Core (`src/openhuman/`) — 66 domains
|
||||
|
||||
| Category | Domains |
|
||||
|---|---|
|
||||
| Agent | `agent`, `agent_experience`, `agent_tool_policy` |
|
||||
| Memory | `memory` (stm_recall, docs), `embeddings`, `learning`, `workspace` |
|
||||
| Skills | `skills` (metadata-only), `mcp_client`, `mcp_clients`, `mcp_server`, `composio` |
|
||||
| Channels | `channels` (dispatch), `telegram`, `discord`, `whatsapp_data`, `webview_accounts` |
|
||||
| Infrastructure | `http_host`, `socket` (Socket.IO server), `runtime_node`, `runtime_python` |
|
||||
| Business Logic | `billing`, `credentials`, `vault`, `encryption`, `notifications`, `webhooks`, `approval`, `cron`, `meet`, `meet_agent`, `team`, `threads`, `todos` |
|
||||
| UI-adjacent | `accessibility`, `autocomplete`, `screen_intelligence`, `voice` |
|
||||
| Other | `config`, `health`, `heartbeat`, `doctor`, `migration`, `update`, `security`, `prompt_injection` |
|
||||
|
||||
### Transport (`src/core/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/core/jsonrpc.rs` | JSON-RPC over HTTP, method dispatch |
|
||||
| `src/core/socketio.rs` | Socket.IO server, `WebChannelEvent` struct for streaming |
|
||||
| `src/core/auth.rs` | HTTP Basic Auth handler |
|
||||
| `src/openhuman/http_host/rpc.rs` | JSON-RPC endpoint (`list()` function) |
|
||||
| `src/openhuman/http_host/auth.rs` | `WWW-Authenticate` header, `unauthorized_response()` |
|
||||
|
||||
### Event Bus (`src/core/event_bus/`)
|
||||
|
||||
Typed pub/sub + in-process typed request/response:
|
||||
|
||||
```text
|
||||
publish_global(DomainEvent) → fire-and-forget broadcast
|
||||
register_native_global(method, handler) → one-to-one typed dispatch
|
||||
request_native_global(method, req) → call and wait for response
|
||||
```
|
||||
|
||||
**Domain events:** `agent`, `memory`, `channel`, `skill`, `tool`, `webhook`, `mcp_client`, `system`, `approval`, `cron`, `triage`
|
||||
|
||||
---
|
||||
|
||||
## 3. Credential & Token Flows
|
||||
|
||||
### Core RPC Auth
|
||||
|
||||
- HTTP JSON-RPC protected by **HTTP Basic Auth**
|
||||
- Realm: `"OpenHuman Hosted Directory"`
|
||||
- Per-launch bearer token, transported differently per deployment shape:
|
||||
- **Desktop / Tauri shell**: bearer is generated in `CoreProcessHandle::new()` and held in-memory as `CoreProcessHandle.rpc_token: Arc<String>`, then handed to the embedded server via an internal in-memory handle (`run_server_embedded_with_ready(rpc_token: Some(_))`). **Not** published to the process environment.
|
||||
- **Standalone CLI / Docker / cloud**: bearer is read from the `OPENHUMAN_CORE_TOKEN` env var (via `init_rpc_token`) or from the `{workspace}/core.token` file. This is the operator-supplied configuration surface for those deployments and is intentional.
|
||||
- Frontend obtains bearer via `invoke('core_rpc_token')` Tauri command
|
||||
|
||||
### Stored Credentials
|
||||
|
||||
- `credentials` domain manages credential storage
|
||||
- `encryption` domain handles at-rest encryption
|
||||
- `auth-profiles.json` — auth data referenced by `settings.ai.apiKeysEncrypted` i18n key
|
||||
|
||||
### MCP Server Auth
|
||||
|
||||
- Composio API key stored via `settings.composio.apiKeyStoredPlaceholder`
|
||||
- MCP client config (Claude Desktop, Cursor, Codex, Zed) generated in settings panel
|
||||
|
||||
---
|
||||
|
||||
## 4. Trust Boundaries & Attack Surface
|
||||
|
||||
### Boundary 1: External Channels (Telegram, Discord, WhatsApp, etc.)
|
||||
|
||||
- Inbound messages from third-party messaging platforms flow through `channels/runtime/dispatch.rs`
|
||||
- Each provider scanner runs as native CDP/scraping — **no JS injection** in migrated providers
|
||||
- `ChannelInboundMessage` event published to event bus
|
||||
|
||||
**Risk:** Third-party message content is untrusted. Prompt injection possible if message content is rendered or echoed without sanitization. The `prompt_injection` domain exists as a guard.
|
||||
|
||||
### Boundary 2: MCP Tool Bridge (`mcp_client/`, `mcp_clients/`)
|
||||
|
||||
- External MCP servers connect via stdio or HTTP
|
||||
- Tools exposed through `tool_registry`
|
||||
- `McpClientToolExecuted` events published
|
||||
|
||||
**Risk:** MCP tools are external services. Tool output flows back into agent context. No obvious output sanitization in the tool execution path.
|
||||
|
||||
### Boundary 3: Skill Runtime (Removed)
|
||||
|
||||
- QuickJS / `rquickjs` runtime was **removed** (PR #1061)
|
||||
- `src/openhuman/skills/` is now metadata-only
|
||||
- No dynamic code execution from skill packages
|
||||
|
||||
**Risk:** Significantly reduced vs. prior architecture.
|
||||
|
||||
### Boundary 4: Local File System Access
|
||||
|
||||
- `workspace`, `vault`, `webview_accounts` domains have file system access
|
||||
- `screen_intelligence`, `accessibility` domains capture screen content
|
||||
- Memory stored via `memory` domain
|
||||
|
||||
**Risk:** Screen capture and file access are high-privilege operations. Controlled by macOS permissions (Accessibility, Screen Recording).
|
||||
|
||||
### Boundary 5: MCP Server Config File
|
||||
|
||||
- Settings panel generates `~/.config/openhuman/mcp.json` for external MCP clients
|
||||
- Config written via `settings.mcpServer.openConfigFile` / `writeFile`
|
||||
- Path exposed via `settings.mcpServer.configFilePath`
|
||||
|
||||
**Risk:** If `mcp.json` is world-readable, token theft possible. Worth auditing file permissions on the config directory.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flows
|
||||
|
||||
### Agent Turn (primary AI interaction)
|
||||
|
||||
```text
|
||||
External message → channels/runtime/dispatch.rs
|
||||
→ request_native_global("agent.run_turn", AgentTurnRequest)
|
||||
→ agent/bus.rs: run_tool_call_loop()
|
||||
→ tool_registry → SkillExecution events
|
||||
→ on_delta mpsc channel → WebChannelEvent (Socket.IO)
|
||||
→ frontend (SocketIOMCPTransportImpl)
|
||||
```
|
||||
|
||||
### Memory Recall
|
||||
|
||||
```text
|
||||
Tool call: memory.recall → memory/stm_recall/recall.rs: stm_recall()
|
||||
→ MemoryRecalled event on event bus
|
||||
→ consumed by skill/mcp_client subscribers
|
||||
```
|
||||
|
||||
### Credential Setup
|
||||
|
||||
```text
|
||||
Frontend settings → core RPC (JSON-RPC over HTTP + Basic Auth)
|
||||
→ credentials domain → encryption domain
|
||||
→ stored to auth-profiles.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Security Observations (Not Exhaustive)
|
||||
|
||||
### Areas Worth Auditing
|
||||
|
||||
1. **Prompt injection from channel messages** — `prompt_injection` domain exists; need to verify it's applied to all channel inbound paths and not just chat UI
|
||||
2. **MCP tool output sanitization** — external MCP tool output flows into agent context without obvious filtering
|
||||
3. **Config directory permissions** — `~/.config/openhuman/` and `mcp.json` permission model not reviewed
|
||||
4. **Credential encryption** — `encryption` domain used for at-rest encryption; key management model unclear
|
||||
5. **WebView CSP** — embedded webviews (Telegram, Discord, etc.) loaded under CEF — need to verify CSP headers and iframe restrictions
|
||||
6. ~~**`OPENHUMAN_CORE_TOKEN` in process env** — bearer token in env var; visible via `/proc/self/environ` on Linux or process inspection on macOS~~ — **resolved**: in-process core now receives the bearer via an in-memory handoff (`run_server_embedded_with_ready(rpc_token: Some(_))`); the env-var crossing has been removed from the Tauri shell's spawn path. CLI / docker / cloud env-as-config remains the intended operator surface for those deployments.
|
||||
7. **No rate limiting observed** on HTTP JSON-RPC endpoint
|
||||
|
||||
### Positive Signals
|
||||
|
||||
- QuickJS skill runtime removed — large attack surface eliminated
|
||||
- CEF webviews for migrated providers have **zero injected JS** — good isolation
|
||||
- MCP server stdio transport provides sandboxing for external tools
|
||||
- `security` domain exists — may contain hardening measures not reviewed here
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommended Next Steps (for Maintainers)
|
||||
|
||||
- [ ] Audit `prompt_injection` domain coverage — is it applied to all channel inbound paths?
|
||||
- [ ] Document `encryption` domain key management
|
||||
- [ ] Check file permissions on `~/.config/openhuman/`
|
||||
- [ ] Add rate limiting to HTTP JSON-RPC endpoint
|
||||
- [ ] Document MCP tool output handling expectations
|
||||
- [x] Review `OPENHUMAN_CORE_TOKEN` lifetime and exposure scope — in-process core now uses in-memory handoff (no env crossing); CLI/docker/cloud retain env-as-config
|
||||
|
||||
---
|
||||
|
||||
## 8. RPC Method Reference
|
||||
|
||||
JSON-RPC methods follow `domain_operation` pattern:
|
||||
|
||||
```text
|
||||
memory_recall_memories
|
||||
memory_recall_context
|
||||
thread_turn_state_lifecycle
|
||||
wallet_setup_round_trips_status
|
||||
tool_registry_lists_and_gets_entries
|
||||
```
|
||||
|
||||
Native (event bus) methods:
|
||||
|
||||
```text
|
||||
agent.run_turn → agent/bus.rs
|
||||
memory.sync → memory/bus.rs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This document is an independent analysis, not an official security assessment.*
|
||||
@@ -1,505 +0,0 @@
|
||||
# Tauri CEF Notification Findings And Changes
|
||||
|
||||
## Scope
|
||||
|
||||
This note summarizes:
|
||||
|
||||
- what was found in `tauri-cef`
|
||||
- what was missing for webview notification permission and delivery
|
||||
- what was changed in `tauri-cef`
|
||||
- what was changed in `openhuman-cursor`
|
||||
- how the setup was debugged and verified
|
||||
|
||||
Relevant codebases:
|
||||
|
||||
- `(external clone, not in this repo)` — standalone `tauri-cef` repo
|
||||
- `.` (repo root)
|
||||
- vendored submodule: `app/src-tauri/vendor/tauri-cef`
|
||||
|
||||
## Initial Findings In `tauri-cef`
|
||||
|
||||
### 1. Browser-side permission acceptance existed
|
||||
|
||||
`tauri-runtime-cef` already had browser-process logic that accepted Chromium notification permission requests:
|
||||
|
||||
- `crates/tauri-runtime-cef/src/permissions.rs`
|
||||
- `crates/tauri-runtime-cef/src/cef_impl.rs`
|
||||
|
||||
This meant CEF could accept `CEF_PERMISSION_TYPE_NOTIFICATIONS`.
|
||||
|
||||
### 2. Renderer-side granted state was not wired into the real runtime path
|
||||
|
||||
Slack and similar apps do not rely only on the browser permission callback. They also inspect browser-visible JavaScript state:
|
||||
|
||||
- `Notification.permission`
|
||||
- `Notification.requestPermission()`
|
||||
- `navigator.permissions.query({ name: "notifications" })`
|
||||
|
||||
A renderer-side shim for this behavior existed only in:
|
||||
|
||||
- `cef-helper/src/notification.rs`
|
||||
|
||||
But the actual runtime app path used by `tauri-runtime-cef` did not attach that renderer process handler in the default `TauriApp` path. As a result, web apps could still observe notification state as `"prompt"` instead of `"granted"`.
|
||||
|
||||
### 3. Notification permission looked partially implemented, not end-to-end
|
||||
|
||||
There was a notification IPC path and registry in `tauri-runtime-cef`, but the setup was incomplete unless the embedder registered a handler:
|
||||
|
||||
- browser process received notification IPC
|
||||
- runtime exposed `notification::register(...)`
|
||||
- without an app-side registration, notifications could still be dropped
|
||||
|
||||
### 4. The old behavior was not sufficient for Slack
|
||||
|
||||
In practice, Slack kept behaving as if notifications still needed to be enabled because the renderer-visible granted state was not consistently exposed on the real runtime path.
|
||||
|
||||
### 5. Additional root cause found during live debugging
|
||||
|
||||
Later live DevTools inspection revealed a second, more concrete failure mode in `openhuman-cursor`:
|
||||
|
||||
- `tauri-plugin-notification` injects its own JavaScript init script into every Tauri webview
|
||||
- that init script overwrote `window.Notification`
|
||||
- the replacement implementation forwarded notification calls to:
|
||||
- `plugin:notification|notify`
|
||||
- over Tauri IPC at `http://ipc.localhost/...`
|
||||
|
||||
For external web pages such as Slack, this is the wrong path:
|
||||
|
||||
- the page is not supposed to use Tauri IPC as its notification transport
|
||||
- the page should stay on the native CEF notification path
|
||||
- when the plugin shim won, calls failed with `500` and console errors such as:
|
||||
- `POST http://ipc.localhost/plugin%3Anotification%7Cnotify 500`
|
||||
- `Origin header is not a valid URL`
|
||||
|
||||
That meant the plugin’s JS shim was effectively undoing the CEF notification interception fix inside external webviews.
|
||||
|
||||
## Changes Made In `tauri-cef`
|
||||
|
||||
These changes were made in the standalone `tauri-cef` repo and pushed there first.
|
||||
|
||||
### 1. Moved notification permission shims into the real runtime path
|
||||
|
||||
Renderer-side notification permission shims were added to `tauri-runtime-cef` so they run on the real Tauri CEF runtime path instead of only in the standalone helper.
|
||||
|
||||
Files involved:
|
||||
|
||||
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/notification.rs`
|
||||
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/cef_impl.rs`
|
||||
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/lib.rs`
|
||||
|
||||
Behavior provided by the shim:
|
||||
|
||||
- `Notification.permission` resolves to granted
|
||||
- `Notification.requestPermission()` resolves to granted
|
||||
- `navigator.permissions.query({ name: "notifications" })` reports granted
|
||||
- notification calls are forwarded through the CEF runtime notification path
|
||||
|
||||
### 2. Hooked the render process handler into `TauriApp`
|
||||
|
||||
The runtime app path now installs the render process handler needed for the notification shim.
|
||||
|
||||
### 3. Started the helper process with a real app object
|
||||
|
||||
`run_cef_helper_process()` was updated to launch CEF with an app object instead of `None`, so the notification renderer setup is available consistently.
|
||||
|
||||
### 4. Added notification handler cleanup on browser close
|
||||
|
||||
In the vendored `tauri-cef` inside `openhuman-cursor`, an additional lifecycle cleanup was added:
|
||||
|
||||
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs`
|
||||
|
||||
Added on browser close:
|
||||
|
||||
```rust
|
||||
crate::notification::unregister(browser_id);
|
||||
```
|
||||
|
||||
This prevents stale notification handlers from remaining registered after a webview is destroyed.
|
||||
|
||||
### 5. Standalone `tauri-cef` commit
|
||||
|
||||
The standalone `tauri-cef` repo was updated and pushed with:
|
||||
|
||||
- branch: `feat/cef`
|
||||
- commit: `c8ece7c78`
|
||||
- message: `Fix CEF notification permission shims`
|
||||
|
||||
The `openhuman-cursor` vendored submodule was then updated to the corresponding submodule commit:
|
||||
|
||||
- `c8ece7c784b8cdff16dc552f6892a0c9982ef1ba`
|
||||
|
||||
## Changes Made In `openhuman-cursor`
|
||||
|
||||
### 1. Enabled shell-side webview notifications by default
|
||||
|
||||
File:
|
||||
|
||||
- `app/src-tauri/src/notification_settings/mod.rs`
|
||||
|
||||
Change:
|
||||
|
||||
- default notification toggle changed to enabled:
|
||||
|
||||
```rust
|
||||
AtomicBool::new(true)
|
||||
```
|
||||
|
||||
This ensures notifications are not disabled by default at the app layer.
|
||||
|
||||
### 2. Added a Slack debugging bypass for internal CDP attachment
|
||||
|
||||
File:
|
||||
|
||||
- `app/src-tauri/src/webview_accounts/mod.rs`
|
||||
|
||||
Environment flag added:
|
||||
|
||||
- `OPENHUMAN_DISABLE_SLACK_SCANNER=1`
|
||||
|
||||
When set for Slack accounts, the app now:
|
||||
|
||||
- skips Slack scanner startup
|
||||
- skips the long-lived CDP session for Slack
|
||||
- loads the real Slack URL directly instead of going through the placeholder `data:` path used for the CDP bootstrap flow
|
||||
|
||||
Expected logs:
|
||||
|
||||
- `[webview-accounts] skipping CDP session via OPENHUMAN_DISABLE_SLACK_SCANNER ...`
|
||||
- `[webview-accounts] slack scanner disabled via OPENHUMAN_DISABLE_SLACK_SCANNER ...`
|
||||
|
||||
This was added only to make manual DevTools inspection possible without the app attaching to the same Slack target.
|
||||
|
||||
### 3. Vendored `tauri-plugin-notification` and removed its JS init script
|
||||
|
||||
To stop `window.Notification` from being overwritten inside external webviews, `tauri-plugin-notification` was vendored into the repo and switched from a git dependency to a path dependency.
|
||||
|
||||
New vendored path:
|
||||
|
||||
- `app/src-tauri/vendor/tauri-plugin-notification`
|
||||
|
||||
Two changes were made:
|
||||
|
||||
1. The plugin dependency in:
|
||||
- `app/src-tauri/Cargo.toml`
|
||||
now points to the vendored path.
|
||||
|
||||
2. The plugin init function in:
|
||||
- `app/src-tauri/vendor/tauri-plugin-notification/src/lib.rs`
|
||||
no longer calls:
|
||||
|
||||
```rust
|
||||
.js_init_script(...)
|
||||
```
|
||||
|
||||
This keeps the Rust-side desktop notification API available through `NotificationExt`, but stops the plugin from globally replacing `window.Notification` in embedded external pages.
|
||||
|
||||
The result is:
|
||||
|
||||
- app Rust code can still fire native notifications
|
||||
- external webviews like Slack no longer get forced onto the Tauri IPC notification path
|
||||
- the native CEF notification shim remains authoritative inside the external webview
|
||||
|
||||
## Verification Performed
|
||||
|
||||
### Build verification
|
||||
|
||||
In `app/src-tauri`:
|
||||
|
||||
- `cargo fmt` passed
|
||||
- `cargo check --features cef` passed for the main notification changes
|
||||
- `cargo check --features cef` also passed after vendoring `tauri-plugin-notification` and removing its JS init script
|
||||
|
||||
One later `cargo check` attempt for the Slack DevTools bypass work was blocked by another running Cargo process holding the build lock, but the changes were localized and formatting completed.
|
||||
|
||||
### Runtime verification
|
||||
|
||||
The app was run in dev mode and the CEF DevTools target list was checked through:
|
||||
|
||||
- `http://localhost:9222/json/list`
|
||||
- `http://127.0.0.1:9222/json/list`
|
||||
- earlier in one run, `http://[::1]:9222/json/list`
|
||||
|
||||
Observed targets included:
|
||||
|
||||
- Slack page target
|
||||
- Slack service worker target
|
||||
- OpenHuman page target
|
||||
|
||||
This confirmed:
|
||||
|
||||
- Slack was running inside the CEF webview
|
||||
- CEF remote debugging was active
|
||||
|
||||
### Slack scanner contention diagnosis
|
||||
|
||||
At first, the main reason DevTools inspection failed for Slack was that the app itself was attaching to the Slack target over CDP:
|
||||
|
||||
- the Slack scanner auto-attached
|
||||
- the long-lived per-account CDP session also attached
|
||||
|
||||
This was why manual DevTools sessions disconnected even when the target existed.
|
||||
|
||||
The debugging bypass confirmed this by producing logs that both internal attachment paths were skipped.
|
||||
|
||||
## Final DevTools Findings
|
||||
|
||||
After the Slack-specific CDP paths were disabled, DevTools could still disconnect even for the `OpenHuman` page target. That showed the remaining issue was not Slack-specific.
|
||||
|
||||
Live verification showed:
|
||||
|
||||
- the active backend was on `127.0.0.1:9222`
|
||||
- `localhost` was inconsistent during checks
|
||||
- Brave already had multiple established connections to `127.0.0.1:9222`
|
||||
- the running `OpenHuman` app was listening on that port
|
||||
|
||||
Most likely explanation:
|
||||
|
||||
- multiple existing DevTools frontend sessions in Brave were already connected
|
||||
- reopening new inspector tabs caused connection churn
|
||||
- `127.0.0.1` was more reliable than `localhost`
|
||||
|
||||
Recommended DevTools usage:
|
||||
|
||||
1. Close all existing DevTools tabs for `localhost:9222` and `127.0.0.1:9222`.
|
||||
2. Reopen only one inspector at a time.
|
||||
3. Prefer the exact `127.0.0.1` websocket host advertised by `/json/list`.
|
||||
|
||||
## Later Runtime And Helper Findings
|
||||
|
||||
### 1. The real helper bundle was stale at first
|
||||
|
||||
During live verification, the main app binary contained the new notification instrumentation but the bundled macOS helper apps did not.
|
||||
|
||||
That turned out to be a packaging issue:
|
||||
|
||||
- the installed `cargo-tauri` binary was stale
|
||||
- helper executables are bundled by the CLI/bundler
|
||||
- rebuilding the app alone was not enough to refresh the helper binaries
|
||||
|
||||
To prevent this from recurring, the local CLI bootstrap script was updated:
|
||||
|
||||
- `scripts/ensure-tauri-cli.sh`
|
||||
|
||||
It now reinstalls vendored `cargo-tauri` when vendored `tauri-cef` sources are newer than the installed CLI binary.
|
||||
|
||||
### 2. The live macOS renderer path uses `cef-helper`
|
||||
|
||||
An important debugging detail was that the actual live renderer helper on macOS was using:
|
||||
|
||||
- `app/src-tauri/vendor/tauri-cef/cef-helper/src/notification.rs`
|
||||
|
||||
and not only the runtime-side notification file in:
|
||||
|
||||
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs`
|
||||
|
||||
So the helper-side shim was instrumented directly with:
|
||||
|
||||
- install logs
|
||||
- execution logs
|
||||
- debug markers
|
||||
- manual test entry points
|
||||
|
||||
Helper-side logs added:
|
||||
|
||||
- `[cef-helper-notify] on_context_created ...`
|
||||
- `[cef-helper-notify] installed shims ...`
|
||||
- `[cef-helper-notify] execute source=...`
|
||||
|
||||
Helper-side debug markers added:
|
||||
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM`
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN`
|
||||
- `Notification.__openhuman_cef`
|
||||
|
||||
Manual helper entry points added:
|
||||
|
||||
- `window.__openhumanFireNotification(title, options)`
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_CONSTRUCTOR`
|
||||
|
||||
### 3. Helper shim installation was verified in the real Slack page
|
||||
|
||||
After rebuilding the helper bundle, live DevTools checks showed:
|
||||
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_SHIM === true`
|
||||
- `window.__OPENHUMAN_CEF_NOTIFICATION_ORIGIN` pointing at the Slack page URL
|
||||
|
||||
This proved that the CEF helper render shim was actually installing in the Slack page context.
|
||||
|
||||
### 4. Manual helper-triggered notifications work end-to-end
|
||||
|
||||
The following manual trigger was verified to reach the app:
|
||||
|
||||
```js
|
||||
window.__openhumanFireNotification("Slack CEF test", {
|
||||
body: "Manual helper path"
|
||||
})
|
||||
```
|
||||
|
||||
Observed runtime logs:
|
||||
|
||||
- `[cef-notify] ipc ...`
|
||||
- `[cef-notify] dispatch ...`
|
||||
- `[notify-cef][...] ...`
|
||||
|
||||
This proved that:
|
||||
|
||||
- renderer helper -> browser IPC works
|
||||
- runtime dispatch works
|
||||
- `openhuman-cursor` receives the notification payload
|
||||
- OS notification delivery can work
|
||||
|
||||
### 5. Tokio runtime panic found and fixed in app notification delivery
|
||||
|
||||
Once the manual helper path reached the app, native notification delivery initially crashed with:
|
||||
|
||||
- `there is no reactor running, must be called from the context of a Tokio 1.x runtime`
|
||||
|
||||
The panic came from using `tokio::task::spawn_blocking(...)` from a CEF callback thread in:
|
||||
|
||||
- `app/src-tauri/src/webview_accounts/mod.rs`
|
||||
|
||||
Fix applied:
|
||||
|
||||
- replaced `tokio::task::spawn_blocking(...)` with `std::thread::spawn(...)`
|
||||
|
||||
This was done for the macOS notification path and the Linux path for consistency.
|
||||
|
||||
After this fix, the manual helper trigger produced an actual OS notification successfully.
|
||||
|
||||
### 6. `Invalid UTF-16 string` messages were observed but were not the blocker
|
||||
|
||||
During manual notification tests, CEF emitted:
|
||||
|
||||
- `Invalid UTF-16 string`
|
||||
|
||||
These warnings appeared before successful notification IPC and dispatch logs, so they were treated as noisy string-conversion warnings rather than the root failure.
|
||||
|
||||
### 7. Slack still does not automatically use the browser notification APIs for real messages
|
||||
|
||||
After helper-side execution logging was added, a real incoming Slack message did **not** produce:
|
||||
|
||||
- `[cef-helper-notify] execute source=0 ...`
|
||||
- `[cef-helper-notify] execute source=1 ...`
|
||||
|
||||
This means the real incoming-message path in this embedded Slack session is not currently hitting:
|
||||
|
||||
- `new Notification(...)`
|
||||
- `ServiceWorkerRegistration.prototype.showNotification(...)`
|
||||
|
||||
So the remaining problem is no longer CEF notification transport. The remaining problem is Slack-specific runtime behavior in this embedded environment.
|
||||
|
||||
### 8. An attempted hard lock on `window.Notification` broke Slack rendering
|
||||
|
||||
One experiment tried to prevent Slack from overwriting the helper-installed notification hooks by making them effectively non-overridable.
|
||||
|
||||
That caused Slack to render a blank screen, so that hardening change was reverted.
|
||||
|
||||
Current safe state:
|
||||
|
||||
- Slack renders normally
|
||||
- helper shim installs
|
||||
- manual helper trigger works
|
||||
- automatic incoming-message notifications still do not use the browser notification APIs
|
||||
|
||||
### 9. Fallback strategy: synthesize notifications from the Slack scanner
|
||||
|
||||
Because Slack’s own incoming-message path was not invoking browser notification APIs, a fallback path was added to the Slack scanner:
|
||||
|
||||
- `app/src-tauri/src/slack_scanner/mod.rs`
|
||||
- `app/src-tauri/src/webview_accounts/mod.rs`
|
||||
|
||||
The scanner logic was updated to:
|
||||
|
||||
- keep a per-channel unread baseline
|
||||
- skip notifications on the first scan
|
||||
- emit a native notification when a channel unread count increases later
|
||||
|
||||
However, live verification showed the scanner path was not actually active because the app was missing the scanner registry in managed state.
|
||||
|
||||
Observed log:
|
||||
|
||||
- `[webview-accounts] slack ScannerRegistry not in app state`
|
||||
|
||||
So the scanner-based fallback is patched in code, but it still needs the app builder to manage `slack_scanner::ScannerRegistry::new()` in:
|
||||
|
||||
- `app/src-tauri/src/lib.rs`
|
||||
|
||||
## Current Outcome
|
||||
|
||||
### Notification permission behavior
|
||||
|
||||
The underlying notification permission problem in `tauri-cef` was fixed by moving the renderer-visible granted-state shim into the actual runtime path.
|
||||
|
||||
This means Slack-style checks should now see notification permission as granted inside the Tauri CEF webview.
|
||||
|
||||
### App-side notification behavior
|
||||
|
||||
`openhuman-cursor` was updated so:
|
||||
|
||||
- shell-side notifications are enabled by default
|
||||
- the vendored `tauri-cef` includes the runtime permission fix
|
||||
- browser notification handlers are cleaned up on webview close
|
||||
- manual helper-triggered notifications reach the OS successfully
|
||||
- app-side native notification delivery no longer depends on a Tokio runtime in the callback thread
|
||||
|
||||
### Remaining distinction
|
||||
|
||||
There are two separate concerns:
|
||||
|
||||
1. Permission/granted-state correctness
|
||||
2. What the app does after a notification is received
|
||||
|
||||
The changes above fix the permission side and the runtime notification bridge plumbing. The app still needs its normal notification handling path to decide how to present or forward those notifications.
|
||||
|
||||
### Current verified status
|
||||
|
||||
Verified working:
|
||||
|
||||
- notification permission appears granted in the Slack webview
|
||||
- helper shim installs in the live Slack page
|
||||
- manual helper notification trigger reaches CEF browser IPC
|
||||
- runtime dispatch reaches `openhuman-cursor`
|
||||
- native OS notification display works for the manual helper-triggered path
|
||||
|
||||
Still not working automatically:
|
||||
|
||||
- real Slack incoming messages do not currently surface through browser notification APIs in this embedded session
|
||||
- scanner-driven fallback notifications will not run until the scanner registry is re-enabled in app state
|
||||
|
||||
## Files Changed
|
||||
|
||||
### In `tauri-cef`
|
||||
|
||||
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/notification.rs`
|
||||
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/cef_impl.rs`
|
||||
- `(external tauri-cef repo)/crates/tauri-runtime-cef/src/lib.rs`
|
||||
- `(external tauri-cef repo)/CEF_NOTIFICATION_PERMISSION_CHANGES.md`
|
||||
|
||||
### In `openhuman-cursor`
|
||||
|
||||
- `app/src-tauri/src/notification_settings/mod.rs`
|
||||
- `app/src-tauri/src/webview_accounts/mod.rs`
|
||||
- `app/src-tauri/src/slack_scanner/mod.rs`
|
||||
- `app/src-tauri/src/lib.rs`
|
||||
- `app/src-tauri/Cargo.toml`
|
||||
- `app/src-tauri/vendor/tauri-plugin-notification/Cargo.toml`
|
||||
- `app/src-tauri/vendor/tauri-plugin-notification/src/lib.rs`
|
||||
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/cef_impl.rs`
|
||||
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/notification.rs`
|
||||
- `app/src-tauri/vendor/tauri-cef/crates/tauri-runtime-cef/src/lib.rs`
|
||||
- `app/src-tauri/vendor/tauri-cef/cef-helper/src/notification.rs`
|
||||
- `app/src-tauri/vendor/tauri-cef/cef-helper/src/main.rs`
|
||||
- `scripts/ensure-tauri-cli.sh`
|
||||
|
||||
## Suggested Follow-Up
|
||||
|
||||
Recommended next functional fix:
|
||||
|
||||
1. Re-enable `slack_scanner::ScannerRegistry::new()` in:
|
||||
- `app/src-tauri/src/lib.rs`
|
||||
2. Rebuild and verify unread-delta notifications from the scanner fallback path.
|
||||
|
||||
Secondary cleanup work:
|
||||
|
||||
1. Remove temporary helper/debug instrumentation once notification behavior is finalized.
|
||||
2. If cleaner inspection is still needed, move remote debugging off `9222` to a dedicated port such as `9333`.
|
||||
@@ -1,178 +0,0 @@
|
||||
# Codex PR Checklist
|
||||
|
||||
Use this checklist for Codex web sessions, Linear-launched implementation agents, and any other remote agent that opens OpenHuman PRs.
|
||||
|
||||
## Required Preflight
|
||||
|
||||
Run the scriptable preflight wrapper (recommended):
|
||||
|
||||
```bash
|
||||
node scripts/codex-pr-preflight.mjs --strict-path --lightweight
|
||||
```
|
||||
|
||||
Use `--lightweight` when you only need environment/repo checks plus changed-surface validation recommendations (it skips heavier runtime validations).
|
||||
|
||||
Run this before editing files:
|
||||
|
||||
```bash
|
||||
pwd
|
||||
git status --porcelain
|
||||
git branch --show-current
|
||||
git remote -v
|
||||
test -f AGENTS.md
|
||||
test -f gitbooks/developing/README.md
|
||||
test -f Cargo.toml
|
||||
test -f app/package.json
|
||||
```
|
||||
|
||||
Expected repository path in Codex web is `/workspace/openhuman`. If the checkout is missing or the command shows another project, stop immediately and report the environment binding problem. Do not edit files in the wrong repository.
|
||||
|
||||
## Launch Trigger Rule
|
||||
|
||||
Use exactly one Codex trigger per Linear issue.
|
||||
|
||||
Preferred launch pattern:
|
||||
|
||||
```md
|
||||
@Codex use the Codex environment for jwalin-shah/openhuman.
|
||||
|
||||
Work issue <ISSUE-KEY>.
|
||||
Expected path: /workspace/openhuman.
|
||||
Start from latest origin/main.
|
||||
Create branch codex/<ISSUE-KEY>-<short-title>.
|
||||
Follow docs/agent-workflows/codex-pr-checklist.md exactly.
|
||||
Do not open duplicate PRs. If validation is blocked, report exact command and error in the PR body and Linear.
|
||||
```
|
||||
|
||||
Do not also set `delegate: Codex` when posting an explicit `@Codex` launch comment. Linear delegate metadata can start its own Codex thread, so combining both mechanisms can double-trigger the same issue.
|
||||
|
||||
If using `delegate: Codex` as the only trigger for an integration that requires it, do not add an `@Codex` comment. Record in the issue which trigger was used.
|
||||
|
||||
## Branch And PR Rules
|
||||
|
||||
- Start from latest `origin/main` unless the Linear issue explicitly says otherwise.
|
||||
- Use one branch and one PR per Linear issue.
|
||||
- Name branches `codex/<ISSUE-KEY>-<short-title>`.
|
||||
- Do not open duplicate PRs for the same issue. If a retry is needed, update the existing PR branch or close the stale duplicate and state which PR is canonical.
|
||||
- PRs should target `jwalin-shah/openhuman:main` unless upstream permissions allow `tinyhumansai/openhuman:main`.
|
||||
|
||||
## Duplicate PR Cleanup
|
||||
|
||||
Canonical PR rule: keep the PR whose head branch is the active issue branch and whose head commit contains the intended final work. If two PRs contain equivalent work, keep the PR already linked from Linear or already carrying useful review/CI history. If neither has history, keep the older PR number to reduce churn. Do not choose by recency alone; compare the heads first and move any useful commits or PR body details onto the canonical PR before closing the duplicate.
|
||||
|
||||
Lightweight comparison and close recipe:
|
||||
|
||||
```bash
|
||||
BASE_REPO=tinyhumansai/openhuman # or jwalin-shah/openhuman for fork-targeted PRs
|
||||
BASE_REMOTE=upstream # remote matching BASE_REPO
|
||||
KEEP=123 # canonical PR number
|
||||
CLOSE=124 # duplicate PR number
|
||||
|
||||
gh pr view "$KEEP" --repo "$BASE_REPO" --json number,url,state,baseRefName,headRefName,headRefOid
|
||||
gh pr view "$CLOSE" --repo "$BASE_REPO" --json number,url,state,baseRefName,headRefName,headRefOid
|
||||
|
||||
git fetch "$BASE_REMOTE" "refs/pull/$KEEP/head:refs/tmp/pr-$KEEP"
|
||||
git fetch "$BASE_REMOTE" "refs/pull/$CLOSE/head:refs/tmp/pr-$CLOSE"
|
||||
git log --oneline --left-right --cherry-pick "refs/tmp/pr-$KEEP...refs/tmp/pr-$CLOSE"
|
||||
git diff --stat "refs/tmp/pr-$KEEP...refs/tmp/pr-$CLOSE"
|
||||
git diff --name-status "refs/tmp/pr-$KEEP...refs/tmp/pr-$CLOSE"
|
||||
|
||||
gh pr close "$CLOSE" --repo "$BASE_REPO" --comment "Closing as a duplicate of #$KEEP for <ISSUE-KEY>. Kept #$KEEP because <canonical reason>."
|
||||
|
||||
git update-ref -d "refs/tmp/pr-$KEEP"
|
||||
git update-ref -d "refs/tmp/pr-$CLOSE"
|
||||
```
|
||||
|
||||
If the duplicate has unique, useful commits, cherry-pick or manually port them onto the canonical branch, push that branch, then repeat the comparison before closing anything.
|
||||
|
||||
Record the cleanup in Linear before handoff:
|
||||
|
||||
- Canonical PR kept: `<PR URL>` with head SHA `<sha>`.
|
||||
- Duplicate PR closed: `<PR URL>` with reason.
|
||||
- Comparison evidence: command summary, for example `git log --left-right --cherry-pick` showed no unique commits in the duplicate.
|
||||
- Any moved commits or remaining blockers.
|
||||
|
||||
Pattern from the SYM-92 incident: two agent launches produced overlapping PRs for the same Linear issue. The cleanup was to compare both heads, keep the PR that represented the active issue branch/final handoff, close the stale duplicate with a pointer to the kept PR, and record both PRs in Linear. Treat that as the reusable pattern; the kept PR is still selected by branch, head diff, and handoff evidence for the current issue.
|
||||
|
||||
## Validation Before PR
|
||||
|
||||
Run the smallest checks that prove the changed surface, plus the relevant merge gates:
|
||||
|
||||
```bash
|
||||
# Always run for app or docs-visible app changes
|
||||
pnpm --filter openhuman-app format:check
|
||||
pnpm typecheck
|
||||
|
||||
# Focused app tests for changed TS/React behavior
|
||||
pnpm --dir app exec vitest run <changed-test-files> --config test/vitest.config.ts
|
||||
|
||||
# Root Rust changes
|
||||
cargo fmt --manifest-path Cargo.toml --all --check
|
||||
|
||||
# Tauri shell changes
|
||||
cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check
|
||||
```
|
||||
|
||||
For Rust behavior changes, prefer focused tests through the repo wrappers where available:
|
||||
|
||||
```bash
|
||||
pnpm debug rust <test-filter>
|
||||
```
|
||||
|
||||
If a command cannot run because the environment lacks vendored files or system packages, do not claim it passed. Copy the exact command and blocker into the PR body.
|
||||
|
||||
## PR Submission Checklist Preflight
|
||||
|
||||
Before handing off an AI-authored PR, validate the PR body locally. This catches unchecked template items before the GitHub workflow reports them.
|
||||
|
||||
For a generated PR body file:
|
||||
|
||||
```bash
|
||||
pnpm pr:checklist /tmp/pr-body.md
|
||||
```
|
||||
|
||||
For a generated body already loaded in an environment variable:
|
||||
|
||||
```bash
|
||||
PR_BODY="$(cat /tmp/pr-body.md)" pnpm pr:checklist
|
||||
```
|
||||
|
||||
For an existing GitHub PR:
|
||||
|
||||
```bash
|
||||
gh pr view <number> --repo tinyhumansai/openhuman --json body --jq .body | pnpm pr:checklist -
|
||||
```
|
||||
|
||||
Every checklist line must be checked. If an item does not apply, check it and include a short `N/A` reason on the same line. Do not leave `N/A` items unchecked.
|
||||
|
||||
## Refactor Parity Rules
|
||||
|
||||
For behavior extraction and architecture refactors:
|
||||
|
||||
- Identify the old guard order, fallback order, dispatch contract, or public API being preserved.
|
||||
- Add focused parity tests when the behavior can be tested without broad integration setup.
|
||||
- Do not reorder guards, fallback layers, RPC methods, or dispatch paths unless the issue explicitly asks for a behavior change.
|
||||
- When adding a drift guard, verify it checks the source of truth as it exists in this repo. Do not assume generated strings are written literally in source files.
|
||||
|
||||
## PR Body Requirements
|
||||
|
||||
Every AI-authored PR must include:
|
||||
|
||||
- Linear issue key and URL.
|
||||
- Branch name.
|
||||
- Commit SHA.
|
||||
- Files changed summary.
|
||||
- Validation commands run.
|
||||
- Validation commands blocked, with exact error text.
|
||||
- Behavior intentionally changed, or `No intended behavior change`.
|
||||
- Any duplicate/stale PRs that were closed or superseded.
|
||||
|
||||
## Review Before Handoff
|
||||
|
||||
Before handing off:
|
||||
|
||||
- Re-check GitHub CI status for the PR.
|
||||
- Pull failed check logs before guessing.
|
||||
- Fix format/type/test failures that are local to the PR.
|
||||
- Leave broad system dependency or environment failures as explicit blockers.
|
||||
- Update the Linear issue with PR URL, commit SHA, validations, and blockers.
|
||||
@@ -1,165 +0,0 @@
|
||||
# Cursor Cloud Agents — parallel workflow
|
||||
|
||||
Operator playbook for running 15–20 [Cursor Cloud Agents](https://cursor.com/docs/cloud-agent) in parallel against OpenHuman. Companion to [`codex-pr-checklist.md`](codex-pr-checklist.md); the same merge gates apply.
|
||||
|
||||
This doc closes [`tinyhumansai/openhuman#1480`](https://github.com/tinyhumansai/openhuman/issues/1480).
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. Write a **batch spec** — one JSON file naming N agents, their issues, branches, and owned paths.
|
||||
2. **Validate** it (`pnpm agent-batch validate <spec>`) and **prove ownership disjointness** (`pnpm agent-batch overlap <spec>`).
|
||||
3. Post **one launch comment** per agent (generated from the spec) into Cursor; each agent opens a branch and PR matching the spec.
|
||||
4. Track progress with `pnpm agent-batch status <spec>` — markdown table of PR + CI per agent.
|
||||
5. Pilot at N=3 before scaling to 15–20.
|
||||
|
||||
Concretely, none of this is "Cursor magic" — it is a JSON contract + three small scripts that fail loudly if humans break it.
|
||||
|
||||
## Why a contract
|
||||
|
||||
Running N agents in parallel breaks in three ways:
|
||||
|
||||
- **Branch / PR collisions** — two agents picking the same branch name, or opening duplicate PRs against the same issue.
|
||||
- **File collisions** — two agents editing the same module, producing conflicting merges.
|
||||
- **Quality drift** — agents skipping format / typecheck / coverage and pushing red PRs.
|
||||
|
||||
The batch spec is the single source of truth that prevents the first two. The third is enforced by upstream CI ([`.github/workflows/pr-ci.yml`](../../.github/workflows/pr-ci.yml), [`.github/workflows/pr-quality.yml`](../../.github/workflows/pr-quality.yml)) — agents do not get to opt out.
|
||||
|
||||
## Batch spec
|
||||
|
||||
A batch is a JSON file living under `docs/agent-workflows/batches/` (gitignored — see [Privacy](#secrets-posture)) or generated ad hoc. Shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"batch_id": "pilot-2026-05-15",
|
||||
"base_repo": "tinyhumansai/openhuman",
|
||||
"base_branch": "main",
|
||||
"tracking_issue": 1480,
|
||||
"agents": [
|
||||
{
|
||||
"id": "a01",
|
||||
"issue": 1234,
|
||||
"title": "short slug for the branch name",
|
||||
"branch": "cursor/a01-1234-short-slug",
|
||||
"owned_paths": ["app/src/features/foo/", "src/openhuman/foo/"],
|
||||
"allowed_shared_paths": ["docs/TEST-COVERAGE-MATRIX.md"],
|
||||
"labels": ["cursor-agent", "pilot"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `batch_id` | yes | Stable identifier — appears in PR bodies and the tracking comment. |
|
||||
| `base_repo` | yes | Always `tinyhumansai/openhuman` unless explicitly delegated. |
|
||||
| `base_branch` | yes | `main`. |
|
||||
| `tracking_issue` | yes | One upstream issue per batch; that issue's comment thread is the dashboard (AC #6). |
|
||||
| `agents[].id` | yes | Two-char + digits, e.g. `a01`–`a20`. Unique within batch. |
|
||||
| `agents[].issue` | yes | Upstream issue number. **One issue per agent**, **one agent per issue.** |
|
||||
| `agents[].branch` | yes | Must start with `cursor/` and contain the agent id and issue number. |
|
||||
| `agents[].owned_paths` | yes | **Path prefixes** (directory ending in `/`) or exact files. **No globs.** Disjoint across agents — `overlap` enforces this. |
|
||||
| `agents[].allowed_shared_paths` | no | Files the agent may touch even if another agent's prefix contains them (e.g. `docs/TEST-COVERAGE-MATRIX.md`, capability catalog). Best-effort only — overlap on these is **warned**, not blocked. |
|
||||
| `agents[].labels` | no | PR labels. Always include `cursor-agent`. Add `docs` or `chore` to opt out of the soft `pr-quality` checks per [`pr-quality.yml`](../../.github/workflows/pr-quality.yml). |
|
||||
|
||||
### Why prefixes, not globs
|
||||
|
||||
A glob ownership model (`app/src/components/**`) is tempting but makes overlap detection ambiguous: does `app/src/**/*.test.ts` collide with `app/src/components/Foo/`? With prefixes the answer is mechanical: prefix containment in either direction = collision. CI files like `.github/workflows/*.yml`, the capability catalog at `src/openhuman/about_app/`, and similar shared surfaces should be assigned to **one** agent for the batch (or to no agent — the batch should be designed not to need them).
|
||||
|
||||
## Branch & PR conventions
|
||||
|
||||
- Branch off **`origin/main` (upstream)** at the moment the spec is written. Each agent fetches `origin/main` itself.
|
||||
- Branch name format: `cursor/<id>-<issue>-<short-slug>` (e.g. `cursor/a04-1456-memory-namespace`). Enforced by `validate.mjs`.
|
||||
- Push to the **forking remote the Cursor workspace is configured with**, not directly to `tinyhumansai/openhuman`. PRs are opened with `--head <fork-owner>:<branch>` against `tinyhumansai/openhuman:main`.
|
||||
- **One PR per issue**, **one PR per branch**. If a retry is needed, update the existing PR; do not open a duplicate. Use the duplicate cleanup recipe in [`codex-pr-checklist.md`](codex-pr-checklist.md#duplicate-pr-cleanup).
|
||||
- PR title: `<area>: <short imperative> (#<issue>)`. PR body **must** follow [`.github/PULL_REQUEST_TEMPLATE.md`](../../.github/PULL_REQUEST_TEMPLATE.md) verbatim, including the `## AI Authored PR Metadata` section.
|
||||
- PR labels include at minimum `cursor-agent` and the batch id label `batch:<batch_id>` so the tracking comment can find them.
|
||||
|
||||
## Ownership boundaries
|
||||
|
||||
Disjointness rule: for any two agents A and B in the batch, no path prefix in `A.owned_paths` may be a prefix of any path in `B.owned_paths`, in either direction. `scripts/agent-batch/overlap.mjs` checks this and exits non-zero on a collision.
|
||||
|
||||
The rule applies to **prefixes**, not file existence. Two agents may own paths that don't exist yet (new modules), as long as no prefix contains another.
|
||||
|
||||
If two issues genuinely need the same module, **do not split them across agents**. Combine them into a single agent's scope or sequence the work.
|
||||
|
||||
## Quality gates
|
||||
|
||||
Agents run the same gates as any other PR. The launch comment instructs them explicitly — they do not get to drop any of these:
|
||||
|
||||
- **Format**: `pnpm --filter openhuman-app format:check`, `cargo fmt --manifest-path Cargo.toml --all --check`, and the Tauri shell equivalent if shell files changed.
|
||||
- **Lint / typecheck**: `pnpm lint`, `pnpm typecheck`.
|
||||
- **Tests (focused)**: targeted Vitest for changed files, focused Rust tests via `pnpm debug rust <filter>` for changed Rust.
|
||||
- **Coverage**: agents must run `pnpm test:coverage` and `pnpm test:rust` locally and add tests for changed lines. The merge gate is `≥ 80% diff coverage`, enforced server-side by [`pr-ci.yml`](../../.github/workflows/pr-ci.yml). PRs below the threshold do not merge — agents that cannot reach the threshold must say so in the PR body, not paper over it.
|
||||
- **PR checklist + coverage matrix**: [`pr-quality.yml`](../../.github/workflows/pr-quality.yml) checks the PR body and `docs/TEST-COVERAGE-MATRIX.md`. The `docs` and `chore` labels exempt a PR from these soft gates — use them only for PRs that genuinely change no behavior.
|
||||
|
||||
If the agent's environment cannot run a gate, the PR body must report the **exact command and error** under `### Validation Blocked`, not claim it passed. This is the same rule as the codex checklist.
|
||||
|
||||
## Secrets posture
|
||||
|
||||
Cursor Cloud Agents inherit env from the workspace. For OpenHuman, the cloud workspace MUST be configured with:
|
||||
|
||||
- **No** `STAGING_*` / `PRODUCTION_*` secrets.
|
||||
- **No** `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or any other LLM provider key used by the production agent runtime — agents do code work, not LLM calls into production providers.
|
||||
- A scoped `GITHUB_TOKEN` with `contents:write` and `pull_requests:write` on the **fork** the workspace pushes to, plus `pull_requests:write` on `tinyhumansai/openhuman` for PR creation. **No `admin:*`, no `actions:write`, no `secrets:*`.**
|
||||
- `OPENHUMAN_APP_ENV` MUST be unset or set to `dev`. Never `staging` or `production` — staging writes `~/.openhuman-staging/core.token` referenced by [`AGENTS.md`](../../AGENTS.md) "Cursor Cloud specific instructions" and that token is **per-developer**, not for shared cloud workspaces.
|
||||
- `.env.local`, `app/.env.local`, and `core.token` files are gitignored and must not be committed.
|
||||
|
||||
The agent's own environment is the smallest blast-radius surface. Production credentials are out of scope for code-writing agents.
|
||||
|
||||
## Progress visibility
|
||||
|
||||
One tracking issue per batch (`tracking_issue` in the spec). The launch script posts a **single comment** on that issue containing a markdown table generated by `scripts/agent-batch/status.mjs`:
|
||||
|
||||
| Agent | Issue | Branch | PR | CI | Coverage | Status |
|
||||
| ----- | ----- | ------------------- | ------- | --------- | -------- | ------------ |
|
||||
| a01 | #1234 | `cursor/a01-1234-…` | `#1234` | ✓ green | 87% | merged |
|
||||
| a02 | #1235 | `cursor/a02-1235-…` | `#1235` | × failing | — | needs review |
|
||||
|
||||
Re-running `status` rewrites the same comment (looked up by a `<!-- batch:<id> -->` marker) so the issue thread doesn't fill with stale tables. The script reads:
|
||||
|
||||
- `gh pr list --repo tinyhumansai/openhuman --label batch:<id> --json …` for PR + state.
|
||||
- `gh pr checks <pr>` for CI rollup.
|
||||
- The `diff-coverage.md` artifact from `pr-ci.yml`, if downloaded — otherwise coverage shows `—`.
|
||||
|
||||
No external dashboard — GitHub issues + labels + the table are the single pane of glass.
|
||||
|
||||
## Pilot-then-scale
|
||||
|
||||
Do not launch 15–20 agents on day one.
|
||||
|
||||
1. **N=3 pilot.** Pick three issues that are small (~200 LOC each), in three distinct domains, no overlap. Run the full flow: spec → validate → overlap → launch → status → merge.
|
||||
2. **N=5 expansion.** Once the N=3 pilot has 3 green PRs (merged or at-review), expand to 5 in one batch. Watch CI queue times and rate limits.
|
||||
3. **N=15–20 production.** Only after two clean expansion batches. At this scale, watch `gh api rate_limit`, GitHub Actions concurrency, and Cursor's per-workspace agent limit.
|
||||
|
||||
If a batch surfaces a class of failure (e.g. agents inventing API names, agents skipping `cargo fmt`), fix the **launch comment template** that all agents inherit — don't fix it case-by-case.
|
||||
|
||||
## Operator quickstart
|
||||
|
||||
```bash
|
||||
# 1. Draft the spec
|
||||
cp docs/agent-workflows/pilot-batch-example.json /tmp/my-batch.json
|
||||
$EDITOR /tmp/my-batch.json
|
||||
|
||||
# 2. Validate shape + naming
|
||||
pnpm agent-batch validate /tmp/my-batch.json
|
||||
|
||||
# 3. Prove ownership disjointness
|
||||
pnpm agent-batch overlap /tmp/my-batch.json
|
||||
|
||||
# 4. Generate one launch comment per agent (paste into Cursor)
|
||||
pnpm agent-batch launch /tmp/my-batch.json --print-only
|
||||
|
||||
# 5. After agents have pushed, refresh the tracking comment
|
||||
pnpm agent-batch status /tmp/my-batch.json --post
|
||||
```
|
||||
|
||||
All scripts are in [`scripts/agent-batch/`](../../scripts/agent-batch/). They are zero-dep Node, executable from the repo root, and exit non-zero on policy violations so they are CI-friendly.
|
||||
|
||||
## Reference
|
||||
|
||||
- [`pilot-batch-example.json`](pilot-batch-example.json) — canonical example with 3 disjoint agents.
|
||||
- [`scripts/agent-batch/`](../../scripts/agent-batch/) — validate / overlap / launch / status implementations + `node:test` suites.
|
||||
- [`codex-pr-checklist.md`](codex-pr-checklist.md) — parent checklist; per-agent rules inherit from it.
|
||||
- [`AGENTS.md`](../../AGENTS.md) and [`CLAUDE.md`](../../CLAUDE.md) — repo-wide rules every agent must follow.
|
||||
@@ -1,151 +0,0 @@
|
||||
# Open PR Reconciliation Handoff - 2026-05-13
|
||||
|
||||
Snapshot taken from `tinyhumansai/openhuman` on 2026-05-13 after fetching
|
||||
`upstream/main` at `2b64ea8a` (`chore(release): v0.53.43`). The queue is
|
||||
moving quickly; refresh the PR list before acting.
|
||||
|
||||
This is a reconciliation-first handoff. It does not change product code.
|
||||
|
||||
## Current Queue
|
||||
|
||||
- Open PRs: 28.
|
||||
- Merge-state shape: 23 `BLOCKED`, 5 `DIRTY`.
|
||||
- Draft PRs: #1644, #1519, #1420, #1383.
|
||||
- High-risk conflict/rebase set: #1671, #1646, #1518, #1462, #1420.
|
||||
- Broadest PRs by changed files: #1420 (93 files), #1518 (90 files), #1671
|
||||
(19 files), #1519 (13 files), #1677 (12 files), #1488 (11 files).
|
||||
|
||||
| PR | State | Merge | Files | Size | Updated | Notes |
|
||||
| --- | --- | --- | ---: | ---: | --- | --- |
|
||||
| #1678 | ready | BLOCKED | 2 | +280/-16 | 2026-05-13 19:29Z | Triage prompt-guard rejection handling. |
|
||||
| #1677 | ready | BLOCKED | 12 | +2623/-1 | 2026-05-13 19:25Z | Gameplay review workflow; failing broad CI and review requested. |
|
||||
| #1676 | ready | BLOCKED | 4 | +238/-3 | 2026-05-13 18:54Z | Backend 4xx observability classifier. |
|
||||
| #1672 | ready | BLOCKED | 2 | +68/-8 | 2026-05-13 18:07Z | Socket sustained-outage classifier; cancelled lightweight checks observed. |
|
||||
| #1671 | ready | DIRTY | 19 | +1772/-265 | 2026-05-13 18:09Z | BYO Composio API key; conflicts with Composio/integrations/settings surfaces. |
|
||||
| #1657 | ready | BLOCKED | 8 | +344/-1 | 2026-05-13 17:31Z | Gmail unsubscribe agent; overlaps tools/memory email surfaces. |
|
||||
| #1656 | ready | BLOCKED | 8 | +52/-103 | 2026-05-13 15:13Z | Local AI test isolation. |
|
||||
| #1646 | ready | DIRTY | 6 | +265/-20 | 2026-05-13 17:06Z | Portfolio readiness docs/lint; conflicts with active UI pages and docs. |
|
||||
| #1645 | ready | BLOCKED | 2 | +39/-1 | 2026-05-13 18:25Z | Provider function argument parse guard. |
|
||||
| #1644 | draft | BLOCKED | 9 | +1474/-0 | 2026-05-13 17:12Z | Deep-work automation scripts; draft. |
|
||||
| #1641 | ready | BLOCKED | 3 | +458/-155 | 2026-05-13 20:02Z | Windows FS retry; changes requested. |
|
||||
| #1636 | ready | BLOCKED | 2 | +212/-1 | 2026-05-13 18:04Z | Stale credentials lock recovery. |
|
||||
| #1635 | ready | BLOCKED | 3 | +88/-31 | 2026-05-13 16:04Z | Screen-intelligence idempotent session start. |
|
||||
| #1634 | ready | BLOCKED | 10 | +325/-51 | 2026-05-13 20:00Z | Agent max-iteration observability; checks still running. |
|
||||
| #1633 | ready | BLOCKED | 10 | +344/-84 | 2026-05-13 20:08Z | Budget-exhausted observability; PR checklist failed and review requested. |
|
||||
| #1632 | ready | BLOCKED | 7 | +474/-37 | 2026-05-13 20:07Z | Transient backend/integrations observability; checks still running. |
|
||||
| #1630 | ready | BLOCKED | 2 | +298/-4 | 2026-05-13 18:28Z | Integrations local-AI URL fallback. |
|
||||
| #1623 | ready | BLOCKED | 1 | +80/-0 | 2026-05-13 19:23Z | Vision RAM-tier observability skip. |
|
||||
| #1620 | ready | BLOCKED | 1 | +57/-10 | 2026-05-13 17:59Z | UTF-8 memory ingest slicing. |
|
||||
| #1589 | ready | BLOCKED | 3 | +377/-0 | 2026-05-13 14:42Z | Contributor reward invite automation. |
|
||||
| #1561 | ready | BLOCKED | 3 | +744/-0 | 2026-05-13 18:38Z | Memory benchmark fixtures. |
|
||||
| #1519 | draft | BLOCKED | 13 | +1065/-19 | 2026-05-12 07:07Z | Learning summarizer; draft. |
|
||||
| #1518 | ready | DIRTY | 90 | +3727/-1295 | 2026-05-13 06:33Z | Chinese i18n; very broad UI surface. |
|
||||
| #1488 | ready | BLOCKED | 11 | +646/-126 | 2026-05-13 18:09Z | Orchestrator delegation collapse. |
|
||||
| #1462 | ready | DIRTY | 5 | +886/-789 | 2026-05-10 23:43Z | "Files Reviewed"; stale/conflicting, many failed checks, changes requested. |
|
||||
| #1420 | draft | DIRTY | 93 | +10060/-35 | 2026-05-10 21:31Z | iOS client; largest branch, draft and conflicting. |
|
||||
| #1383 | draft | BLOCKED | 1 | +57/-0 | 2026-05-10 21:30Z | Worker clone disposition docs. |
|
||||
| #1321 | ready | BLOCKED | 4 | +84/-13 | 2026-05-10 21:31Z | Core-state rewards timeout UX. |
|
||||
|
||||
## Conflict Clusters
|
||||
|
||||
These files are touched by multiple open PRs and should be treated as hot
|
||||
surfaces. Do not launch implementation work here until the relevant PRs are
|
||||
rebased, merged, or closed.
|
||||
|
||||
| Surface | PRs | Risk |
|
||||
| --- | --- | --- |
|
||||
| `src/core/observability.rs` | #1676, #1634, #1633, #1632, #1623 | Highest overlap. Pick a canonical observability stack order before rebasing. |
|
||||
| `app/src-tauri/src/lib.rs` | #1634, #1633, #1632, #1420 | Tauri shell overlap; avoid mixing iOS shell work with observability fixes. |
|
||||
| `src/main.rs` | #1634, #1633, #1632 | Observability initialization overlap. |
|
||||
| `src/openhuman/integrations/client.rs` | #1676, #1632, #1630 | Integrations error classification and local-AI fallback interact. |
|
||||
| `app/src/App.tsx` | #1518, #1420 | Broad UI architecture conflict between i18n and iOS routing. |
|
||||
| `app/src/pages/Settings.tsx` | #1671, #1420 | BYO Composio settings conflicts with iOS settings changes. |
|
||||
| `src/core/all.rs`, `src/openhuman/about_app/catalog.rs`, `src/openhuman/mod.rs` | #1677, #1420 | Core module/catalog registration overlap. |
|
||||
| `src/openhuman/agent/harness/*`, `src/openhuman/channels/runtime/dispatch.rs` | #1634, #1519, #1488 | Agent runtime, learning, and delegation work should not proceed independently. |
|
||||
| `src/openhuman/credentials/profiles.rs` | #1641, #1636 | Windows retry and stale-lock recovery need one canonical credential-lock PR. |
|
||||
| `Cargo.toml`, `Cargo.lock`, `pnpm-lock.yaml` | #1462, #1420 | Dependency/lockfile churn; #1462 appears stale and should not be used as a base. |
|
||||
|
||||
## Validation Gates
|
||||
|
||||
OpenHuman PRs carry both local and CI gates. For code PRs, the minimum gate is
|
||||
the smallest focused command that proves the changed surface plus the relevant
|
||||
global merge checks:
|
||||
|
||||
- PR body/template gate: `pnpm pr:checklist <body-file>`.
|
||||
- Formatting: `pnpm --filter openhuman-app format:check`; Rust changes also
|
||||
need `cargo fmt --manifest-path Cargo.toml --all --check` and, for Tauri,
|
||||
`cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check`.
|
||||
- TypeScript: `pnpm typecheck` for app-facing TypeScript changes.
|
||||
- Focused tests: Vitest file-level tests for changed React/TS behavior; `pnpm
|
||||
debug rust <filter>` for Rust behavior.
|
||||
- Coverage gate: CI enforces changed-line diff coverage at or above 80%.
|
||||
- Docs-only changes: run the PR checklist parser and any available markdown
|
||||
link/check workflow locally when dependencies are installed.
|
||||
|
||||
Current blocker pattern:
|
||||
|
||||
- `DIRTY` PRs need rebase/conflict resolution before any CI result matters.
|
||||
- `BLOCKED` PRs are mostly mergeable but waiting on checks, reviews, checklist
|
||||
failures, or branch protection.
|
||||
- #1677 and #1462 have concrete failed CI/check evidence and should not be
|
||||
treated as safe to merge without repair.
|
||||
- #1633 has a failed PR checklist and requested changes; fix the body/checklist
|
||||
before spending compute on broad tests.
|
||||
- Before rebasing or repairing any existing PR, refresh metadata with `gh pr
|
||||
view <number> --repo tinyhumansai/openhuman --json
|
||||
body,changedFiles,mergeStateStatus,mergeable,state,statusCheckRollup`.
|
||||
- If validating an existing PR body, feed the actual body into the checklist
|
||||
parser: `gh pr view <number> --repo tinyhumansai/openhuman --json body --jq
|
||||
.body | pnpm pr:checklist -`.
|
||||
|
||||
## Recommended Reconciliation Order
|
||||
|
||||
1. Close or explicitly supersede stale/meta PRs before implementation:
|
||||
#1462 should be audited first because it is conflicting, has broad failed
|
||||
checks, and has an unclear title. If it has no unique current work, close it
|
||||
with a pointer to the canonical replacement.
|
||||
2. Reconcile observability as a single stack:
|
||||
compare #1623, #1632, #1633, #1634, #1676, and #1672. Choose an order from
|
||||
narrowest independent classifier to broadest runtime initialization. Rebase
|
||||
one PR at a time and rerun focused observability tests plus checklist.
|
||||
#1676 is the likely base candidate because it changes the backend 4xx
|
||||
classifier that broader observability filters build on.
|
||||
3. Reconcile credentials-lock PRs:
|
||||
decide whether #1636 or #1641 is canonical, then port any unique tests/fixes
|
||||
into the kept branch.
|
||||
4. Keep broad feature branches out of the merge queue until smaller fixes land:
|
||||
#1420, #1518, #1671, #1677, #1519, and #1488 all overlap hot surfaces.
|
||||
5. Only after the queue is quieter, launch implementation work outside these
|
||||
hot files.
|
||||
|
||||
Do not batch-rebase PRs from the conflict-cluster table. Rebase and validate one
|
||||
branch at a time so failures can be attributed to the branch being repaired.
|
||||
|
||||
## Next Safe Implementation Slice
|
||||
|
||||
Do not start in observability, settings, Tauri shell, agent harness, or iOS/i18n
|
||||
routing. The next safe slice is a small reconciliation task:
|
||||
|
||||
- Target: canonicalize #1462 disposition.
|
||||
- Work:
|
||||
- Refresh metadata first with `gh pr view 1462 --repo tinyhumansai/openhuman
|
||||
--json body,changedFiles,mergeStateStatus,mergeable,state,statusCheckRollup`.
|
||||
- Fetch `refs/pull/1462/head` and compare it against `upstream/main`.
|
||||
- Identify whether the branch contains any unique, still-relevant code.
|
||||
- Inspect lockfile/dependency churn in `Cargo.toml`, `Cargo.lock`,
|
||||
`pnpm-lock.yaml`, and `scripts/mock-api-core.mjs`; these may have been
|
||||
superseded by the `v0.53.43` release merge.
|
||||
- If no unique work remains, close #1462 as stale/superseded and link this
|
||||
handoff plus the canonical replacement PRs.
|
||||
- If useful work remains, create a new narrow issue/branch for only that
|
||||
surface; do not rebase the full PR.
|
||||
- Validation:
|
||||
- `git diff --name-status upstream/main...refs/tmp/pr-1462`
|
||||
- `git log --left-right --cherry-pick --oneline upstream/main...refs/tmp/pr-1462`
|
||||
- `gh pr view 1462 --repo tinyhumansai/openhuman --json body --jq .body |
|
||||
pnpm pr:checklist -`
|
||||
- `pnpm pr:checklist <generated-body>` only if a replacement PR is opened.
|
||||
|
||||
After #1462 is resolved, the next implementation-grade slice should be the
|
||||
observability-stack canonicalization because it is blocking the most active
|
||||
ready PRs.
|
||||
@@ -1,165 +0,0 @@
|
||||
# Agent prompt audit: grounding, tools, skills, MCPs
|
||||
|
||||
_Audit + remediation of the orchestrator and sub-agent system prompts, focused on
|
||||
anti-hallucination discipline and de-duplication. Inspired by
|
||||
[NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)'s named,
|
||||
reusable guidance blocks._
|
||||
|
||||
## Why this exists
|
||||
|
||||
The system prompts for the orchestrator and the ~27 sub-agents had accreted "slop":
|
||||
the same anti-fabrication paragraph copy-pasted (and quietly drifting) across many
|
||||
agents, specialist walkthroughs living in the wrong agent, a `## Safety` block that said
|
||||
nothing about grounding, and an em-dash rule restated inline despite a global suffix
|
||||
already banning it. There was **no single source of truth** for how an agent should treat
|
||||
its tool list, skills, or missing context, so each agent re-invented it.
|
||||
|
||||
## How the prompt system assembles (reference)
|
||||
|
||||
- `SystemPromptBuilder` (`src/openhuman/agent/prompts/builder.rs`) renders an ordered
|
||||
`Vec<Box<dyn PromptSection>>`, joins with `\n\n`, and appends `GLOBAL_STYLE_SUFFIX`.
|
||||
- Concrete sections live in `src/openhuman/agent/prompts/sections.rs`.
|
||||
- Three render paths produce a final system prompt:
|
||||
1. **Static chain** (`SystemPromptBuilder::with_defaults` / `for_subagent`).
|
||||
2. **Dynamic builders** (`from_dynamic`) — the orchestrator and ~25 other
|
||||
`agents/<id>/prompt.rs` files each hand-assemble their body by calling the free
|
||||
`render_*` helpers in `render_helpers.rs`. They funnel through
|
||||
`SystemPromptBuilder::build()` for the final wrap.
|
||||
3. **Narrow index-based renderer** (`render_subagent_system_prompt_with_format`) used by
|
||||
the sub-agent runner for spawned sub-agents (does NOT go through `build()`).
|
||||
- **Skills**: the orchestrator lists installed skills by name+desc; full SKILL.md bodies
|
||||
are injected at *turn time* by `src/openhuman/workflows/inject.rs`, not in the system
|
||||
prompt.
|
||||
- **MCPs**: not injected into any agent prompt. OpenHuman's MCP server only exposes tools
|
||||
to *external* clients; agents act through their own tool surface. There is no
|
||||
agent-facing MCP catalogue section, by design.
|
||||
- **KV-cache contract**: the system prompt is built once per session and frozen. Anything
|
||||
in the cache-friendly prefix must be byte-stable (no time / RNG / host).
|
||||
|
||||
## Findings
|
||||
|
||||
| # | Finding | Resolution |
|
||||
|---|---------|-----------|
|
||||
| 1 | Anti-fabrication paragraph ("never invent ids; a tool not in your list does not exist") copy-pasted across crypto / markets / integrations / account-admin / mcp-setup / morning-briefing / researcher with no shared source. | Centralized into `GROUNDING_BODY` (one source of truth). |
|
||||
| 2 | `## Safety` covered exfiltration / destructive commands but said nothing about grounding, not inventing tools/ids/files, or not ending a turn with a promise instead of an action. | Added the orthogonal grounding contract; kept `## Safety` for security. |
|
||||
| 3 | Orchestrator `prompt.md` (235 lines) carried specialist content: a full Apple-Music click-sequence + keyboard walkthrough, and a ~47-line cron JSON walkthrough. | Moved to `desktop_control_agent` / `scheduler_agent`; orchestrator is now a lean routing table (175 lines). |
|
||||
| 4 | Desktop-control instructions triple-duplicated across `SOUL.md`, orchestrator `prompt.md`, and `desktop_control_agent`. | `SOUL.md` + orchestrator trimmed to pointers; the worked example lives only in `desktop_control_agent`. |
|
||||
| 5 | Em-dash rule restated inline in `orchestrator/prompt.md` although `GLOBAL_STYLE_SUFFIX` already bans em-dashes in every output. | Removed the inline restatement. |
|
||||
| 6 | `omit_skills_catalog` rendering effect is inert (skills are agent-owned; the narrow renderer no longer emits a skills catalog). | **Retained.** See "Decisions" — the field is still live API surface (serde, CLI, 30+ TOMLs); removing it is a 60-site churn with no functional benefit. Documented as inert-but-retained. |
|
||||
|
||||
## The grounding contract
|
||||
|
||||
`GROUNDING_BODY` (`src/openhuman/agent/prompts/sections.rs`) is the canonical block.
|
||||
It is deliberately **generic**: every clause must be true for *every* agent, including the
|
||||
integrations executor. Agent-specific routing ("delegate external services", "pull slugs
|
||||
from `composio_list_tools`") stays in that agent's own `prompt.md`.
|
||||
|
||||
```
|
||||
## Grounding and tool use
|
||||
|
||||
- Your tools are exactly the ones listed in this prompt. You can only act through them.
|
||||
If a capability is not one of your tools, say so plainly rather than pretending it exists.
|
||||
- Never invent tool names, arguments, ids, slugs, file paths, URLs, chain ids, addresses,
|
||||
quotes, metrics, or any other value. If you do not have it from a tool result or the user,
|
||||
ask for it or look it up with a tool.
|
||||
- Use your tools to act. Do not just describe what you would do and stop, and never end a
|
||||
turn with a promise of future action: do it now, or hand back a concrete result.
|
||||
- Never substitute plausible looking but fabricated output (made up data, invented file
|
||||
contents, synthesised tool or API responses) for results you could not actually produce.
|
||||
If a step failed, say it failed.
|
||||
- Ground every factual claim in evidence you actually observed: a tool result, the user's
|
||||
message, or cited memory. If the evidence is missing, partial, or truncated, say so or
|
||||
fetch more instead of guessing.
|
||||
- Skills run only via `run_workflow`, and only the skills listed as installed exist. Do not
|
||||
invent skill ids.
|
||||
```
|
||||
|
||||
The first, third, and fourth bullets mirror Hermes's `TOOL_USE_ENFORCEMENT_GUIDANCE`
|
||||
("use tools to act, never end a turn with a promise") and `TASK_COMPLETION_GUIDANCE`
|
||||
("never substitute fabricated output for results you couldn't produce").
|
||||
|
||||
### How it reaches every agent
|
||||
|
||||
Rather than splice it into all ~26 dynamic `prompt.rs` files (drift-prone), the contract
|
||||
is appended at the two chokepoints that every prompt funnels through, just like
|
||||
`GLOBAL_STYLE_SUFFIX`:
|
||||
|
||||
- `SystemPromptBuilder::build()` — covers the static chain and all dynamic agents.
|
||||
- `render_subagent_system_prompt_with_format()` — covers spawned sub-agents.
|
||||
|
||||
Both reference the same `GROUNDING_BODY` const, so they can never drift. A `render_grounding()`
|
||||
helper and a `GroundingSection` PromptSection are also exposed for explicit use. Placement is
|
||||
near the tail (just before the output-style rules) so it reads as a closing contract; the
|
||||
text is byte-stable, so it stays in the cache-friendly prefix.
|
||||
|
||||
Coverage is asserted by `grounding_contract_appended_to_every_build_path`
|
||||
(`mod_tests.rs`), which checks the marker appears exactly once across the static, sub-agent,
|
||||
and dynamic build paths, plus an assertion in the narrow-renderer test.
|
||||
|
||||
## Decisions / deviations from the original plan
|
||||
|
||||
- **No `omit_grounding` flag.** The contract is always on. It is universal
|
||||
anti-hallucination guidance and the text is generic enough to be true for every agent, so
|
||||
threading an opt-out through serde + every caller would be churn for zero benefit.
|
||||
- **`omit_skills_catalog` retained, not removed.** Its rendering effect is inert, but the
|
||||
field is wired into serde defaults, the agent CLI dump, and 30+ `agent.toml` files.
|
||||
Removing it is a 60-site change with real regression surface and no functional payoff,
|
||||
which is out of proportion for a prompt audit. Flagged here instead.
|
||||
- **Cron delegate-vs-direct contradiction left in place.** The orchestrator decision tree
|
||||
routes scheduling to `schedule_task`, while the (now compressed) scheduling rules still
|
||||
let it call `cron_add` directly when that tool is in its list. Resolving which is
|
||||
authoritative is a behavioral change beyond this audit; the load-bearing rules
|
||||
(never via `run_workflow`; always confirm) were preserved.
|
||||
|
||||
## Touched files
|
||||
|
||||
- `src/openhuman/agent/prompts/sections.rs` — `GROUNDING_BODY` + `GroundingSection`.
|
||||
- `src/openhuman/agent/prompts/builder.rs` — central append in `build()`.
|
||||
- `src/openhuman/agent/prompts/render_helpers.rs` — `render_grounding()` + narrow-renderer append.
|
||||
- `src/openhuman/agent/prompts/mod.rs` / `mod_tests.rs` — re-export + coverage tests.
|
||||
- `src/openhuman/agent_registry/agents/{crypto_agent,markets_agent}/prompt.md` — drop generic clause.
|
||||
- `src/openhuman/agent_registry/agents/orchestrator/prompt.md` — slimmed routing table.
|
||||
- `src/openhuman/agent_registry/agents/{desktop_control_agent,scheduler_agent}/prompt.md` — received the moved walkthroughs.
|
||||
- `src/openhuman/agent/prompts/SOUL.md` — trimmed duplicated desktop sequence.
|
||||
|
||||
## Verifying
|
||||
|
||||
```bash
|
||||
GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib agent::prompts::
|
||||
GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib agent_registry::agents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider prompt-cache behaviour (#3939)
|
||||
|
||||
The byte-stable prompt prefix above is what makes KV-cache reuse possible. How
|
||||
much of that reuse a user actually gets depends on the backend they route to.
|
||||
`Provider::prompt_cache_capabilities()` (`src/openhuman/inference/provider/traits.rs`,
|
||||
`PromptCacheCapabilities`) makes that contract explicit per provider:
|
||||
|
||||
| Provider | automatic prefix cache | reports cached input tokens | explicit cache-control | thread/session grouping |
|
||||
|---|---|---|---|---|
|
||||
| OpenHuman backend | ✓ | ✓ | — | ✓ (`thread_id` extension) |
|
||||
| OpenAI / OpenRouter / GMI (OpenAI-compatible) | ✓ | ✓ | — | — (prefix identity) |
|
||||
| Other / custom / LM Studio compatible | conservative default — all `false` | | | |
|
||||
|
||||
Notes:
|
||||
|
||||
- **Conservative by default.** Unknown or custom OpenAI-compatible slugs report
|
||||
no caching, so we never assume cache hits or send cache-only request fields a
|
||||
provider may not honour. Opting a provider in is a one-line edit to
|
||||
`prompt_cache_for_compatible_slug` (`compatible.rs`) once verified.
|
||||
- **Usage normalization is provider-agnostic.** `extract_usage` already folds the
|
||||
OpenAI `usage.prompt_tokens_details.cached_tokens` shape and the
|
||||
`openhuman.usage.cached_input_tokens` extension into
|
||||
`UsageInfo.cached_input_tokens`, so cached-prefix cost accounting
|
||||
(`src/openhuman/agent/cost.rs`) is exact wherever the provider reports it.
|
||||
- **No OpenHuman-only leakage.** `explicit_cache_control` stays `false` for every
|
||||
OpenAI-compatible provider (the chat-completions API has no such field), and
|
||||
`thread_id` grouping is declared only on `OpenHumanBackendProvider`.
|
||||
|
||||
Follow-ups (not in this slice): explicit cache-control request shaping for
|
||||
providers that support it (e.g. Anthropic `cache_control`), a `ChatRequest`
|
||||
cache-boundary marker, and extending cached-token parsing to non-OpenAI usage
|
||||
shapes (e.g. DeepSeek `prompt_cache_hit_tokens`).
|
||||
@@ -1,204 +0,0 @@
|
||||
# iOS Client Setup
|
||||
|
||||
This document covers everything a developer needs to build, run, and test the OpenHuman iOS client.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS 14+ with Xcode 15.4+
|
||||
- iOS 17+ physical device or simulator
|
||||
- Rust toolchain with `aarch64-apple-ios` target
|
||||
- pnpm (version pinned in root `package.json`)
|
||||
- Apple Developer account with a provisioning profile
|
||||
|
||||
```bash
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Initial setup
|
||||
|
||||
Run the helper script from the repo root. It calls `tauri ios init` with the correct working directory and prints next steps.
|
||||
|
||||
```bash
|
||||
bash scripts/ios-init.sh
|
||||
```
|
||||
|
||||
`tauri ios init` scaffolds `app/src-tauri-mobile/gen/apple/`. That directory is **gitignored** (it contains bundle-identifier-specific Xcode project files that differ per developer account).
|
||||
|
||||
### Info.plist privacy keys
|
||||
|
||||
`tauri ios init` creates a generated `Info.plist` at:
|
||||
|
||||
```
|
||||
app/src-tauri-mobile/gen/apple/<bundle-id>_iOS/Info.plist
|
||||
```
|
||||
|
||||
`scripts/ios-init.sh` injects these privacy keys into the generated plist:
|
||||
|
||||
```xml
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>OpenHuman uses the camera to scan the pairing QR code from your desktop.</string>
|
||||
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>OpenHuman uses the microphone for push-to-talk voice messages.</string>
|
||||
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>OpenHuman uses on-device speech recognition to transcribe your voice messages.</string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development workflow
|
||||
|
||||
```bash
|
||||
# Start the iOS dev build (hot-reload via Vite, deployed to simulator or device):
|
||||
pnpm tauri:ios:dev
|
||||
|
||||
# From the repo root:
|
||||
pnpm tauri:ios:dev
|
||||
```
|
||||
|
||||
The `tauri:ios:dev` script uses `@tauri-apps/cli@^2` directly (via `npx --package`), **not** the vendored CEF-aware CLI. The CEF CLI is only needed for the desktop build.
|
||||
|
||||
Set your development team in Xcode (generated project > Signing & Capabilities) before deploying to a physical device.
|
||||
|
||||
---
|
||||
|
||||
## Production build
|
||||
|
||||
```bash
|
||||
pnpm tauri:ios:build
|
||||
# or from repo root:
|
||||
pnpm tauri:ios:build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## App Store Connect delivery
|
||||
|
||||
`.github/workflows/ios-appstore.yml` builds a signed `iphoneos` archive, exports an IPA, uploads the IPA to App Store Connect/TestFlight with `altool`, and stores the IPA + dSYMs as GitHub Actions artifacts.
|
||||
|
||||
Run it from GitHub Actions > iOS App Store. Inputs:
|
||||
|
||||
- `ref` -- optional git ref to build.
|
||||
- `build_number` -- optional `CFBundleVersion`; defaults to `github.run_number`.
|
||||
- `upload_to_app_store_connect` -- set `false` for a signed archive/export dry run.
|
||||
|
||||
Required GitHub environment: `App-Store`.
|
||||
|
||||
Required secrets:
|
||||
|
||||
- `APPLE_TEAM_ID` -- Apple Developer Team ID.
|
||||
- `IOS_KEYCHAIN_PASSWORD` -- temporary CI keychain password.
|
||||
- `IOS_DISTRIBUTION_CERTIFICATE_BASE64` -- base64-encoded `.p12` Apple Distribution certificate.
|
||||
- `IOS_DISTRIBUTION_CERTIFICATE_PASSWORD` -- password for that `.p12`.
|
||||
- `IOS_APPSTORE_PROVISIONING_PROFILE_BASE64` -- base64-encoded App Store provisioning profile for `com.tinyhumansai.openhuman`.
|
||||
- `APP_STORE_CONNECT_API_KEY_ID` -- App Store Connect API key ID.
|
||||
- `APP_STORE_CONNECT_ISSUER_ID` -- App Store Connect issuer ID.
|
||||
- `APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64` -- base64-encoded `AuthKey_<key id>.p8`.
|
||||
|
||||
Local encoding helpers:
|
||||
|
||||
```bash
|
||||
base64 -i ios_distribution.p12 | pbcopy
|
||||
base64 -i OpenHuman_AppStore.mobileprovision | pbcopy
|
||||
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy
|
||||
```
|
||||
|
||||
The workflow uploads a build to App Store Connect. It does not submit the build for App Review; that remains a deliberate App Store Connect action.
|
||||
|
||||
### Local upload script
|
||||
|
||||
After downloading an App Store provisioning profile and App Store Connect API key, you can build/export/upload from this Mac:
|
||||
|
||||
```bash
|
||||
TEAM_ID=XXXXXXXXXX \
|
||||
IOS_APPSTORE_PROVISIONING_PROFILE_PATH=/path/to/OpenHuman_AppStore.mobileprovision \
|
||||
ASC_KEY_ID=XXXXXXXXXX \
|
||||
ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
|
||||
ASC_KEY_PATH=/path/to/AuthKey_XXXXXXXXXX.p8 \
|
||||
UPLOAD=1 \
|
||||
scripts/ios-appstore-upload.sh
|
||||
```
|
||||
|
||||
Use `UPLOAD=0` to stop after IPA export.
|
||||
|
||||
### Updating without a new App Store build
|
||||
|
||||
iOS cannot self-update native code or replace the installed app binary outside App Store/TestFlight distribution. For OpenHuman, this means changes to Rust, Tauri plugins, native permissions, bundled frontend code, or the app shell need a new reviewed build.
|
||||
|
||||
Safe server-side updates include model/provider configuration, feature flags, prompt/content changes, remote data, and backend behavior that the shipped client already knows how to render. Be conservative with remote JavaScript or plugin-style features: Apple allows some software/content delivered outside the binary under specific rules, but it must stay within App Review limits and must not expose native platform APIs without permission.
|
||||
|
||||
---
|
||||
|
||||
## Pairing flow
|
||||
|
||||
```
|
||||
Desktop iOS
|
||||
| |
|
||||
|-- Settings > Devices > "Pair" |
|
||||
|-- devices_create_pairing RPC |
|
||||
| (backend issues channelId, |
|
||||
| pairingToken, sessionToken) |
|
||||
|-- QR shown |
|
||||
| scan QR --------|
|
||||
| (extract cid, |
|
||||
| pt, cpk, rpc?) |
|
||||
| iOS connects |
|
||||
| to backend |
|
||||
| tunnel:connect |
|
||||
| (role:client, |
|
||||
| channelId, |
|
||||
| pairingToken) |
|
||||
| backend returns |
|
||||
| iOS sessionToken|
|
||||
| X25519 handshake|
|
||||
| over tunnel |
|
||||
|<-- DevicePaired event |
|
||||
|-- device appears in Devices list |
|
||||
```
|
||||
|
||||
Transport selection (handled by `TransportManager`):
|
||||
|
||||
1. LAN HTTP -- fast, zero-latency, requires same network.
|
||||
2. Socket.io tunnel -- E2E encrypted via XChaCha20-Poly1305 over X25519 key agreement.
|
||||
3. Cloud HTTP -- fallback when LAN and tunnel are unreachable.
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
- The tunnel backend is a **blind forwarder**. It never sees plaintext payloads.
|
||||
- `pairingToken` is single-use and hashed at rest on the backend.
|
||||
- `sessionToken` is per-peer, revocable from the desktop Devices panel.
|
||||
- X25519 key agreement runs on first connect; the derived symmetric key is stored in-memory for the session.
|
||||
- **TODO (follow-up PR):** migrate the iOS symmetric key to the iOS Keychain for persistence across app restarts without re-pairing.
|
||||
|
||||
---
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Single backend instance only (no multi-region failover).
|
||||
- No APNs push notifications -- app must be foregrounded for real-time delivery.
|
||||
- Event-driven pairing detection on the desktop side uses 2-second polling until an SSE/socket event bridge lands.
|
||||
|
||||
---
|
||||
|
||||
## CI
|
||||
|
||||
The `.github/workflows/ios-compile.yml` workflow runs as an iOS compile sanity check. It provides:
|
||||
|
||||
- **Hard gate:** `cargo check` on the iOS target for `app/src-tauri-mobile` and a host-target check for `packages/tauri-plugin-ptt`.
|
||||
- **Hard gate:** TypeScript compile (`pnpm compile`).
|
||||
- **Hard gate:** iOS-related Vitest suites.
|
||||
|
||||
Full signed App Store builds run through `.github/workflows/ios-appstore.yml`.
|
||||
|
||||
---
|
||||
|
||||
## Backend dependency
|
||||
|
||||
The tunnel transport requires `tinyhumansai/backend#709` to be merged and deployed before end-to-end pairing works. The `devices_create_pairing` RPC will return a tunnel registration error until that backend is live.
|
||||
@@ -1,24 +0,0 @@
|
||||
# App Review Information Draft
|
||||
|
||||
Fastlane uploads `fastlane/metadata/review_information/` automatically when that directory exists. Keep draft review details outside the Fastlane metadata tree until the real review contact email and phone number are ready.
|
||||
|
||||
## Contact
|
||||
|
||||
- First name: Steven
|
||||
- Last name: Enamakel
|
||||
- Email: TODO
|
||||
- Phone: TODO
|
||||
|
||||
## Notes
|
||||
|
||||
OpenHuman for iPhone is a companion app for the OpenHuman desktop runtime.
|
||||
|
||||
To review the app:
|
||||
|
||||
1. Install and open the submitted iOS build.
|
||||
2. Install and open the OpenHuman desktop app on macOS, Windows, or Linux.
|
||||
3. In the desktop app, open Settings > Devices and choose Pair phone.
|
||||
4. Use the iPhone app to scan the pairing QR code.
|
||||
5. After pairing, test text chat and push-to-talk voice input from the iPhone.
|
||||
|
||||
The iOS app requires camera permission for QR pairing and microphone permission for push-to-talk voice input. It does not function as a standalone assistant without a paired OpenHuman desktop runtime.
|
||||
@@ -1,104 +0,0 @@
|
||||
# App Store Listing
|
||||
|
||||
This is the prepared product-page kit for the OpenHuman iOS companion app.
|
||||
|
||||
## App Store Connect Fields
|
||||
|
||||
- App name: `OpenHuman`
|
||||
- Subtitle: `AI companion for your desktop`
|
||||
- Bundle ID: `com.tinyhumansai.openhuman`
|
||||
- SKU: `com.tinyhumansai.openhuman`
|
||||
- Primary category: `Productivity`
|
||||
- Secondary category: `Utilities`
|
||||
- Copyright: `2026 Tiny Humans AI`
|
||||
- Support URL: `https://tinyhumans.ai/openhuman`
|
||||
- Marketing URL: `https://tinyhumans.ai/openhuman`
|
||||
- Privacy Policy URL: `https://tinyhumans.gitbook.io/openhuman/legal/privacy-policy`
|
||||
|
||||
Metadata files live in `fastlane/metadata/en-US/`.
|
||||
|
||||
## Description
|
||||
|
||||
Use `fastlane/metadata/en-US/description.txt`.
|
||||
|
||||
## Keywords
|
||||
|
||||
Use `fastlane/metadata/en-US/keywords.txt`.
|
||||
|
||||
## App Icon
|
||||
|
||||
The App Store icon is already included in the iOS build:
|
||||
|
||||
- `app/src-tauri-mobile/icons/store/appstore.png`
|
||||
- `app/src-tauri-mobile/icons/ios/AppIcon.appiconset/1024.png`
|
||||
|
||||
Both are 1024 x 1024 PNG assets.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Generate the 6.9-inch iPhone screenshot set:
|
||||
|
||||
```bash
|
||||
pnpm --dir app exec node ../scripts/ios-appstore-assets.mjs
|
||||
```
|
||||
|
||||
Upload the generated files from:
|
||||
|
||||
```text
|
||||
fastlane/screenshots/en-US/
|
||||
```
|
||||
|
||||
Use these in App Store Connect under the iPhone 6.9-inch display screenshot slot.
|
||||
|
||||
You can also push the generated screenshots with Fastlane:
|
||||
|
||||
```bash
|
||||
ASC_KEY_ID=9KD934428C \
|
||||
ASC_ISSUER_ID=69a6de8b-cc07-47e3-e053-5b8c7c11a4d1 \
|
||||
ASC_KEY_PATH=/Users/enamakel/Downloads/AuthKey_9KD934428C.p8 \
|
||||
ASC_APP_VERSION=1.0 \
|
||||
fastlane ios push_screenshots
|
||||
```
|
||||
|
||||
Metadata can be pushed with the App Store Connect API helper:
|
||||
|
||||
```bash
|
||||
ASC_KEY_ID=9KD934428C \
|
||||
ASC_ISSUER_ID=69a6de8b-cc07-47e3-e053-5b8c7c11a4d1 \
|
||||
ASC_KEY_PATH=/Users/enamakel/Downloads/AuthKey_9KD934428C.p8 \
|
||||
ASC_APP_ID=6761229174 \
|
||||
scripts/ios-appstore-metadata.mjs
|
||||
```
|
||||
|
||||
## App Review Notes
|
||||
|
||||
Use `fastlane/metadata/review_information/notes.txt`.
|
||||
|
||||
Before submitting, replace the TODO contact fields in:
|
||||
|
||||
- `fastlane/metadata/review_information/email_address.txt`
|
||||
- `fastlane/metadata/review_information/phone_number.txt`
|
||||
|
||||
## Privacy Answers Draft
|
||||
|
||||
Use this as the starting point for App Privacy in App Store Connect:
|
||||
|
||||
- Camera: used to scan the desktop pairing QR code.
|
||||
- Microphone: used for push-to-talk voice messages.
|
||||
- Speech recognition: used to transcribe voice messages.
|
||||
- Identifiers/session data: pairing/session data may be used to connect the phone to the paired OpenHuman desktop runtime.
|
||||
- User content: messages and voice transcripts are sent to the paired OpenHuman runtime to provide assistant responses.
|
||||
|
||||
Before submitting, make sure the App Privacy answers match the production backend/runtime behavior.
|
||||
|
||||
## Current Apple Requirements Checked
|
||||
|
||||
Apple currently requires one to ten screenshots for each platform localization, accepts `.jpeg`, `.jpg`, and `.png`, and allows high-resolution iPhone screenshots to scale down to smaller sizes. Apple lists the 6.9-inch portrait sizes as accepted high-resolution iPhone screenshot sizes.
|
||||
|
||||
Apple currently limits:
|
||||
|
||||
- App name: 30 characters
|
||||
- Subtitle: 30 characters
|
||||
- Promotional text: 170 characters
|
||||
- Description: 4000 characters
|
||||
- Keywords: 100 bytes
|
||||
@@ -1,47 +0,0 @@
|
||||
# OpenHuman iOS Privacy Policy Draft
|
||||
|
||||
Last updated: June 6, 2026
|
||||
|
||||
OpenHuman for iPhone is a companion app for the OpenHuman desktop runtime. It lets you pair your phone with your desktop OpenHuman app, send text messages, and use push-to-talk voice input.
|
||||
|
||||
The App Store Connect listing uses the published TinyHumans privacy policy:
|
||||
|
||||
https://tinyhumans.gitbook.io/openhuman/legal/privacy-policy
|
||||
|
||||
This draft is retained as iOS-specific reference material for review notes and privacy-answer preparation.
|
||||
|
||||
## Data The App Uses
|
||||
|
||||
OpenHuman for iPhone may process:
|
||||
|
||||
- Pairing data, such as a short-lived QR pairing token and device connection identifiers.
|
||||
- Messages you send to your paired OpenHuman desktop runtime.
|
||||
- Voice transcripts created from push-to-talk input.
|
||||
- Basic connection state needed to route requests to the paired desktop runtime.
|
||||
|
||||
## Permissions
|
||||
|
||||
The iOS app requests:
|
||||
|
||||
- Camera access to scan the desktop pairing QR code.
|
||||
- Microphone access for push-to-talk voice messages.
|
||||
- Speech recognition access to transcribe push-to-talk messages.
|
||||
|
||||
## How Data Is Used
|
||||
|
||||
Data is used to:
|
||||
|
||||
- Connect your phone to your paired OpenHuman desktop runtime.
|
||||
- Send your messages and voice transcripts to the paired runtime.
|
||||
- Receive assistant responses from the paired runtime.
|
||||
- Maintain connection health while the app is in use.
|
||||
|
||||
## Desktop Runtime
|
||||
|
||||
The iPhone app is not a standalone assistant. Your long-term workspace, memory, configuration, and integrations are managed by your OpenHuman desktop setup. The exact data handling behavior depends on how you configure OpenHuman desktop, including whether you use managed services, local providers, or custom providers.
|
||||
|
||||
## Contact
|
||||
|
||||
For support, visit:
|
||||
|
||||
https://tinyhumans.ai/openhuman
|
||||
@@ -1,181 +0,0 @@
|
||||
# Skills Runner Unification
|
||||
|
||||
**Status**: Proposed — implementation in progress on `run/codegraph-full`.
|
||||
**Owner**: codegraph-skills track.
|
||||
**Related**: PR #2802 (dev-workflow active-config UX), PR #2864 (smart issue selection / cron prompt embedding), the `feat/codegraph-skills` series that introduced `SkillsRunnerBody`.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
We currently maintain **two parallel scheduling UIs** for the same underlying primitive (`openhuman.cron_*` RPCs against an agent `prompt`):
|
||||
|
||||
1. **`SkillsRunnerBody`** (generic, every bundled skill) — picks any skill, dynamically renders inputs from `openhuman.skills_describe`, can run-now or save-as-cron. Lists saved schedules as a read-only summary with run/remove buttons.
|
||||
2. **`DevWorkflowPanel`** (dev-workflow-only) — bespoke smart-issue-picker UI for `dev-workflow`, with an "active configuration" card, enable/disable toggle, embedded run history with per-run output viewer.
|
||||
|
||||
`DevWorkflowPanel` is just a hard-coded specialization of what `SkillsRunnerBody` already does — same cron RPC, same `agent` job type, same prompt-template scheme. The polish that lives in `DevWorkflowPanel` (toggle, run-history-with-output, active-config card) should be lifted into `SkillsRunnerBody`, and the dev-workflow-specific GitHub issue picker should be extracted as a conditionally-mounted sub-component. Then `DevWorkflowPanel` retires.
|
||||
|
||||
---
|
||||
|
||||
## Current state
|
||||
|
||||
### `app/src/components/skills/SkillsRunnerBody.tsx` (990 lines)
|
||||
|
||||
Generic skill runner used by both Settings → Developer Options → Skills Runner AND the `/skills` "Runners" tab.
|
||||
|
||||
| Concern | Location |
|
||||
| --- | --- |
|
||||
| Per-skill cron-name prefix | line 94 (`CRON_NAME_PREFIX = 'skill-run-'`) |
|
||||
| Cron-name builder (skill + inputs hash) | lines 100-113 |
|
||||
| Generic agent-prompt builder (re-fires `skill_run` at tick) | lines 116-127 |
|
||||
| `scheduledJobs` state (filtered by name prefix) | line 233 |
|
||||
| Saved-schedule render list | lines 828-875 |
|
||||
| Per-schedule actions | `handleRunJobNow` (526-536), `handleRemoveJob` (538-548) — **no enable/disable** |
|
||||
| Recent-runs viewer (cross-skill or scoped) | lines 882-985 |
|
||||
| Inline log viewer (per run, auto-tail) | lines 440-519 |
|
||||
|
||||
### `app/src/components/settings/panels/DevWorkflowPanel.tsx` (792 lines)
|
||||
|
||||
| Concern | Location |
|
||||
| --- | --- |
|
||||
| `dev-workflow-`-prefixed cron-name | line 376 |
|
||||
| Fork detection via Composio | lines 190-303 |
|
||||
| Branch dropdown | lines 707-732 |
|
||||
| Embedded skill-instructions prompt | lines 338-373 |
|
||||
| **Active-config card** (rendered at top when any job exists) | lines 486-647 |
|
||||
| **Enable/disable toggle** | lines 433-444 + render at 502-516 |
|
||||
| **Run-history rows with per-run expandable output** | lines 591-645 (render), 306-322 (fetch via `openhumanCronRuns`) |
|
||||
| **`last_output` viewer** | lines 580-589 |
|
||||
|
||||
### What's NOT in `SkillsRunnerBody` today
|
||||
|
||||
- Enable/disable toggle per schedule (the cron RPC supports `openhumanCronUpdate(id, { enabled })` — DevWorkflowPanel:439 — already wired generically).
|
||||
- Per-schedule run history (`openhumanCronRuns(jobId, limit)` exists in `cron.ts`; SkillsRunnerBody currently shows recent runs only via `skillsApi.recentRuns()` which scans the skill log directory, not the cron `runs` table).
|
||||
- "Active config card" — surface the most-recently-active schedule prominently rather than as one row in a list.
|
||||
- Anything dev-workflow-specific (the GitHub repo / fork / branch picker workflow with smart auto-detection).
|
||||
|
||||
---
|
||||
|
||||
## Why unify
|
||||
|
||||
1. **UX symmetry.** Users with multiple `dev-workflow` schedules across different repos (a real use case Cyrus's PR #2802 was prepping for) need exactly the same UI as users with multiple `pr-review-shepherd` schedules across different repos. Today there's a privileged surface for one skill.
|
||||
2. **Cron RPCs are already generic.** `openhuman.cron_update`, `cron_runs`, `cron_run` all take a `job_id` — no skill-specific logic in the core. We have zero RPC work to do.
|
||||
3. **`DevWorkflowPanel` is a hard-coded specialization.** It bakes `dev-workflow-` into the cron name and the prompt template into the file. Both are anti-patterns once the generic runner exists: `SkillsRunnerBody` already builds prompts that re-fire `skill_run`, which routes through the skill registry and gets the up-to-date `system.md`/inputs without re-deploying the panel.
|
||||
4. **Smaller surface to maintain.** Two views means two test suites, two i18n key namespaces (`settings.devWorkflow.*` and `settings.skillsRunner.*`), two render bugs to fix. Cyrus shipped 8 fixes to DevWorkflowPanel in 4 weeks; every one would have benefited the generic runner if they'd shared code.
|
||||
|
||||
---
|
||||
|
||||
## Unified information architecture
|
||||
|
||||
```
|
||||
/skills
|
||||
├─ Library tab (existing)
|
||||
└─ Runners tab (existing — this is where SkillsRunnerBody lives)
|
||||
|
||||
Runners tab body (SkillsRunnerBody, after unification):
|
||||
|
||||
┌─ Skill picker ─────────────────────────────────────────────┐
|
||||
│ Skill: [ dev-workflow ▾ ] │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ Saved schedules for this skill ───────────────────────────┐
|
||||
│ │
|
||||
│ ★ ACTIVE dev-workflow-tinyhumansai-openhuman │
|
||||
│ every 2 hours · next run in 23m │
|
||||
│ last: ok · 47s ago [⏵ toggle] │
|
||||
│ [Run now] [▾ history (5)] [Remove] │
|
||||
│ ▾ history │
|
||||
│ 2026-05-29 13:01 ok 51s │
|
||||
│ <expandable per-run output pre> │
|
||||
│ 2026-05-29 11:01 ok 49s │
|
||||
│ │
|
||||
│ ────────────────────────────────────────────────────── │
|
||||
│ │
|
||||
│ ○ dev-workflow-graycyrus-openhuman │
|
||||
│ daily @ 9am · paused │
|
||||
│ last: error · 4h ago [⏵ toggle] │
|
||||
│ [Run now] [▾ history] [Remove] │
|
||||
│ │
|
||||
│ [ + Add new schedule for dev-workflow ] │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ Configure & run ──────────────────────────────────────────┐
|
||||
│ [skill description — what_to_use] │
|
||||
│ │
|
||||
│ Inputs: │
|
||||
│ <schema-driven form, today> │
|
||||
│ │
|
||||
│ When skill_id === 'dev-workflow', ALSO render: │
|
||||
│ <SmartIssuePicker subcomponent — fork detection, │
|
||||
│ GitHub-connected repo dropdown, branch dropdown> │
|
||||
│ │
|
||||
│ [Run now] [Save as schedule …] │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ Recent runs (skill-scoped) ───────────────────────────────┐
|
||||
│ (existing — unchanged; reads skill_log scan) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The **★ ACTIVE** treatment lifts DevWorkflowPanel's "active configuration" pattern but generalises it: any enabled schedule gets the same emphasis. If multiple are enabled, the most recently run one is "active" — first in the list, larger card.
|
||||
- The "+ Add new schedule" affordance just reveals the existing inputs form (we don't need a second form; we just gate the existing one behind a disclosure so the saved-schedules list reads cleaner).
|
||||
- Run history per schedule uses `openhuman.cron_runs` (the same RPC DevWorkflowPanel already uses) — this is *separate* from "Recent runs" at the bottom which uses `skillsApi.recentRuns()` scanning the skill log directory. Both have value: cron-run history is structured per-schedule with status/duration; skill log scan catches run-now invocations that don't go through a schedule.
|
||||
|
||||
---
|
||||
|
||||
## Migration / deprecation path
|
||||
|
||||
### Phase 1 — Design doc (this file).
|
||||
### Phase 2 — Smoke prototype.
|
||||
- Add enable/disable toggle to existing `scheduledJobs.map(...)` row in `SkillsRunnerBody.tsx`.
|
||||
- Mirror `openhumanCronUpdate(jobId, { enabled: !job.enabled })` from DevWorkflowPanel:439 exactly.
|
||||
- New i18n keys: `settings.skillsRunner.scheduleEnabled`, `settings.skillsRunner.scheduleDisabled` (full 12-locale chunk parity).
|
||||
- Vitest unit coverage for toggle.
|
||||
|
||||
### Phase 3 — Full incremental implementation (one commit per chunk).
|
||||
1. Per-schedule run-history + expandable output viewer (port DevWorkflowPanel:593-635).
|
||||
2. Active-config card pattern (port DevWorkflowPanel:502-547).
|
||||
3. Extract `SmartIssuePicker` subcomponent into `app/src/components/skills/SmartIssuePicker.tsx`; conditionally render in `SkillsRunnerBody` when `skillId === 'dev-workflow'` (with a TODO for a schema-driven `[input].picker = "github-issue"` upgrade — see Open questions).
|
||||
4. Deprecate `DevWorkflowPanel`: replace its body with a one-line "moved to /skills" notice + nav link; OR strip it from the Settings nav and delete the file. Update or remove `DevWorkflowPanel.test.tsx` accordingly. Decision in Phase 3 chunk 4 once we see whether the Settings nav strip is clean.
|
||||
5. i18n parity audit (`pnpm i18n:check`) — fold any new keys into all 12 non-English chunk files.
|
||||
|
||||
### Phase 4 — Verification.
|
||||
- Playwright via CDP `http://127.0.0.1:19222` against the running dev app on Vite port 1428.
|
||||
- Confirm: schedule toggle + expand work; smart-issue picker shows only for `dev-workflow`; switching to `github-issue-crusher` / `pr-review-shepherd` shows the generic form.
|
||||
- Save `G:/tmp/oh-skills-unified.png`.
|
||||
- Run `pnpm typecheck` + `pnpm debug unit` on changed files.
|
||||
|
||||
---
|
||||
|
||||
## Risk + test plan
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
| --- | --- |
|
||||
| **Breaking existing dev-workflow users** — they have a configured cron job named `dev-workflow-<repo>`; the new generic runner uses `skill-run-<skillId>-<hash>` as the prefix. | Filter saved-schedules list by **both** prefixes when `skillId === 'dev-workflow'` so existing jobs surface. Don't migrate names — they keep working. |
|
||||
| **i18n drift** — there's already significant pre-existing drift in `en-N.ts` chunks for `settings.skillsRunner.*` keys (audit log shows ~50 keys in `en.ts` not in chunks). | Address chunk drift for the **new** keys this PR adds (toggle, history, smart-picker). Pre-existing drift is out of scope but worth a follow-up. |
|
||||
| **`DevWorkflowPanel` removal breaks the deep link or settings nav route.** | Check `settings/AppSettings.tsx` (or wherever the nav is wired) and either preserve the route as a redirect to `/skills` or update the nav entry. |
|
||||
| **Coverage gate (≥80% on changed lines)** on a 990-line component. | Add focused tests per Phase 3 chunk: toggle test (Phase 2), history-expand test (chunk 1), active-card render test (chunk 2), conditional-picker test (chunk 3). |
|
||||
| **Stale-component test deletion regressing dev-workflow coverage.** | If `DevWorkflowPanel.test.tsx` is deleted, ensure equivalent coverage exists in `SkillsRunnerBody.test.tsx` for the smart-picker path. |
|
||||
|
||||
### Test plan
|
||||
|
||||
Per phase commit:
|
||||
|
||||
- **Phase 2**: `SkillsRunnerBody.test.tsx` — render one saved job, click toggle, assert `openhumanCronUpdate` called with `{ enabled: false }`, assert refresh-list invoked.
|
||||
- **Phase 3.1**: render one saved job with `runHistory`, click expand, assert per-run output `<pre>` visible. Assert `openhumanCronRuns(jobId, 5)` called on render.
|
||||
- **Phase 3.2**: render multiple jobs, assert most-recent-active sorted to top with `data-testid="active-schedule"` (or equivalent). Assert non-enabled jobs sorted below.
|
||||
- **Phase 3.3**: render with `skillId === 'dev-workflow'`, assert `SmartIssuePicker` present. Render with `skillId === 'github-issue-crusher'`, assert it's absent. Subcomponent's own tests cover Composio loading/error paths (move from `DevWorkflowPanel.test.tsx`).
|
||||
- **Phase 3.4**: if DevWorkflowPanel becomes a redirect, assert nav still routes correctly; if deleted, delete the test file.
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Schema-driven pickers vs. hard-coded `if (skillId === 'dev-workflow')`.** The clean answer is to extend `skill.toml`'s `[[inputs]]` schema with an optional `picker = "github-issue"` field, and let the runner route to a `SmartIssuePicker` subcomponent based on the picker key. The shortcut answer is the hard-coded `if`. Phase 3 chunk 3 ships the shortcut with a `TODO(picker-schema)` comment. The schema upgrade is a follow-up issue worth filing — it would also benefit `RepoPicker` and `BranchPicker`, which today route by *name convention* (`REPO_INPUT_NAMES` / `BRANCH_INPUT_NAMES` sets in SkillsRunnerBody:42-55), which is brittle.
|
||||
2. **One enabled schedule per (skill, inputs) combo, or many?** Today DevWorkflowPanel allows only one (it looks up `name?.startsWith('dev-workflow')` and updates in place). `SkillsRunnerBody` allows many (the cron-name hash includes input values, so two different repos produce two jobs). The unified UX should keep "many" — but display only one as the prominent "ACTIVE" card. Pre-existing `dev-workflow-<repo>` jobs will surface in the list once we add the dual-prefix filter.
|
||||
3. **Run history retention.** DevWorkflowPanel pulls last 5; SkillsRunnerBody's bottom "Recent runs" scans last 10 log files. Unify on a single source? For now, keep both — the per-schedule history is structured cron data, the bottom list is a cross-cutting "what ran lately" surface useful even with no schedules. Worth re-evaluating once both surfaces are in production for a few weeks.
|
||||
4. **`last_output` field on `CoreCronJob` vs. per-run output on `CoreCronRun`.** Today both exist (`cron.ts:42-43` and `cron.ts:51`). DevWorkflowPanel renders `existingJob.last_output` in the active card AND per-run `run.output` in history rows. After unification, drop the duplicated `last_output` block; the per-run history already shows the most recent run's output. Lightweight change.
|
||||
5. **Deprecation timing for the Settings → Developer Options → Dev Workflow nav entry.** Strip immediately (Phase 3 chunk 4), or leave as a redirect for one release? Leaning toward strip — the user is the maintainer, the panel is dev-only, and `/skills` is more discoverable than a buried Settings sub-page.
|
||||
@@ -1,675 +0,0 @@
|
||||
# Operator MVP - OpenHuman Fork Execution Plan
|
||||
|
||||
**Status:** Draft execution plan
|
||||
**Date:** 2026-05-11
|
||||
**Primary repo:** `openhuman`
|
||||
**Reference repo:** `inbox`
|
||||
**Goal:** Turn the fork into a phone-reachable personal operator that observes work, proposes next actions, asks for approval, acts through trusted connectors, verifies outcomes, and records evidence.
|
||||
|
||||
> **For agentic workers:** This is a planning lane artifact. Do not start broad implementation from chat context. Turn selected slices into Linear issues with acceptance criteria, branch names, owned files, validation commands, and PR handoff requirements.
|
||||
|
||||
---
|
||||
|
||||
## Product Thesis
|
||||
|
||||
The product is not a generic desktop assistant.
|
||||
|
||||
The product is an operator loop:
|
||||
|
||||
```text
|
||||
Observe -> Propose -> Approve -> Act -> Verify -> Remember
|
||||
```
|
||||
|
||||
Every feature should attach to one of those stages. Anything that does not make that loop faster, safer, or more useful is backlog.
|
||||
|
||||
The user-facing promise:
|
||||
|
||||
```text
|
||||
Connect your real work once. The operator finds what needs attention, drafts useful next actions, asks before doing anything risky, executes through the right account/tool, and shows evidence that the work is done.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why This Can Beat The Existing Shape
|
||||
|
||||
OpenHuman already has a strong desktop shell, Rust core, controller registry, Composio integration, local memory, skills, screen/voice surfaces, and a growing capability catalog. The current weakness is product focus: many capabilities exist, but the user still has to decide what to ask, where to ask it, and how to know whether the agent actually finished.
|
||||
|
||||
The inbox repo already solved the more important product primitive: normalize noisy sources into work to do. It has concrete patterns for:
|
||||
|
||||
- local-first source adapters
|
||||
- normalized thread/item records
|
||||
- sync state
|
||||
- source-of-truth routing
|
||||
- confirmation-gated tools
|
||||
- narrow MCP tools
|
||||
- "Now", "Actionable", "Waiting On", and calendar-attached work views
|
||||
|
||||
The fork should combine them by making OpenHuman the operator runtime and inbox logic the work-discovery/control plane.
|
||||
|
||||
---
|
||||
|
||||
## Existing Assets To Reuse
|
||||
|
||||
### From OpenHuman
|
||||
|
||||
- Tauri desktop app and Rust sidecar runtime.
|
||||
- Controller registry in `src/core/all.rs`.
|
||||
- Domain layout under `src/openhuman/<domain>/`.
|
||||
- Composio controllers in `src/openhuman/composio/`.
|
||||
- Frontend Composio API wrappers in `app/src/lib/composio/`.
|
||||
- Skills/runtime surfaces for app actions.
|
||||
- Memory store and memory tree ingestion/retrieval.
|
||||
- Capability catalog in `src/openhuman/about_app/`.
|
||||
- Channels/controllers surface for external channels.
|
||||
- Voice, screen intelligence, provider surfaces, notifications, and subconscious/background work scaffolding.
|
||||
|
||||
### From Inbox
|
||||
|
||||
- `message_index_store.py`: operational index with items, threads, sync state, actionability, urgency, open loops, summaries, and sender stats.
|
||||
- `tools_registry.py`: one registry driving readonly and write-capable tools, with confirmation-gated writes.
|
||||
- `CONNECTOR_ROADMAP.md`: source adapters -> normalization -> policy -> intent tools.
|
||||
- `PLAN.md`: organize around work to do, not raw providers.
|
||||
- `MCP_V1_PLAN.md`: public MCP/chat surface -> private local server, with token gates and confirmation gates.
|
||||
- Google account/source-of-truth routing rules.
|
||||
- Gmail, Calendar, Drive, Sheets, Notes, Reminders, GitHub, and iMessage connector experience.
|
||||
|
||||
---
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### New Rust Domain
|
||||
|
||||
Create a dedicated domain:
|
||||
|
||||
```text
|
||||
src/openhuman/operator/
|
||||
|-- mod.rs
|
||||
|-- types.rs
|
||||
|-- store.rs
|
||||
|-- policy.rs
|
||||
|-- preflight.rs
|
||||
|-- evidence.rs
|
||||
|-- goal_packs.rs
|
||||
|-- inbox_adapter.rs
|
||||
|-- schemas.rs
|
||||
|-- ops.rs
|
||||
`-- ops_tests.rs
|
||||
```
|
||||
|
||||
The domain owns operator state and business logic. React should render operator state and call controllers; it should not decide routing, safety, approval, or source-of-truth behavior.
|
||||
|
||||
### Controller Surface
|
||||
|
||||
Expose controllers through the existing registry:
|
||||
|
||||
```text
|
||||
openhuman.operator_list_goal_packs
|
||||
openhuman.operator_create_goal_pack
|
||||
openhuman.operator_update_goal_pack
|
||||
openhuman.operator_list_work_items
|
||||
openhuman.operator_propose_next_actions
|
||||
openhuman.operator_preview_action
|
||||
openhuman.operator_create_approval_request
|
||||
openhuman.operator_approve_request
|
||||
openhuman.operator_reject_request
|
||||
openhuman.operator_execute_approved_action
|
||||
openhuman.operator_record_evidence
|
||||
openhuman.operator_list_evidence
|
||||
openhuman.operator_explain_routing_decision
|
||||
```
|
||||
|
||||
Registry additions belong in `src/core/all.rs` and the operator domain schema tests should prove schema/handler parity.
|
||||
|
||||
### Data Model
|
||||
|
||||
Minimum local tables:
|
||||
|
||||
```text
|
||||
operator_goal_packs
|
||||
operator_work_items
|
||||
operator_action_proposals
|
||||
operator_approval_requests
|
||||
operator_execution_runs
|
||||
operator_evidence_events
|
||||
operator_connector_accounts
|
||||
operator_policy_rules
|
||||
operator_sync_state
|
||||
```
|
||||
|
||||
Minimum concepts:
|
||||
|
||||
- **GoalPack:** a reusable outcome, trigger, context, permissions, and done criteria.
|
||||
- **WorkItem:** normalized thing that may need attention.
|
||||
- **ActionProposal:** draft action the operator wants to take.
|
||||
- **ApprovalRequest:** explicit user decision required before risk-bearing actions.
|
||||
- **ExecutionRun:** actual attempt to do approved work.
|
||||
- **EvidenceEvent:** proof, receipt, diff, message id, PR URL, calendar id, or error trace.
|
||||
- **PolicyRule:** source-of-truth and permission rule resolved in code, not prompts.
|
||||
|
||||
### Goal Pack Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "daily-brief",
|
||||
"name": "Daily Brief",
|
||||
"objective": "Tell me what changed, what needs a reply, and what should be done today.",
|
||||
"triggers": [
|
||||
{ "kind": "schedule", "cron": "0 7 * * MON-FRI" },
|
||||
{ "kind": "manual", "surface": "phone" }
|
||||
],
|
||||
"sources": ["gmail", "calendar", "github", "linear", "imessage"],
|
||||
"allowed_actions": ["read", "summarize", "draft", "classify", "propose"],
|
||||
"approval_required": [
|
||||
"send",
|
||||
"create_external_record",
|
||||
"update_external_record"
|
||||
],
|
||||
"done_criteria": [
|
||||
"brief contains top changes",
|
||||
"items needing reply are separated from FYI",
|
||||
"each proposed action has source links"
|
||||
],
|
||||
"evidence_required": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inbox Logic Integration
|
||||
|
||||
Do not port the whole inbox repo first. Use an adapter boundary.
|
||||
|
||||
### Phase 1 Adapter
|
||||
|
||||
OpenHuman calls a local inbox server when present:
|
||||
|
||||
```text
|
||||
OpenHuman operator domain -> inbox_adapter.rs -> http://127.0.0.1:9849
|
||||
```
|
||||
|
||||
Adapter functions:
|
||||
|
||||
- `list_actionable_threads`
|
||||
- `list_waiting_on_me`
|
||||
- `list_waiting_on_others`
|
||||
- `list_calendar_context`
|
||||
- `draft_reply_for_thread`
|
||||
- `preflight_write`
|
||||
- `explain_routing_decision`
|
||||
|
||||
This gives immediate reuse while keeping OpenHuman stable.
|
||||
|
||||
### Phase 2 Native Port
|
||||
|
||||
Port only stable primitives into Rust after the product loop is proven:
|
||||
|
||||
- normalized `WorkItem` schema
|
||||
- sync state
|
||||
- source/account ownership
|
||||
- actionability classification
|
||||
- source-of-truth routing
|
||||
- confirmation gates
|
||||
- evidence recording
|
||||
|
||||
Do not port provider-specific code until a native source has clear user value.
|
||||
|
||||
---
|
||||
|
||||
## Connector Strategy
|
||||
|
||||
### Bootstrap
|
||||
|
||||
Use Composio for breadth and speed:
|
||||
|
||||
- OAuth handoff
|
||||
- long-tail connector discovery
|
||||
- initial Gmail/GitHub/Linear/Slack/Notion-style coverage
|
||||
- trigger discovery where useful
|
||||
|
||||
But do not expose generic Composio execution as the product API. Wrap it in operator policy:
|
||||
|
||||
```text
|
||||
GoalPack -> Proposal -> Preflight -> Approval -> Composio execute -> Verify -> Evidence
|
||||
```
|
||||
|
||||
### Core Connectors
|
||||
|
||||
Move high-value connectors to first-class/direct support when they become central to the product:
|
||||
|
||||
- Gmail
|
||||
- Google Calendar
|
||||
- GitHub
|
||||
- Linear
|
||||
- Google Drive/Docs
|
||||
- Slack
|
||||
- iMessage/local Messages bridge for power users
|
||||
|
||||
### Source-Of-Truth Rule
|
||||
|
||||
Routing belongs in code. The model should not guess which account to use.
|
||||
|
||||
Examples:
|
||||
|
||||
- Replies route through the owning account/thread unless explicitly overridden.
|
||||
- New Google writes use the configured default account.
|
||||
- GitHub PR comments route to the repo/owner from the original item.
|
||||
- Linear updates route to the issue/team from the original work item.
|
||||
|
||||
Each write preview must show:
|
||||
|
||||
- connector
|
||||
- account
|
||||
- target object
|
||||
- action type
|
||||
- irreversible risk
|
||||
- evidence expected after execution
|
||||
|
||||
---
|
||||
|
||||
## Phone And Chat Surfaces
|
||||
|
||||
The near-term goal is to meet users where they already are without pretending iMessage automation is simple or App Store-friendly.
|
||||
|
||||
### V0 Surfaces
|
||||
|
||||
- Desktop app approval queue.
|
||||
- Local web approval page.
|
||||
- Email or push-style notifications for approval requests.
|
||||
- Telegram/Slack bot as an early phone chat surface.
|
||||
- iOS Shortcuts + Share Sheet for "send this to operator".
|
||||
- MCP/chat bridge for ChatGPT or other clients when available.
|
||||
|
||||
### Power User Surface
|
||||
|
||||
- Local iMessage bridge from macOS using the inbox patterns.
|
||||
- Treat as a local-only advanced connector, not the primary commercial onboarding path.
|
||||
|
||||
### Later Surface
|
||||
|
||||
- Messages for Business only if the user base and Apple approval process justify it.
|
||||
- It is not a general encrypted personal assistant channel by default, and it should not be the MVP dependency.
|
||||
|
||||
---
|
||||
|
||||
## Data Connect Control Plane
|
||||
|
||||
The control plane should store coordination metadata, not raw private data by default.
|
||||
|
||||
Candidate hosted tables:
|
||||
|
||||
```text
|
||||
users
|
||||
devices
|
||||
connector_accounts
|
||||
goal_packs
|
||||
goal_pack_memberships
|
||||
automation_runs
|
||||
approval_requests
|
||||
evidence_events
|
||||
memory_summaries
|
||||
channel_threads
|
||||
sync_cursors
|
||||
audit_events
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Raw inbox/email/message content stays local unless the user explicitly opts in.
|
||||
- Store derived summaries, hashes, source ids, and evidence metadata when enough.
|
||||
- Every externally visible action records an audit event.
|
||||
- Cloud unlocks multi-device continuity, phone approval, and backup, not broad data extraction.
|
||||
|
||||
TEE/private hosted execution is a later trust upgrade, not a blocker for v0. V0 should be local-first, boring, and auditable.
|
||||
|
||||
---
|
||||
|
||||
## MVP Goal Packs
|
||||
|
||||
### 1. Daily Brief
|
||||
|
||||
Outcome:
|
||||
|
||||
- show what changed
|
||||
- show what needs a reply
|
||||
- show today's meetings and prep
|
||||
- show blocked/waiting items
|
||||
|
||||
Sources:
|
||||
|
||||
- Gmail
|
||||
- Calendar
|
||||
- GitHub
|
||||
- Linear
|
||||
- iMessage if available
|
||||
|
||||
Allowed without approval:
|
||||
|
||||
- read
|
||||
- classify
|
||||
- summarize
|
||||
- propose
|
||||
|
||||
Approval required:
|
||||
|
||||
- send
|
||||
- create issue
|
||||
- update issue
|
||||
- archive/delete
|
||||
|
||||
### 2. Reply Drafts
|
||||
|
||||
Outcome:
|
||||
|
||||
- find conversations needing reply
|
||||
- draft replies in the user's voice
|
||||
- let user approve, edit, or reject
|
||||
- send through the owning account only after approval
|
||||
|
||||
Evidence:
|
||||
|
||||
- source thread id
|
||||
- sent message id
|
||||
- account used
|
||||
- final body hash
|
||||
|
||||
### 3. Meeting Prep
|
||||
|
||||
Outcome:
|
||||
|
||||
- for upcoming meetings, gather calendar details, related threads/docs/issues, and suggested agenda/follow-ups
|
||||
|
||||
Evidence:
|
||||
|
||||
- calendar event id
|
||||
- linked source items
|
||||
- generated prep artifact id
|
||||
|
||||
### 4. Engineering Review / Next Goals
|
||||
|
||||
Outcome:
|
||||
|
||||
- inspect GitHub/Linear state
|
||||
- identify stale PRs, failed checks, blocked issues, and next execution slices
|
||||
- propose next Linear issues or PR review tasks
|
||||
|
||||
Approval required:
|
||||
|
||||
- create/update Linear issues
|
||||
- comment on GitHub
|
||||
- push code
|
||||
- open/close PRs
|
||||
|
||||
---
|
||||
|
||||
## Safety Policy
|
||||
|
||||
### Auto-Allowed
|
||||
|
||||
- observe
|
||||
- classify
|
||||
- summarize
|
||||
- draft
|
||||
- rank
|
||||
- prepare local previews
|
||||
- create local evidence records
|
||||
- run read-only checks
|
||||
|
||||
### Approval Required
|
||||
|
||||
- send message or email
|
||||
- create/update/delete external records
|
||||
- schedule/reschedule meetings
|
||||
- spend money
|
||||
- post publicly
|
||||
- push code
|
||||
- merge/close PRs
|
||||
- destructive local filesystem operations
|
||||
- broad data export
|
||||
|
||||
### Always Visible
|
||||
|
||||
Every proposal must show:
|
||||
|
||||
- what will happen
|
||||
- why it is being proposed
|
||||
- what account/source will be used
|
||||
- what data leaves the device
|
||||
- what evidence will be recorded
|
||||
- how to undo or recover when possible
|
||||
|
||||
---
|
||||
|
||||
## Implementation Gates
|
||||
|
||||
### Gate 0 - Fork And Product Boundaries
|
||||
|
||||
- [ ] Decide fork name, package identifiers, and user-facing brand copy.
|
||||
- [ ] Preserve upstream GPL-3.0 obligations for OpenHuman-derived client code.
|
||||
- [ ] Preserve MIT notices for any inbox-derived code.
|
||||
- [ ] Decide whether the hosted control plane is separate proprietary service code or open source.
|
||||
- [ ] Add a `FORK_NOTES.md` or equivalent with upstream sync policy.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
rg -n "GPL|MIT|license|License" README.md LICENSE* app src docs
|
||||
```
|
||||
|
||||
### Gate 1 - Operator Domain Skeleton
|
||||
|
||||
Files:
|
||||
|
||||
- `src/openhuman/operator/types.rs`
|
||||
- `src/openhuman/operator/store.rs`
|
||||
- `src/openhuman/operator/schemas.rs`
|
||||
- `src/openhuman/operator/ops.rs`
|
||||
- `src/openhuman/operator/ops_tests.rs`
|
||||
- `src/openhuman/operator/mod.rs`
|
||||
- `src/core/all.rs`
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Define `GoalPack`, `WorkItem`, `ActionProposal`, `ApprovalRequest`, `ExecutionRun`, and `EvidenceEvent`.
|
||||
- [ ] Add local SQLite-backed store or reuse the existing app storage pattern.
|
||||
- [ ] Register list/create/update/list evidence controllers.
|
||||
- [ ] Add schema parity tests.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
cargo test operator --manifest-path app/src-tauri/Cargo.toml
|
||||
cargo check --manifest-path app/src-tauri/Cargo.toml
|
||||
```
|
||||
|
||||
### Gate 2 - Inbox Adapter
|
||||
|
||||
Files:
|
||||
|
||||
- `src/openhuman/operator/inbox_adapter.rs`
|
||||
- `src/openhuman/operator/preflight.rs`
|
||||
- `src/openhuman/operator/policy.rs`
|
||||
- `src/openhuman/operator/ops_tests.rs`
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Add adapter trait for actionable work.
|
||||
- [ ] Implement local HTTP adapter for inbox server.
|
||||
- [ ] Add inert/fake adapter for tests.
|
||||
- [ ] Normalize inbox rows into operator `WorkItem`.
|
||||
- [ ] Expose `operator_list_work_items` and `operator_explain_routing_decision`.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
cargo test operator --manifest-path app/src-tauri/Cargo.toml
|
||||
```
|
||||
|
||||
### Gate 3 - Approval And Evidence Loop
|
||||
|
||||
Files:
|
||||
|
||||
- `src/openhuman/operator/preflight.rs`
|
||||
- `src/openhuman/operator/evidence.rs`
|
||||
- `src/openhuman/operator/ops.rs`
|
||||
- `src/openhuman/operator/ops_tests.rs`
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] `preview_action` returns account, target, risk, and expected evidence.
|
||||
- [ ] `create_approval_request` stores a pending approval.
|
||||
- [ ] `approve_request` transitions pending -> approved.
|
||||
- [ ] `execute_approved_action` refuses unapproved writes.
|
||||
- [ ] `record_evidence` links evidence to run/proposal/work item.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
cargo test operator:: --manifest-path app/src-tauri/Cargo.toml
|
||||
```
|
||||
|
||||
### Gate 4 - First UI
|
||||
|
||||
Files:
|
||||
|
||||
- `app/src/lib/operator/`
|
||||
- `app/src/components/operator/`
|
||||
- `app/src/pages/Operator.tsx` or the existing app route pattern
|
||||
- relevant route/nav files
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Add "Today" view with goal packs and work items.
|
||||
- [ ] Add approval queue.
|
||||
- [ ] Add evidence timeline.
|
||||
- [ ] Add preview drawer for a proposed action.
|
||||
- [ ] Keep all business logic in Rust controllers.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
pnpm --cwd app compile
|
||||
pnpm --cwd app test -- --run operator
|
||||
```
|
||||
|
||||
### Gate 5 - Composio Execution Wrapper
|
||||
|
||||
Files:
|
||||
|
||||
- `src/openhuman/operator/connectors.rs`
|
||||
- `src/openhuman/operator/preflight.rs`
|
||||
- `src/openhuman/operator/evidence.rs`
|
||||
- `src/openhuman/composio/` only if narrow changes are required
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Execute only approved proposals.
|
||||
- [ ] Map proposal action types to Composio action calls.
|
||||
- [ ] Record connector result ids as evidence.
|
||||
- [ ] Refuse generic tool calls from goal packs that lack explicit permission.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
cargo test operator composio --manifest-path app/src-tauri/Cargo.toml
|
||||
```
|
||||
|
||||
### Gate 6 - Phone Reachability
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Add local web approval page or channel controller for approval requests.
|
||||
- [ ] Add Telegram/Slack bot or MCP/chat bridge as first phone surface.
|
||||
- [ ] Add iOS Shortcut/share-sheet handoff docs.
|
||||
- [ ] Keep local iMessage bridge as advanced local connector, not onboarding dependency.
|
||||
|
||||
Validation:
|
||||
|
||||
```bash
|
||||
cargo test channels operator --manifest-path app/src-tauri/Cargo.toml
|
||||
pnpm --cwd app compile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## First Linear Slices
|
||||
|
||||
Each issue should be independently reviewable and should create a real PR.
|
||||
|
||||
1. **Operator domain skeleton**
|
||||
- Acceptance: schemas compile, registry sees operator controllers, tests prove schema/handler parity.
|
||||
- Validation: `cargo test operator --manifest-path app/src-tauri/Cargo.toml`.
|
||||
|
||||
2. **Goal pack store**
|
||||
- Acceptance: create/list/update goal packs round trip through local store.
|
||||
- Validation: `cargo test operator::goal_packs --manifest-path app/src-tauri/Cargo.toml`.
|
||||
|
||||
3. **Approval request state machine**
|
||||
- Acceptance: pending/approved/rejected/executed transitions are enforced.
|
||||
- Validation: `cargo test operator::approval --manifest-path app/src-tauri/Cargo.toml`.
|
||||
|
||||
4. **Evidence event store**
|
||||
- Acceptance: evidence links to work item/proposal/run and is queryable.
|
||||
- Validation: `cargo test operator::evidence --manifest-path app/src-tauri/Cargo.toml`.
|
||||
|
||||
5. **Inbox adapter trait plus fake adapter**
|
||||
- Acceptance: operator can list normalized work items without real inbox server.
|
||||
- Validation: `cargo test operator::inbox_adapter --manifest-path app/src-tauri/Cargo.toml`.
|
||||
|
||||
6. **Inbox HTTP adapter**
|
||||
- Acceptance: when inbox server is reachable, actionable threads map to `WorkItem`; when unreachable, UI/API returns a clear degraded state.
|
||||
- Validation: adapter unit tests plus one manual local smoke.
|
||||
|
||||
7. **Preflight/routing explanation**
|
||||
- Acceptance: previews show source, account, target, risk, and expected evidence.
|
||||
- Validation: `cargo test operator::preflight --manifest-path app/src-tauri/Cargo.toml`.
|
||||
|
||||
8. **Operator Today UI**
|
||||
- Acceptance: user can see goal packs, actionable items, approvals, and evidence.
|
||||
- Validation: `pnpm --cwd app compile` and operator component tests.
|
||||
|
||||
9. **Composio approved execution wrapper**
|
||||
- Acceptance: unapproved writes fail; approved writes call the connector wrapper and record evidence.
|
||||
- Validation: Rust tests with fake connector.
|
||||
|
||||
10. **Daily Brief goal pack**
|
||||
- Acceptance: daily brief can run in preview mode and produce evidence-backed proposed actions.
|
||||
- Validation: unit tests plus a local seeded-data smoke.
|
||||
|
||||
---
|
||||
|
||||
## Proof Metrics
|
||||
|
||||
MVP is useful only if these are true:
|
||||
|
||||
- Time from install to first useful proposed action is under 5 minutes.
|
||||
- A non-technical user can connect at least one account and understand what the operator wants to do.
|
||||
- Every write action has an approval record.
|
||||
- Every completed action has evidence.
|
||||
- The operator can explain why it chose an account/source.
|
||||
- The user can reject a proposal without breaking the workflow.
|
||||
- The product is useful with one connector and gets better with more connectors.
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals For V0
|
||||
|
||||
- Full mobile app.
|
||||
- Messages for Business as primary onboarding.
|
||||
- Hosted TEE execution.
|
||||
- Open-source model hosting as a product dependency.
|
||||
- Marketplace for goal packs.
|
||||
- Generic all-connector automation.
|
||||
- Porting the whole inbox repo into Rust.
|
||||
- Autonomous writes without approval.
|
||||
|
||||
---
|
||||
|
||||
## Decision Rules
|
||||
|
||||
- If a feature does not produce a work item, proposal, approval, action, or evidence event, it is probably not MVP.
|
||||
- If a model is deciding account routing from prose, move the rule into code.
|
||||
- If a connector action cannot be previewed, it cannot be executed.
|
||||
- If an action cannot produce evidence, it should not claim completion.
|
||||
- If a phone surface cannot carry approvals clearly, it is only a notification surface.
|
||||
- If a goal pack cannot define done criteria, it is too vague.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,980 +0,0 @@
|
||||
# Memory Tree per-integration health strip — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a compact per-integration health strip inside `MemoryTreeStatusPanel`, between the four tiles and the auto-sync toggle, so operators can see at a glance which integrations are contributing chunks vs. idle. Issue #2763.
|
||||
|
||||
**Architecture:** Frontend-only. Reuses the existing `openhuman.memory_sync_status_list` RPC (no Rust changes). Extends `useMemoryTreeStatus` to fetch both endpoints in parallel on one shared 1.5s / 4s adaptive timer. Status mapping is pure TS: `Active` (freshness=active) vs `Stale` (freshness=recent|idle). Error state intentionally deferred — needs proper per-provider job→source linkage in core.
|
||||
|
||||
**Tech Stack:** React 18, TypeScript, Vitest + Testing Library, Tailwind. RPC bridge via `callCoreRpc` in `app/src/services/coreRpcClient`. i18n via `useT()` from `app/src/lib/i18n/I18nContext`.
|
||||
|
||||
**Branch:** `feat/memory-tree-integration-health` (already created from `origin/main`).
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-memory-tree-integration-health-design.md`.
|
||||
|
||||
## File map
|
||||
|
||||
| File | Action | Responsibility |
|
||||
| --- | --- | --- |
|
||||
| `app/src/utils/tauriCommands/memoryTree.ts` | Modify | Add `MemorySyncFreshness`, `MemorySyncStatusRow`, `memorySyncStatusList()` wrapper for `openhuman.memory_sync_status_list`. |
|
||||
| `app/src/components/intelligence/MemoryTreeStatusPanel.tsx` | Modify | Extend `useMemoryTreeStatus` hook to fetch both endpoints in parallel; add `IntegrationHealthStrip` sub-component; mount it between the 4-tile grid and the toggle row. |
|
||||
| `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx` | Modify | New cases: renders rows, status mapping, empty state, icon fallback, shared polling assertion. |
|
||||
| `app/src/lib/i18n/en.ts` | Modify | 5 new keys under `memoryTree.status.integration*`. |
|
||||
| `app/src/lib/i18n/{ar,bn,de,es,fr,hi,id,it,ko,pl,pt,ru,zh-CN}.ts` | Modify | Same 5 keys with real translations (CLAUDE.md i18n rule — no English fallback). |
|
||||
|
||||
No new files created. Strip lives as a sub-component inside `MemoryTreeStatusPanel.tsx` because the two concerns share state (hook output) and polling cadence.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `memorySyncStatusList()` wrapper
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/utils/tauriCommands/memoryTree.ts` (append at end of file, before any closing exports)
|
||||
|
||||
**Context:** The Rust RPC `openhuman.memory_sync_status_list` returns `{ statuses: MemorySyncStatus[] }`. Wire shape from `src/openhuman/memory_sync/sync_status/types.rs`:
|
||||
|
||||
```rust
|
||||
pub struct MemorySyncStatus {
|
||||
pub provider: String,
|
||||
pub chunks_synced: u64,
|
||||
pub chunks_pending: u64,
|
||||
pub batch_total: u64,
|
||||
pub batch_processed: u64,
|
||||
pub last_chunk_at_ms: Option<i64>,
|
||||
pub freshness: FreshnessLabel, // snake_case: "active" | "recent" | "idle"
|
||||
}
|
||||
```
|
||||
|
||||
We only consume `provider`, `chunks_synced`, `last_chunk_at_ms`, `freshness` in v1. Keep the full type so the wrapper is reusable.
|
||||
|
||||
- [ ] **Step 1: Append the wrapper to memoryTree.ts**
|
||||
|
||||
Append at the bottom of `app/src/utils/tauriCommands/memoryTree.ts` (after the existing `memoryTreeSetEnabled` block):
|
||||
|
||||
```ts
|
||||
// ── memory_sync_status_list (#2763 — per-integration health strip) ───────
|
||||
|
||||
/**
|
||||
* Freshness label emitted by `openhuman.memory_sync_status_list`. Snake-case
|
||||
* mirrors the Rust `FreshnessLabel` serde rename. Derived from
|
||||
* `now - last_chunk_at_ms` at RPC time, not stored.
|
||||
*/
|
||||
export type MemorySyncFreshness = 'active' | 'recent' | 'idle';
|
||||
|
||||
/**
|
||||
* One row per provider that has produced chunks. Mirrors the Rust
|
||||
* `MemorySyncStatus` struct exactly — snake_case carried through so the
|
||||
* wire payload deserialises without a remap layer.
|
||||
*/
|
||||
export interface MemorySyncStatusRow {
|
||||
/** Provider key — `slack`, `gmail`, `notion`, `discord`, `telegram`, etc. */
|
||||
provider: string;
|
||||
/** Total chunks in `mem_tree_chunks` for this provider. */
|
||||
chunks_synced: number;
|
||||
/** Chunks fetched but not yet extracted/embedded. Lifetime metric. */
|
||||
chunks_pending: number;
|
||||
/** Total chunks in the current sync wave. Zero when no wave is active. */
|
||||
batch_total: number;
|
||||
/** Of `batch_total`, how many have been processed. */
|
||||
batch_processed: number;
|
||||
/** Epoch ms of the most-recent chunk for this provider; null if none yet. */
|
||||
last_chunk_at_ms: number | null;
|
||||
/** Coarse activity label — derived at RPC time. */
|
||||
freshness: MemorySyncFreshness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the per-provider sync-status list. Single SQL query against
|
||||
* `mem_tree_chunks` (GROUP BY source_kind); safe to poll alongside
|
||||
* `memoryTreePipelineStatus` on the same 1.5s / 4s adaptive cadence.
|
||||
*
|
||||
* Backed by `openhuman.memory_sync_status_list` (#1136). Surfaced by the
|
||||
* per-integration health strip in `MemoryTreeStatusPanel` (#2763).
|
||||
*/
|
||||
export async function memorySyncStatusList(): Promise<MemorySyncStatusRow[]> {
|
||||
console.debug('[memory-tree-rpc] memorySyncStatusList: entry');
|
||||
const resp = await callCoreRpc<
|
||||
{ statuses: MemorySyncStatusRow[] } | ResultEnvelope<{ statuses: MemorySyncStatusRow[] }>
|
||||
>({ method: 'openhuman.memory_sync_status_list', params: {} });
|
||||
const out = unwrapResult(resp);
|
||||
const rows = out.statuses ?? [];
|
||||
console.debug('[memory-tree-rpc] memorySyncStatusList: exit rows=%d', rows.length);
|
||||
return rows;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file compiles**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: PASS (no new TS errors). If it fails on `callCoreRpc`/`unwrapResult`/`ResultEnvelope` not being in scope, they're already imported in this file — check the top of the file. Re-check the appended block uses the same identifiers verbatim.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/utils/tauriCommands/memoryTree.ts
|
||||
git commit -m "feat(memory-tree): add memorySyncStatusList RPC wrapper (#2763)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add the 5 new i18n keys (English)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/lib/i18n/en.ts:521` (immediately after the existing `memoryTree.status.daysAgo` line)
|
||||
|
||||
- [ ] **Step 1: Insert the new keys**
|
||||
|
||||
Open `app/src/lib/i18n/en.ts`. Find the line `'memoryTree.status.daysAgo': '{count} days ago',` (around line 521). Insert immediately after it:
|
||||
|
||||
```ts
|
||||
// Per-integration health strip (#2763) — rendered between the 4-tile grid
|
||||
// and the auto-sync toggle in MemoryTreeStatusPanel.
|
||||
'memoryTree.status.integrationsTitle': 'Per-integration health',
|
||||
'memoryTree.status.integrationsEmpty': 'No integrations connected',
|
||||
'memoryTree.status.integrationActive': 'Active',
|
||||
'memoryTree.status.integrationStale': 'Stale',
|
||||
'memoryTree.status.integrationChunks': '{count} chunks',
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify typecheck still clean**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run the i18n parity gate to confirm it now expects these keys in every other locale**
|
||||
|
||||
Run: `pnpm i18n:check`
|
||||
Expected: FAIL with messages like `Missing key 'memoryTree.status.integrationsTitle' in locale 'ar'` etc. (13 missing-key errors per new key). This is the desired failure that Task 3 fixes.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/lib/i18n/en.ts
|
||||
git commit -m "feat(i18n): add English keys for integration health strip (#2763)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add real translations for all 13 non-English locales
|
||||
|
||||
**Files:** Modify each of:
|
||||
- `app/src/lib/i18n/ar.ts`
|
||||
- `app/src/lib/i18n/bn.ts`
|
||||
- `app/src/lib/i18n/de.ts`
|
||||
- `app/src/lib/i18n/es.ts`
|
||||
- `app/src/lib/i18n/fr.ts`
|
||||
- `app/src/lib/i18n/hi.ts`
|
||||
- `app/src/lib/i18n/id.ts`
|
||||
- `app/src/lib/i18n/it.ts`
|
||||
- `app/src/lib/i18n/ko.ts`
|
||||
- `app/src/lib/i18n/pl.ts`
|
||||
- `app/src/lib/i18n/pt.ts`
|
||||
- `app/src/lib/i18n/ru.ts`
|
||||
- `app/src/lib/i18n/zh-CN.ts`
|
||||
|
||||
Each file already contains the `memoryTree.status.daysAgo` key (sibling to where we inserted in en.ts). Insert the 5 new keys directly after that line in every file, in the language of that file.
|
||||
|
||||
- [ ] **Step 1: Insert translations into each locale file**
|
||||
|
||||
For each locale file, find `memoryTree.status.daysAgo` and insert the corresponding block below. Use these exact translations:
|
||||
|
||||
**`ar.ts`** (Arabic):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'حالة التكاملات',
|
||||
'memoryTree.status.integrationsEmpty': 'لا توجد تكاملات متصلة',
|
||||
'memoryTree.status.integrationActive': 'نشط',
|
||||
'memoryTree.status.integrationStale': 'قديم',
|
||||
'memoryTree.status.integrationChunks': '{count} قطعة',
|
||||
```
|
||||
|
||||
**`bn.ts`** (Bengali):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'প্রতি-ইন্টিগ্রেশন স্বাস্থ্য',
|
||||
'memoryTree.status.integrationsEmpty': 'কোনো ইন্টিগ্রেশন সংযুক্ত নেই',
|
||||
'memoryTree.status.integrationActive': 'সক্রিয়',
|
||||
'memoryTree.status.integrationStale': 'পুরানো',
|
||||
'memoryTree.status.integrationChunks': '{count} টি অংশ',
|
||||
```
|
||||
|
||||
**`de.ts`** (German):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Integrationsstatus',
|
||||
'memoryTree.status.integrationsEmpty': 'Keine Integrationen verbunden',
|
||||
'memoryTree.status.integrationActive': 'Aktiv',
|
||||
'memoryTree.status.integrationStale': 'Veraltet',
|
||||
'memoryTree.status.integrationChunks': '{count} Chunks',
|
||||
```
|
||||
|
||||
**`es.ts`** (Spanish):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Estado por integración',
|
||||
'memoryTree.status.integrationsEmpty': 'No hay integraciones conectadas',
|
||||
'memoryTree.status.integrationActive': 'Activa',
|
||||
'memoryTree.status.integrationStale': 'Inactiva',
|
||||
'memoryTree.status.integrationChunks': '{count} fragmentos',
|
||||
```
|
||||
|
||||
**`fr.ts`** (French):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Santé par intégration',
|
||||
'memoryTree.status.integrationsEmpty': 'Aucune intégration connectée',
|
||||
'memoryTree.status.integrationActive': 'Active',
|
||||
'memoryTree.status.integrationStale': 'Obsolète',
|
||||
'memoryTree.status.integrationChunks': '{count} fragments',
|
||||
```
|
||||
|
||||
**`hi.ts`** (Hindi):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'प्रति-एकीकरण स्थिति',
|
||||
'memoryTree.status.integrationsEmpty': 'कोई एकीकरण कनेक्ट नहीं है',
|
||||
'memoryTree.status.integrationActive': 'सक्रिय',
|
||||
'memoryTree.status.integrationStale': 'पुराना',
|
||||
'memoryTree.status.integrationChunks': '{count} खंड',
|
||||
```
|
||||
|
||||
**`id.ts`** (Indonesian):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Kesehatan per integrasi',
|
||||
'memoryTree.status.integrationsEmpty': 'Tidak ada integrasi tersambung',
|
||||
'memoryTree.status.integrationActive': 'Aktif',
|
||||
'memoryTree.status.integrationStale': 'Usang',
|
||||
'memoryTree.status.integrationChunks': '{count} potongan',
|
||||
```
|
||||
|
||||
**`it.ts`** (Italian):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Stato per integrazione',
|
||||
'memoryTree.status.integrationsEmpty': 'Nessuna integrazione collegata',
|
||||
'memoryTree.status.integrationActive': 'Attiva',
|
||||
'memoryTree.status.integrationStale': 'Obsoleta',
|
||||
'memoryTree.status.integrationChunks': '{count} frammenti',
|
||||
```
|
||||
|
||||
**`ko.ts`** (Korean):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': '통합별 상태',
|
||||
'memoryTree.status.integrationsEmpty': '연결된 통합이 없습니다',
|
||||
'memoryTree.status.integrationActive': '활성',
|
||||
'memoryTree.status.integrationStale': '오래됨',
|
||||
'memoryTree.status.integrationChunks': '{count}개 청크',
|
||||
```
|
||||
|
||||
**`pl.ts`** (Polish):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Stan poszczególnych integracji',
|
||||
'memoryTree.status.integrationsEmpty': 'Brak podłączonych integracji',
|
||||
'memoryTree.status.integrationActive': 'Aktywna',
|
||||
'memoryTree.status.integrationStale': 'Nieaktualna',
|
||||
'memoryTree.status.integrationChunks': '{count} fragmentów',
|
||||
```
|
||||
|
||||
**`pt.ts`** (Portuguese):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Saúde por integração',
|
||||
'memoryTree.status.integrationsEmpty': 'Nenhuma integração conectada',
|
||||
'memoryTree.status.integrationActive': 'Ativa',
|
||||
'memoryTree.status.integrationStale': 'Obsoleta',
|
||||
'memoryTree.status.integrationChunks': '{count} fragmentos',
|
||||
```
|
||||
|
||||
**`ru.ts`** (Russian):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': 'Состояние интеграций',
|
||||
'memoryTree.status.integrationsEmpty': 'Нет подключённых интеграций',
|
||||
'memoryTree.status.integrationActive': 'Активна',
|
||||
'memoryTree.status.integrationStale': 'Устарела',
|
||||
'memoryTree.status.integrationChunks': '{count} фрагментов',
|
||||
```
|
||||
|
||||
**`zh-CN.ts`** (Simplified Chinese):
|
||||
```ts
|
||||
'memoryTree.status.integrationsTitle': '各集成状态',
|
||||
'memoryTree.status.integrationsEmpty': '未连接任何集成',
|
||||
'memoryTree.status.integrationActive': '活跃',
|
||||
'memoryTree.status.integrationStale': '过期',
|
||||
'memoryTree.status.integrationChunks': '{count} 个块',
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify i18n parity gate now passes**
|
||||
|
||||
Run: `pnpm i18n:check`
|
||||
Expected: PASS (no missing-key errors).
|
||||
|
||||
- [ ] **Step 3: Verify the English-detection gate passes**
|
||||
|
||||
Run: `pnpm i18n:english:check`
|
||||
Expected: PASS — none of the new translations match the English-detection heuristic. If a value is incorrectly flagged (very unlikely for these short strings) the failure prints the offending key + locale; rewrite that translation rather than touching the `INTENTIONAL_ENGLISH` allowlist.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/lib/i18n/ar.ts app/src/lib/i18n/bn.ts app/src/lib/i18n/de.ts \
|
||||
app/src/lib/i18n/es.ts app/src/lib/i18n/fr.ts app/src/lib/i18n/hi.ts \
|
||||
app/src/lib/i18n/id.ts app/src/lib/i18n/it.ts app/src/lib/i18n/ko.ts \
|
||||
app/src/lib/i18n/pl.ts app/src/lib/i18n/pt.ts app/src/lib/i18n/ru.ts \
|
||||
app/src/lib/i18n/zh-CN.ts
|
||||
git commit -m "feat(i18n): translate integration health strip keys (13 locales, #2763)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Extend `useMemoryTreeStatus` to fetch sync-status in parallel (test first)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx` (add a test before extending the hook)
|
||||
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.tsx` (extend hook)
|
||||
|
||||
**Context:** The existing `useMemoryTreeStatus` returns `{ status, loading, error, refresh }`. We add `integrations: MemorySyncStatusRow[]` to the return shape. The fetcher swaps a single `await memoryTreePipelineStatus()` for a `Promise.all` against both endpoints. On per-endpoint failure we degrade gracefully — pipeline-status failure already shows the existing error banner; sync-status failure logs a warn and renders an empty integration list, so the rest of the panel stays functional.
|
||||
|
||||
- [ ] **Step 1: Add the failing test**
|
||||
|
||||
In `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx`, find the existing `vi.mock` block (around line 21). Replace it with:
|
||||
|
||||
```ts
|
||||
const mockPipelineStatus = vi.fn();
|
||||
const mockSetEnabled = vi.fn();
|
||||
const mockSyncStatusList = vi.fn();
|
||||
|
||||
vi.mock('../../utils/tauriCommands', async importOriginal => {
|
||||
const actual = await importOriginal<typeof import('../../utils/tauriCommands')>();
|
||||
return {
|
||||
...actual,
|
||||
memoryTreePipelineStatus: (...args: unknown[]) => mockPipelineStatus(...args),
|
||||
memoryTreeSetEnabled: (...args: unknown[]) => mockSetEnabled(...args),
|
||||
memorySyncStatusList: (...args: unknown[]) => mockSyncStatusList(...args),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
Then in the existing `beforeEach`, add a reset line:
|
||||
```ts
|
||||
mockSyncStatusList.mockReset();
|
||||
mockSyncStatusList.mockResolvedValue([]); // default: empty, harmless to existing tests
|
||||
```
|
||||
|
||||
Add the new test case inside the `describe('<MemoryTreeStatusPanel />', ...)` block (anywhere after the existing `'renders the four tiles ...'` case):
|
||||
|
||||
```ts
|
||||
it('fetches integration list and pipeline status in parallel on the same tick', async () => {
|
||||
mockPipelineStatus.mockResolvedValue(payload());
|
||||
mockSyncStatusList.mockResolvedValue([
|
||||
{
|
||||
provider: 'slack',
|
||||
chunks_synced: 5231,
|
||||
chunks_pending: 0,
|
||||
batch_total: 0,
|
||||
batch_processed: 0,
|
||||
last_chunk_at_ms: FIXED_NOW_MS - 3 * 60 * 1000,
|
||||
freshness: 'active',
|
||||
},
|
||||
]);
|
||||
|
||||
render(<MemoryTreeStatusPanel />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPipelineStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mockSyncStatusList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new test, watch it fail**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "fetches integration list"`
|
||||
Expected: FAIL — `expected mockSyncStatusList to have been called 1 time, but got 0` (the hook isn't yet calling it).
|
||||
|
||||
- [ ] **Step 3: Extend the hook to fetch both endpoints**
|
||||
|
||||
In `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`:
|
||||
|
||||
1. Update the import line (currently at ~line 26):
|
||||
```ts
|
||||
import {
|
||||
memoryTreePipelineStatus,
|
||||
type MemoryTreePipelineStatus,
|
||||
memoryTreeSetEnabled,
|
||||
memorySyncStatusList,
|
||||
type MemorySyncStatusRow,
|
||||
} from '../../utils/tauriCommands';
|
||||
```
|
||||
|
||||
2. Replace the entire `useMemoryTreeStatus` hook (currently lines 46–102) with this version. New state slot, `Promise.all` in the fetcher, return shape gains `integrations`:
|
||||
|
||||
```ts
|
||||
function useMemoryTreeStatus(): {
|
||||
status: MemoryTreePipelineStatus | null;
|
||||
integrations: MemorySyncStatusRow[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
} {
|
||||
const [status, setStatus] = useState<MemoryTreePipelineStatus | null>(null);
|
||||
const [integrations, setIntegrations] = useState<MemorySyncStatusRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const cancelledRef = useRef(false);
|
||||
const statusRef = useRef<MemoryTreePipelineStatus | null>(null);
|
||||
statusRef.current = status;
|
||||
|
||||
const fetchOnce = useCallback(async () => {
|
||||
console.debug('[ui-flow][memory-tree-status] fetchOnce: entry');
|
||||
try {
|
||||
// Fetch pipeline + per-integration health in parallel so the strip
|
||||
// and the tiles share a single 1.5s / 4s adaptive tick (#2763).
|
||||
const [next, rows] = await Promise.all([
|
||||
memoryTreePipelineStatus(),
|
||||
memorySyncStatusList().catch(err => {
|
||||
// Per-integration list is best-effort: surface an empty strip
|
||||
// rather than wiping the panel when only the secondary endpoint
|
||||
// fails. Pipeline failure still flips the panel-wide error.
|
||||
console.warn(
|
||||
'[ui-flow][memory-tree-status] memorySyncStatusList failed: %s',
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
return [] as MemorySyncStatusRow[];
|
||||
}),
|
||||
]);
|
||||
if (cancelledRef.current) return;
|
||||
setStatus(next);
|
||||
setIntegrations(rows);
|
||||
setError(null);
|
||||
console.debug(
|
||||
'[ui-flow][memory-tree-status] fetchOnce: ok status=%s total=%d integrations=%d',
|
||||
next.status,
|
||||
next.total_chunks,
|
||||
rows.length
|
||||
);
|
||||
} catch (err) {
|
||||
if (cancelledRef.current) return;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.warn('[ui-flow][memory-tree-status] fetchOnce: error %s', message);
|
||||
setError(message);
|
||||
} finally {
|
||||
if (!cancelledRef.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cancelledRef.current = false;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const tick = async () => {
|
||||
await fetchOnce();
|
||||
if (cancelledRef.current) return;
|
||||
const live = statusRef.current;
|
||||
const fast = live?.is_syncing || (live?.pipeline_jobs?.running ?? 0) > 0;
|
||||
timer = setTimeout(tick, fast ? FAST_POLL_MS : DEFAULT_POLL_MS);
|
||||
};
|
||||
|
||||
void tick();
|
||||
|
||||
return () => {
|
||||
cancelledRef.current = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, [fetchOnce]);
|
||||
|
||||
return { status, integrations, loading, error, refresh: fetchOnce };
|
||||
}
|
||||
```
|
||||
|
||||
3. Update the `MemoryTreeStatusPanel` body to destructure `integrations`. Replace the existing `const { status, loading, error, refresh } = useMemoryTreeStatus();` (around line 184) with:
|
||||
```ts
|
||||
const { status, integrations, loading, error, refresh } = useMemoryTreeStatus();
|
||||
```
|
||||
|
||||
(The strip itself is wired in Task 6; for now `integrations` is unused — TypeScript will allow this because it's a destructured property, not a declared local.)
|
||||
|
||||
- [ ] **Step 4: Run the new test, watch it pass**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "fetches integration list"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Run the full file's existing tests to confirm no regression**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
|
||||
Expected: All existing tests still PASS (the default empty `mockSyncStatusList` resolution preserves behaviour).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/components/intelligence/MemoryTreeStatusPanel.tsx \
|
||||
app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx
|
||||
git commit -m "feat(memory-tree): share pipeline + sync-status poll in useMemoryTreeStatus (#2763)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add status-classification helper + provider icon map (test first)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
|
||||
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`
|
||||
|
||||
**Context:** Two pure helpers in the panel file (not exported). `classifyIntegration(freshness)` returns `'active' | 'stale'`. `providerIconChar(provider)` returns a single emoji glyph from a small built-in map, falling back to `'🔌'` for unknown providers. These are tested independently of the React render.
|
||||
|
||||
- [ ] **Step 1: Add tests for the helpers (failing)**
|
||||
|
||||
At the top of `MemoryTreeStatusPanel.test.tsx`, change the `import { MemoryTreeStatusPanel } from './MemoryTreeStatusPanel';` line to also pull the new helpers:
|
||||
|
||||
```ts
|
||||
import {
|
||||
MemoryTreeStatusPanel,
|
||||
classifyIntegration,
|
||||
providerIconChar,
|
||||
} from './MemoryTreeStatusPanel';
|
||||
```
|
||||
|
||||
Below the existing `describe('<MemoryTreeStatusPanel />', ...)` block, add a sibling describe:
|
||||
|
||||
```ts
|
||||
describe('integration health helpers', () => {
|
||||
describe('classifyIntegration', () => {
|
||||
it('maps active freshness to active', () => {
|
||||
expect(classifyIntegration('active')).toBe('active');
|
||||
});
|
||||
it('maps recent freshness to stale', () => {
|
||||
expect(classifyIntegration('recent')).toBe('stale');
|
||||
});
|
||||
it('maps idle freshness to stale', () => {
|
||||
expect(classifyIntegration('idle')).toBe('stale');
|
||||
});
|
||||
});
|
||||
|
||||
describe('providerIconChar', () => {
|
||||
it('returns a known glyph for slack', () => {
|
||||
expect(providerIconChar('slack')).toBe('💬');
|
||||
});
|
||||
it('returns a known glyph for gmail', () => {
|
||||
expect(providerIconChar('gmail')).toBe('📧');
|
||||
});
|
||||
it('falls back to the plug glyph for unknown providers', () => {
|
||||
expect(providerIconChar('definitely-not-a-real-provider')).toBe('🔌');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new tests, watch them fail**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "integration health helpers"`
|
||||
Expected: FAIL — `classifyIntegration` and `providerIconChar` aren't exported yet.
|
||||
|
||||
- [ ] **Step 3: Add the helpers and export them**
|
||||
|
||||
In `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`, add the following block just after `function statusDotClass(...)` (around line 174, before the `MemoryTreeStatusPanelProps` interface):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* UI health classification for a single provider row in the integration
|
||||
* health strip (#2763). The wire shape's three-state `freshness` collapses
|
||||
* to two states here — `Active` (currently producing chunks) vs `Stale`
|
||||
* (anything older). An `Error` state is intentionally NOT derived from the
|
||||
* current data; per-provider failure attribution needs new core work and
|
||||
* is filed as a follow-up to issue #2763.
|
||||
*/
|
||||
export type IntegrationHealth = 'active' | 'stale';
|
||||
|
||||
/** Map the wire `freshness` enum to the two-state UI classification. */
|
||||
export function classifyIntegration(freshness: MemorySyncStatusRow['freshness']): IntegrationHealth {
|
||||
return freshness === 'active' ? 'active' : 'stale';
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in glyph for each known provider key from `memory_sync_status_list`.
|
||||
* Source: `MemorySyncStatus.provider` in `src/openhuman/memory_sync/sync_status/types.rs`
|
||||
* — that file's doc comment enumerates the providers ("slack", "gmail",
|
||||
* "discord", "telegram", "whatsapp", "notion", "meeting_notes",
|
||||
* "drive_docs", etc.). Anything not in this map falls back to a generic
|
||||
* plug glyph so unknown providers still render cleanly.
|
||||
*
|
||||
* Kept inline (rather than re-using `SOURCE_KIND_ICONS` from
|
||||
* `memorySourcesService`) because that map is keyed by `SourceKind`
|
||||
* (`composio` / `folder` / `github_repo` / …) — a different taxonomy.
|
||||
*/
|
||||
const PROVIDER_ICONS: Record<string, string> = {
|
||||
slack: '💬',
|
||||
gmail: '📧',
|
||||
discord: '🎮',
|
||||
telegram: '✈️',
|
||||
whatsapp: '🟢',
|
||||
notion: '📝',
|
||||
meeting_notes: '🎙️',
|
||||
drive_docs: '📄',
|
||||
github: '🐙',
|
||||
};
|
||||
|
||||
/** Look up a provider glyph; fall back to a generic plug for unknowns. */
|
||||
export function providerIconChar(provider: string): string {
|
||||
return PROVIDER_ICONS[provider] ?? '🔌';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the helper tests, watch them pass**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "integration health helpers"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/components/intelligence/MemoryTreeStatusPanel.tsx \
|
||||
app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx
|
||||
git commit -m "feat(memory-tree): add integration-health classifier + provider icons (#2763)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Render the `IntegrationHealthStrip` sub-component (test first)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
|
||||
- Modify: `app/src/components/intelligence/MemoryTreeStatusPanel.tsx`
|
||||
|
||||
**Context:** Internal sub-component (not exported). Takes `integrations: MemorySyncStatusRow[]` and a translator. Renders header + scrollable list of rows, or empty-state copy when the array is empty. Mounted inside `MemoryTreeStatusPanel` between the tile grid (currently the `<div className="grid grid-cols-2 ..." data-testid="memory-tree-status-tiles">` block) and the auto-sync toggle row.
|
||||
|
||||
- [ ] **Step 1: Add the failing UI tests**
|
||||
|
||||
In `MemoryTreeStatusPanel.test.tsx`, inside the `describe('<MemoryTreeStatusPanel />', ...)` block, add three more cases:
|
||||
|
||||
```ts
|
||||
it('renders a row per integration with provider name, chunk count, freshness pill', async () => {
|
||||
mockPipelineStatus.mockResolvedValue(payload());
|
||||
mockSyncStatusList.mockResolvedValue([
|
||||
{
|
||||
provider: 'slack',
|
||||
chunks_synced: 5231,
|
||||
chunks_pending: 0,
|
||||
batch_total: 0,
|
||||
batch_processed: 0,
|
||||
last_chunk_at_ms: FIXED_NOW_MS - 3 * 60 * 1000,
|
||||
freshness: 'active',
|
||||
},
|
||||
{
|
||||
provider: 'gmail',
|
||||
chunks_synced: 842,
|
||||
chunks_pending: 0,
|
||||
batch_total: 0,
|
||||
batch_processed: 0,
|
||||
last_chunk_at_ms: FIXED_NOW_MS - 2 * 60 * 60 * 1000,
|
||||
freshness: 'idle',
|
||||
},
|
||||
]);
|
||||
|
||||
render(<MemoryTreeStatusPanel />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('memory-tree-integrations')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const rows = screen.getAllByTestId(/^memory-tree-integration-row-/);
|
||||
expect(rows).toHaveLength(2);
|
||||
|
||||
// Slack row: active dot, "Active" label, chunk count rendered
|
||||
const slackRow = screen.getByTestId('memory-tree-integration-row-slack');
|
||||
expect(slackRow).toHaveTextContent(/slack/i);
|
||||
expect(slackRow).toHaveTextContent(/5,231 chunks/);
|
||||
expect(slackRow).toHaveTextContent(/Active/);
|
||||
|
||||
// Gmail row: stale label
|
||||
const gmailRow = screen.getByTestId('memory-tree-integration-row-gmail');
|
||||
expect(gmailRow).toHaveTextContent(/gmail/i);
|
||||
expect(gmailRow).toHaveTextContent(/Stale/);
|
||||
});
|
||||
|
||||
it('shows the empty state when there are no integrations', async () => {
|
||||
mockPipelineStatus.mockResolvedValue(payload());
|
||||
mockSyncStatusList.mockResolvedValue([]);
|
||||
|
||||
render(<MemoryTreeStatusPanel />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('memory-tree-integrations-empty')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('memory-tree-integrations-empty')).toHaveTextContent(
|
||||
/no integrations connected/i
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the integration strip between the tile grid and the toggle row', async () => {
|
||||
mockPipelineStatus.mockResolvedValue(payload());
|
||||
mockSyncStatusList.mockResolvedValue([
|
||||
{
|
||||
provider: 'slack',
|
||||
chunks_synced: 1,
|
||||
chunks_pending: 0,
|
||||
batch_total: 0,
|
||||
batch_processed: 0,
|
||||
last_chunk_at_ms: FIXED_NOW_MS - 1000,
|
||||
freshness: 'active',
|
||||
},
|
||||
]);
|
||||
|
||||
render(<MemoryTreeStatusPanel />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('memory-tree-integrations')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// DOM order: tiles → integrations → toggle row.
|
||||
const panel = screen.getByTestId('memory-tree-status-panel');
|
||||
const tiles = screen.getByTestId('memory-tree-status-tiles');
|
||||
const strip = screen.getByTestId('memory-tree-integrations');
|
||||
const toggle = screen.getByTestId('memory-tree-status-toggle-row');
|
||||
|
||||
const order = Array.from(panel.querySelectorAll('[data-testid]'))
|
||||
.map(el => el.getAttribute('data-testid'))
|
||||
.filter(id =>
|
||||
['memory-tree-status-tiles', 'memory-tree-integrations', 'memory-tree-status-toggle-row'].includes(
|
||||
id ?? ''
|
||||
)
|
||||
);
|
||||
|
||||
expect(order).toEqual([
|
||||
'memory-tree-status-tiles',
|
||||
'memory-tree-integrations',
|
||||
'memory-tree-status-toggle-row',
|
||||
]);
|
||||
// Sanity references so unused-var lint doesn't flag the locals above.
|
||||
expect(tiles).toBeInTheDocument();
|
||||
expect(strip).toBeInTheDocument();
|
||||
expect(toggle).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new tests, watch them fail**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "renders a row per integration|empty state when there are no integrations|integration strip between"`
|
||||
Expected: FAIL — `memory-tree-integrations` and `memory-tree-integrations-empty` test-ids do not exist yet.
|
||||
|
||||
- [ ] **Step 3: Add the sub-component**
|
||||
|
||||
In `MemoryTreeStatusPanel.tsx`, just before the existing `export function MemoryTreeStatusPanel(...)` declaration (around line 182), insert:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Per-integration health strip (#2763). Rendered between the four pipeline
|
||||
* tiles and the auto-sync toggle inside `MemoryTreeStatusPanel`. Consumes
|
||||
* the `integrations` slice returned by `useMemoryTreeStatus` — no
|
||||
* additional fetch, no second timer.
|
||||
*/
|
||||
function IntegrationHealthStrip({
|
||||
integrations,
|
||||
t,
|
||||
}: {
|
||||
integrations: MemorySyncStatusRow[];
|
||||
t: TFn;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2" data-testid="memory-tree-integrations">
|
||||
<div className="text-[11px] uppercase tracking-wide text-stone-500 dark:text-neutral-400">
|
||||
{t('memoryTree.status.integrationsTitle')}
|
||||
</div>
|
||||
{integrations.length === 0 ? (
|
||||
<div
|
||||
data-testid="memory-tree-integrations-empty"
|
||||
className="rounded-lg border border-dashed border-stone-200 dark:border-neutral-800 px-3 py-2 text-xs text-stone-500 dark:text-neutral-400">
|
||||
{t('memoryTree.status.integrationsEmpty')}
|
||||
</div>
|
||||
) : (
|
||||
<ul
|
||||
className="max-h-48 space-y-1 overflow-y-auto rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50/40 dark:bg-neutral-800/30 p-2"
|
||||
aria-label={t('memoryTree.status.integrationsTitle')}>
|
||||
{integrations.map(row => {
|
||||
const health = classifyIntegration(row.freshness);
|
||||
const healthLabel =
|
||||
health === 'active'
|
||||
? t('memoryTree.status.integrationActive')
|
||||
: t('memoryTree.status.integrationStale');
|
||||
const dot = health === 'active' ? 'bg-sage-400' : 'bg-stone-400 dark:bg-neutral-500';
|
||||
return (
|
||||
<li
|
||||
key={row.provider}
|
||||
data-testid={`memory-tree-integration-row-${row.provider}`}
|
||||
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-stone-100/60 dark:hover:bg-neutral-800/60">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span aria-hidden className="text-base leading-none">
|
||||
{providerIconChar(row.provider)}
|
||||
</span>
|
||||
<span className="truncate text-sm font-medium text-stone-800 dark:text-neutral-200">
|
||||
{row.provider}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3 text-xs text-stone-500 dark:text-neutral-400">
|
||||
<span>
|
||||
{t('memoryTree.status.integrationChunks').replace(
|
||||
'{count}',
|
||||
new Intl.NumberFormat().format(row.chunks_synced)
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{formatRelativeMs(row.last_chunk_at_ms ?? 0, t, t('memoryTree.status.never'))}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-white dark:bg-neutral-900 px-2 py-0.5 text-[11px] font-medium text-stone-700 dark:text-neutral-200 ring-1 ring-stone-200 dark:ring-neutral-700">
|
||||
<span aria-hidden className={`inline-block h-1.5 w-1.5 rounded-full ${dot}`} />
|
||||
{healthLabel}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Mount the strip between the tiles and the toggle**
|
||||
|
||||
Still in `MemoryTreeStatusPanel.tsx`, find the closing `</div>` of the `data-testid="memory-tree-status-tiles"` block (the grid div that holds the 4 tiles — its closing `</div>` is right before the `{/* Auto-sync toggle row ... */}` comment, around line 317). Insert immediately after that closing `</div>`:
|
||||
|
||||
```tsx
|
||||
<IntegrationHealthStrip integrations={integrations} t={t} />
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the new UI tests, watch them pass**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "renders a row per integration|empty state when there are no integrations|integration strip between"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Run the entire file's tests to confirm no regression**
|
||||
|
||||
Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
|
||||
Expected: All tests PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/components/intelligence/MemoryTreeStatusPanel.tsx \
|
||||
app/src/components/intelligence/MemoryTreeStatusPanel.test.tsx
|
||||
git commit -m "feat(memory-tree): render per-integration health strip in status panel (#2763)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Full quality suite + format + push
|
||||
|
||||
**Files:** No code changes; verification + push.
|
||||
|
||||
- [ ] **Step 1: Format**
|
||||
|
||||
Run: `pnpm format`
|
||||
Expected: Prettier + cargo fmt clean (no Rust changed in this PR; cargo fmt is a no-op but still safe).
|
||||
|
||||
- [ ] **Step 2: Lint**
|
||||
|
||||
Run: `pnpm lint`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: i18n parity + English-detection**
|
||||
|
||||
Run: `pnpm i18n:check && pnpm i18n:english:check`
|
||||
Expected: Both PASS.
|
||||
|
||||
- [ ] **Step 5: Full Vitest suite**
|
||||
|
||||
Run: `pnpm debug unit`
|
||||
Expected: All tests PASS. New MemoryTreeStatusPanel cases are visible in the summary.
|
||||
|
||||
- [ ] **Step 6: Coverage spot-check (changed lines)**
|
||||
|
||||
Run: `pnpm test:coverage --run`
|
||||
Expected: PASS; new lines in `MemoryTreeStatusPanel.tsx` and `memoryTree.ts` are exercised by the tests added above (classifier, icon map, render branches, empty state). If diff-cover reports any new line uncovered, add a targeted test before pushing — don't ship below the 80 % gate.
|
||||
|
||||
- [ ] **Step 7: Stage any format-only changes that fell out of Step 1**
|
||||
|
||||
```bash
|
||||
git status
|
||||
# If there are uncommitted Prettier-only changes:
|
||||
git add -p # review and stage the trivial fixups
|
||||
git commit -m "chore: pnpm format pass"
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Push to fork**
|
||||
|
||||
```bash
|
||||
git push aniketh feat/memory-tree-integration-health -u
|
||||
```
|
||||
|
||||
Expected: branch lands on `github.com/CodeGhost21/openhuman`. If the pre-push hook fails on `prettier: command not found` (fresh worktree missing `node_modules`), check `cargo fmt --check` is clean and push with `--no-verify` — this is a known worktree gotcha and noted in `.claude/memory.md`.
|
||||
|
||||
- [ ] **Step 9: Open PR against upstream**
|
||||
|
||||
```bash
|
||||
gh pr create \
|
||||
--repo tinyhumansai/openhuman \
|
||||
--base main \
|
||||
--head CodeGhost21:feat/memory-tree-integration-health \
|
||||
--title "feat(memory-tree): per-integration health strip (#2763)" \
|
||||
--body-file - <<'EOF'
|
||||
## Summary
|
||||
|
||||
Adds a compact per-integration health strip inside `MemoryTreeStatusPanel`, between the four pipeline tiles and the auto-sync toggle. Each row shows provider icon + name + chunk count + relative last-sync time + an Active/Stale pill. Closes #2763 (#1856 Part 3).
|
||||
|
||||
- Reuses the existing `openhuman.memory_sync_status_list` RPC — no Rust changes.
|
||||
- Single shared poll: `useMemoryTreeStatus` now fetches pipeline + sync-status in parallel on the existing 1.5s / 4s adaptive timer.
|
||||
- Status mapping is pure TS: `freshness=active` → Active; `freshness=recent|idle` → Stale.
|
||||
- i18n: 5 new keys, real translations across all 14 locales.
|
||||
|
||||
## Deviations from issue acceptance criteria
|
||||
|
||||
These are intentional; see `docs/superpowers/specs/2026-06-02-memory-tree-integration-health-design.md` for the full rationale:
|
||||
|
||||
- **AC #1** (extend `memory_tree_pipeline_status` with `integrations` array) — instead we consume the pre-existing `openhuman.memory_sync_status_list` RPC. Same data; no schema bump.
|
||||
- **AC #2** (`Active / Stale / Error`) — we ship **Active / Stale** only. Per-provider Error attribution needs new core work (`mem_tree_jobs` has no `source_kind` / `source_id` column); I'll file the follow-up after this lands.
|
||||
|
||||
## Test plan
|
||||
|
||||
- [ ] `pnpm typecheck`
|
||||
- [ ] `pnpm lint`
|
||||
- [ ] `pnpm i18n:check`
|
||||
- [ ] `pnpm i18n:english:check`
|
||||
- [ ] `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx`
|
||||
- [ ] `pnpm test:coverage` — changed-lines coverage ≥ 80 %
|
||||
- [ ] Manual: open Intelligence page with at least one Composio integration connected; confirm strip renders with correct freshness pill and updates on the shared poll.
|
||||
- [ ] Manual: with zero integrations, confirm the empty-state copy renders.
|
||||
|
||||
## Submission Checklist
|
||||
|
||||
- [x] Branch is based on `tinyhumansai/openhuman:main`.
|
||||
- [x] PR title follows conventional commits.
|
||||
- [x] Tests added/updated for changed behavior.
|
||||
- [x] i18n keys added to all 14 locale files with real translations.
|
||||
- [x] Diff coverage ≥ 80 % on changed lines.
|
||||
- [x] No Rust changes (N/A for `cargo check`, `cargo fmt`, `cargo test`).
|
||||
- [x] Linked issue: #2763.
|
||||
|
||||
EOF
|
||||
```
|
||||
|
||||
Expected: PR URL printed. Drop it in chat for the user.
|
||||
|
||||
- [ ] **Step 10: Verify PR landed cleanly**
|
||||
|
||||
Run: `gh pr view --web` (optional — opens in browser) or `gh pr checks <PR-number>` to watch CI start.
|
||||
Expected: Submission Checklist job picks up the `[x]`-filled checklist; `i18n:check`, `i18n:english:check`, lint, typecheck, unit tests, and coverage all PASS on CI.
|
||||
|
||||
---
|
||||
|
||||
## Self-review (done before save)
|
||||
|
||||
**Spec coverage:**
|
||||
- Architecture (strip inside panel, shared poll) → Tasks 4 + 6.
|
||||
- Status mapping → Task 5.
|
||||
- i18n parity → Tasks 2 + 3.
|
||||
- Test coverage → Tasks 4–6 (each TDD'd).
|
||||
- Deviations called out → Task 7 PR body Step 9.
|
||||
- No new RPC, no Rust → no Rust task; Task 7 still runs format/typecheck/lint (no `pnpm rust:check` because zero Rust changed).
|
||||
|
||||
**Placeholder scan:** None — every code block is complete and copy-pasteable. No "TBD" / "TODO" / "fill in details". Translations are written out per locale.
|
||||
|
||||
**Type consistency:** `MemorySyncStatusRow` defined once in Task 1, referenced verbatim in Tasks 4 / 5 / 6. `classifyIntegration` / `providerIconChar` signatures match between definition (Task 5) and call sites (Task 6). `IntegrationHealth` type matches the union returned by `classifyIntegration`.
|
||||
|
||||
**Risks the engineer should know about:**
|
||||
1. The pre-push hook can fail on a fresh worktree (`prettier: command not found`); Task 7 Step 8 calls this out and points at the memory note.
|
||||
2. `pnpm i18n:english:check` is strict about Latin-script values — if a Spanish/Portuguese/Italian translation accidentally reuses the English word verbatim it will fail. The recommended translations above were chosen to avoid this; if a reviewer requests a different word, double-check it doesn't collide with English-only function words.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,486 +0,0 @@
|
||||
# Telegram remote-control phase 2 — inline approvals
|
||||
|
||||
**Issue**: [#1805](https://github.com/tinyhumansai/openhuman/issues/1805) (phase 2 of N)
|
||||
**Date**: 2026-05-23
|
||||
**Status**: Approved for implementation
|
||||
**Phase 1**: [PR #2249](https://github.com/tinyhumansai/openhuman/pull/2249) (merged) — `/status`, `/sessions`, `/new`, `/help`
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
When `ApprovalGate` parks a tool call, every Telegram chat that has a session binding receives an inline-button prompt; whoever (or whatever surface) decides first wins, and all chats' prompts are edited to reflect the final state with attribution. Add a `/pending` command for on-demand recovery when the auto-broadcast was missed.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **Agent Q&A elicitation.** Free-form "ask the agent a question" already works via plain reply to the bound thread. This slice does not introduce a structured question/answer primitive.
|
||||
- **Active expiry sweeping.** Buttons may go stale; tapping a stale button is the path that surfaces "expired" to the user. No background sweep task.
|
||||
- **Real Telegram automation in CI.** Closest deterministic harness lives in the existing Rust integration test against `mockito`.
|
||||
- **Other slices in #1805** (live activity ticks, `/abort`/`/detach`, scheduled tasks, file browsing, mode/model switching, worktree). Each will be its own spec → plan → PR cycle.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ ApprovalGate │ intercepts external-effect tool calls
|
||||
│ (existing) │ parks future on oneshot
|
||||
└────────┬─────────┘
|
||||
│ publishes
|
||||
▼
|
||||
┌───────────────────────────────┐
|
||||
│ DomainEvent::ApprovalRequested│
|
||||
└─────────────┬─────────────────┘
|
||||
│ event bus
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────────┐ ┌──────────────────────┐ ┌────────────────┐
|
||||
│ Desktop UI │ │ TelegramApproval- │ │ (future │
|
||||
│ (existing) │ │ Subscriber (new) │ │ providers) │
|
||||
└───────┬────────┘ └──────────┬───────────┘ └────────────────┘
|
||||
│ │
|
||||
│ │ for every bound chat:
|
||||
│ │ sendMessage + inline_keyboard
|
||||
│ │ record (chat_id, msg_id) in pending_map
|
||||
│ ▼
|
||||
│ ┌─────────────────┐
|
||||
│ │ Telegram Bot │
|
||||
│ │ API │
|
||||
│ └────────┬────────┘
|
||||
│ │ user taps button
|
||||
│ ▼
|
||||
│ ┌─────────────────────────┐
|
||||
│ │ channel_recv.rs: │
|
||||
│ │ callback_query handler │ ← allowlist re-check
|
||||
│ │ (new branch) │
|
||||
│ └────────────┬────────────┘
|
||||
│ │ approval_decide(
|
||||
│ │ request_id, decision,
|
||||
│ │ actor=@user, surface="telegram")
|
||||
▼ ▼
|
||||
approval_decide RPC ───────┐
|
||||
│ publishes
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ DomainEvent::ApprovalDecided │
|
||||
│ (extended w/ decided_by_actor + │
|
||||
│ decided_by_surface) │
|
||||
└────────────────┬─────────────────┘
|
||||
│
|
||||
▼
|
||||
TelegramApprovalSubscriber.handle()
|
||||
for each (chat_id, msg_id) in pending_map:
|
||||
editMessageText("✓ decided by @who via X")
|
||||
[remove inline_keyboard]
|
||||
pending_map.remove(request_id)
|
||||
```
|
||||
|
||||
### Key design choices
|
||||
|
||||
| Choice | Decision | Rationale |
|
||||
|---|---|---|
|
||||
| **Routing** | Broadcast to all chats with a session binding | Mirrors multi-device approval UX; first-decision-wins; `pending_map` carries the chat list for the follow-up edit. |
|
||||
| **Pending → message-ids map** | In-memory inside `TelegramApprovalSubscriber` | After a core restart, taps on still-visible buttons fail gracefully ("Already decided" toast). Persistence would cost IO + schema churn for an edge case. |
|
||||
| **Stale tap UX** | Toast via `answerCallbackQuery` + best-effort message edit | Operator never sees a "dead button" with no feedback. |
|
||||
| **Cross-chat sync** | Subscribe to `ApprovalDecided`; edit every recorded posted message | Visibility into who acted and from where. |
|
||||
| **Decider identity** | Extend `ApprovalDecided` event + `approval_decide` RPC + `ApprovalAuditEntry` with optional `decided_by_actor` / `decided_by_surface` | Without this, "decided by @who via X" is fiction. Surfaces other than Telegram default to `surface="desktop"`. |
|
||||
| **Telegram API surface** | New `pub(crate)` methods on `TelegramChannel`: `send_with_inline_keyboard`, `edit_message_text`, `answer_callback_query` | Telegram-specific primitives stay in the provider; the generic `Channel` trait isn't dragged into approvals. |
|
||||
| **Subscriber → callback bridge** | `static GLOBAL_APPROVAL_SUB: OnceLock<Arc<TelegramApprovalSubscriber>>` | Same singleton pattern as `ApprovalGate::try_global`; `channel_recv.rs` reaches the subscriber without restructuring the long-poll loop. |
|
||||
| **`/pending` command** | One chat, on demand; reuses the same send + button + map registration path | Critical when the auto-broadcast was missed. Negligible extra code. |
|
||||
| **Expiry handling** | Render `_Expires in Xm_` when `expires_at` is set; no background sweep; stale taps self-correct | Adding a sweep is its own bucket of work. |
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
### New
|
||||
|
||||
| File | Purpose | Approx LOC |
|
||||
|---|---|---|
|
||||
| `src/openhuman/channels/providers/telegram/approval_bus.rs` | `TelegramApprovalSubscriber` — `EventHandler` on `approval` domain. Holds `Arc<TelegramChannel>`, `workspace_dir`, `pending_map: parking_lot::Mutex<HashMap<String, Vec<PostedPrompt>>>`. Provides `decide_from_callback(...)` for `channel_recv.rs`. Exposes `set_global`/`try_global` over a `OnceLock`. | ~250 |
|
||||
| `src/openhuman/channels/providers/telegram/approval_bus_tests.rs` | Unit tests for the subscriber (see Testing). | ~300 |
|
||||
|
||||
### Modified — Telegram provider
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/openhuman/channels/providers/telegram/mod.rs` | `mod approval_bus;` + `pub use approval_bus::TelegramApprovalSubscriber;` + `#[cfg(test)] #[path = "approval_bus_tests.rs"] mod approval_bus_tests;`. |
|
||||
| `src/openhuman/channels/providers/telegram/channel_send.rs` | Three new `pub(crate) async` methods on `TelegramChannel`: `send_with_inline_keyboard(chat_id, text, keyboard) -> Result<i64>`, `edit_message_text(chat_id, message_id, text, keyboard: Option<&Value>) -> Result<()>`, `answer_callback_query(callback_query_id, text, show_alert) -> Result<()>`. |
|
||||
| `src/openhuman/channels/providers/telegram/channel_recv.rs` | New top-of-loop branch: `if let Some(cb) = update.get("callback_query") { self.handle_callback_query(cb).await; return; }`. New `handle_callback_query` method (allowlist re-check, parse `appr:<o\|a\|d>:<rid>`, dispatch). |
|
||||
| `src/openhuman/channels/providers/telegram/remote_control.rs` | Add `TELEGRAM_CMD_PENDING = "/pending"` const + parser arm + `build_pending_response(...)` async builder. The builder uses the subscriber (`TelegramApprovalSubscriber::try_global()`) to broadcast prompts for currently-pending approvals into the calling chat only, registering them in `pending_map`. Help text updated. |
|
||||
|
||||
### Modified — core / approval domain
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/core/event_bus/events.rs` | `DomainEvent::ApprovalDecided` gains `decided_by_actor: Option<String>`, `decided_by_surface: Option<String>`. |
|
||||
| `src/openhuman/approval/types.rs` | `ApprovalAuditEntry` gains the same two optional fields. |
|
||||
| `src/openhuman/approval/store.rs` | SQLite migration: `ALTER TABLE approval_audit ADD COLUMN decided_by_actor TEXT; ALTER TABLE approval_audit ADD COLUMN decided_by_surface TEXT;`. Read/write paths updated. |
|
||||
| `src/openhuman/approval/gate.rs` | `decide()` gains `actor: Option<String>`, `surface: Option<String>`; threaded into the audit insert and the published event. |
|
||||
| `src/openhuman/approval/rpc.rs` | `approval_decide` RPC gains two optional params. When both are absent, default `surface = Some("desktop")` for back-compat with older app builds. |
|
||||
| `src/openhuman/approval/schemas.rs` | Schema definitions updated to reflect the optional params. |
|
||||
|
||||
### Modified — runtime wiring
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/openhuman/channels/runtime/startup.rs` | After the existing `_telegram_remote_handle` block: if `channels_by_name.get("telegram")` downcasts to `TelegramChannel`, construct + `set_global` + subscribe the new `TelegramApprovalSubscriber`. On downcast failure, log error + continue (approvals fall back to desktop). |
|
||||
|
||||
A small surface change may be needed to make the `Arc<dyn Channel>` → `Arc<TelegramChannel>` conversion work. Acceptable options, ordered by preference:
|
||||
|
||||
1. **Helper free function** in `src/openhuman/channels/providers/telegram/mod.rs`:
|
||||
```rust
|
||||
pub fn try_downcast(arc: &Arc<dyn Channel>) -> Option<Arc<TelegramChannel>> { ... }
|
||||
```
|
||||
No trait changes; only the call site at `startup.rs` knows about it.
|
||||
2. **`as_any` on `Channel` trait**, then `Arc::downcast` via `Arc<dyn Any>`. Slightly larger blast radius.
|
||||
|
||||
The plan should pick (1) unless implementation reveals a blocker.
|
||||
|
||||
### Modified — capability catalog
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/openhuman/about_app/catalog.rs` | Extend the existing Telegram remote-control entry with `"Inline approval / deny / approve-always buttons"` + `"/pending"` capability strings. |
|
||||
|
||||
### Modified — frontend
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `app/src/services/api/approvalApi.ts` (or wherever the `approval_decide` wrapper lives) | Add optional `actor` / `surface` params; default `surface: "desktop"` on desktop callers. |
|
||||
| `app/src/services/api/__tests__/approvalApi.test.ts` | One test asserting the wrapper passes `surface: "desktop"` when caller omits both. |
|
||||
| `app/src/components/channels/TelegramConfig.tsx` | A single status-line addition under existing remote-control copy: "Remote approvals: enabled (always-on when Telegram is connected)". No new toggle. |
|
||||
| `app/src/components/channels/__tests__/TelegramConfig.test.tsx` | Snapshot/render assertion for the new copy. |
|
||||
|
||||
### Modified — E2E / integration
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `tests/json_rpc_e2e.rs` | New scenario `approval_decide_accepts_actor_and_surface_optional_params` — call with both fields present and absent. |
|
||||
| `src/openhuman/channels/tests/telegram_integration.rs` | New scenario `approval_round_trip_via_callback_query` — boot TelegramChannel + subscriber against mockito; bind one chat; publish synthetic `ApprovalRequested`; assert mock receives `sendMessage` with inline keyboard; feed synthetic `callback_query`; assert `approval_decide` → `ApprovalDecided` → mock receives `editMessageText` + `answerCallbackQuery`. |
|
||||
|
||||
---
|
||||
|
||||
## Data flow
|
||||
|
||||
### Subscriber registration (startup)
|
||||
|
||||
`src/openhuman/channels/runtime/startup.rs`, immediately after `_telegram_remote_handle`:
|
||||
|
||||
```rust
|
||||
let _telegram_approval_handle = match channels_by_name.get("telegram")
|
||||
.and_then(crate::openhuman::channels::providers::telegram::try_downcast)
|
||||
{
|
||||
Some(tg) => {
|
||||
let sub = Arc::new(TelegramApprovalSubscriber::new(
|
||||
tg,
|
||||
config.workspace_dir.clone(),
|
||||
));
|
||||
TelegramApprovalSubscriber::set_global(Arc::clone(&sub));
|
||||
let handle = bus.subscribe(sub);
|
||||
tracing::debug!("[telegram-approval] registered TelegramApprovalSubscriber");
|
||||
Some(handle)
|
||||
}
|
||||
None => {
|
||||
if channels_by_name.contains_key("telegram") {
|
||||
tracing::error!("[telegram-approval] channel present but downcast failed; approvals via Telegram disabled");
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### `ApprovalRequested` → broadcast
|
||||
|
||||
```rust
|
||||
async fn handle(&self, event: &DomainEvent) {
|
||||
match event {
|
||||
DomainEvent::ApprovalRequested { request_id, tool_name, action_summary, .. } => {
|
||||
let bindings = self.load_bindings_snapshot();
|
||||
if bindings.is_empty() {
|
||||
tracing::debug!("[telegram-approval] no bound chats; skip id={request_id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-insert empty vec so a racing ApprovalDecided event finds a
|
||||
// (possibly partial) list rather than misses entirely.
|
||||
self.pending_map.lock().insert(request_id.clone(), Vec::new());
|
||||
|
||||
let text = render_prompt(tool_name, action_summary, *expires_at);
|
||||
let keyboard = approval_keyboard(request_id, tool_name);
|
||||
|
||||
for binding in bindings {
|
||||
match self.channel.send_with_inline_keyboard(&binding.chat_id, &text, &keyboard).await {
|
||||
Ok(message_id) => {
|
||||
self.pending_map.lock()
|
||||
.get_mut(request_id)
|
||||
.map(|v| v.push(PostedPrompt { chat_id: binding.chat_id.clone(), message_id }));
|
||||
}
|
||||
Err(err) => tracing::warn!(
|
||||
"[telegram-approval] send failed chat={} id={} err={}",
|
||||
binding.chat_id, request_id, err,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
DomainEvent::ApprovalDecided { request_id, decision, decided_by_actor, decided_by_surface, .. } => {
|
||||
let posted = match self.pending_map.lock().remove(request_id) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
tracing::debug!("[telegram-approval] decided event for unknown id={request_id}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let final_text = render_decided(decision, decided_by_actor.as_deref(), decided_by_surface.as_deref());
|
||||
for p in posted {
|
||||
if let Err(err) = self.channel.edit_message_text(&p.chat_id, p.message_id, &final_text, None).await {
|
||||
tracing::warn!(
|
||||
"[telegram-approval] edit failed chat={} msg={} err={}",
|
||||
p.chat_id, p.message_id, err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`approval_keyboard(request_id, tool_name)` returns three rows:
|
||||
|
||||
```json
|
||||
[
|
||||
[{"text": "✅ Approve once", "callback_data": "appr:o:<rid>"}],
|
||||
[{"text": "♻ Always for <tool>", "callback_data": "appr:a:<rid>"}],
|
||||
[{"text": "❌ Deny", "callback_data": "appr:d:<rid>"}]
|
||||
]
|
||||
```
|
||||
|
||||
`callback_data` is limited to 64 bytes by Telegram. `appr:<o|a|d>:<uuid>` is comfortably under.
|
||||
|
||||
### Inbound `callback_query` → decide
|
||||
|
||||
In `channel_recv.rs`:
|
||||
|
||||
```rust
|
||||
if let Some(cb) = update.get("callback_query") {
|
||||
if let Err(err) = self.handle_callback_query(cb).await {
|
||||
tracing::warn!("[telegram][callback] handler error: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// (existing message handler follows)
|
||||
```
|
||||
|
||||
`handle_callback_query`:
|
||||
|
||||
1. Parse `id`, `from.username`, `from.id`, `message.chat.id`, `message.message_id`, `data`.
|
||||
2. **Re-check allowlist** with `is_any_user_allowed([username_norm, id_norm])`. Non-allowlisted → `answer_callback_query(id, "Not authorized", show_alert=true)`; log; return.
|
||||
3. Parse `data` as `appr:<o|a|d>:<rid>`. Malformed → toast "Invalid action"; return.
|
||||
4. Map to `ApprovalDecision::{ApproveOnce, ApproveAlwaysForTool, Deny}`.
|
||||
5. Call `approval_decide(rid, decision, actor=Some(@username), surface=Some("telegram"))`.
|
||||
6. On `Err` whose source resolves to "no pending approval found" → toast "Already decided or expired"; best-effort `edit_message_text(chat_id, message_id, "⏱ already decided or expired", None)`.
|
||||
7. On `Ok(_)` → toast "Decision recorded". The bus-driven edit (`ApprovalDecided` branch) updates message text in all bound chats.
|
||||
|
||||
### `/pending` command
|
||||
|
||||
In `remote_control.rs`:
|
||||
|
||||
1. `approval_list_pending().await?` → `Vec<PendingApproval>`.
|
||||
2. Empty → reply `_No approvals pending._`.
|
||||
3. Non-empty → for each row (capped at 5):
|
||||
- **Dedupe per chat**: if `pending_map[request_id]` already has a `PostedPrompt` whose `chat_id` matches the calling chat, skip — the operator already has a live prompt in this chat from the auto-broadcast.
|
||||
- Otherwise, construct prompt + keyboard exactly like the auto-broadcast path; call `subscriber.channel.send_with_inline_keyboard(...)` against **this chat only**; on success push `PostedPrompt` into `pending_map[request_id]` (creating the entry if absent — `/pending` may run for a request still parked but never broadcast, e.g. across a core restart).
|
||||
4. If `> 5` rows: append a `_… and N more (open the desktop app to see all)_` line.
|
||||
|
||||
### Decider identity propagation
|
||||
|
||||
- `approval_decide` JSON-RPC body extends from `{ request_id, decision }` to `{ request_id, decision, actor?, surface? }`.
|
||||
- `ApprovalGate::decide(...)`: signature gains `actor: Option<String>, surface: Option<String>`; values are passed through to `pending_approvals` row mutation + `approval_audit` row insert + the published `ApprovalDecided` event.
|
||||
- SQLite migration: two new nullable TEXT columns on `approval_audit`. Existing rows keep `NULL` (which renders as "decided" with no "by" suffix — back-compat for any historical UI that re-reads audit).
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
| Failure | Behavior |
|
||||
|---|---|
|
||||
| `send_with_inline_keyboard` fails for one chat during broadcast | Log + skip; other chats still get a working prompt; `pending_map` records only successes. |
|
||||
| All sends fail | Log error. Desktop UI still functions. No retry — single event, single attempt. |
|
||||
| `edit_message_text` fails on a posted prompt | Log + continue editing the rest. Stale message remains; the next tap triggers the "already decided" self-correction path. |
|
||||
| `approval_decide` returns "gate not installed" | Toast "Approvals disabled" + log. Defensive; subscriber would not have been registered if the gate is absent in a normal boot. |
|
||||
| `approval_decide` returns "no pending approval found" | Toast "Already decided or expired" + best-effort edit on the tapped message. |
|
||||
| Callback `data` malformed | Toast "Invalid action" + log. Poll loop continues. |
|
||||
| Channel downcast fails at startup | Log error; subscriber not registered. Approvals fall back to desktop-only. Core continues. |
|
||||
| SQLite migration fails (existing prod row count > 0, column add fails) | Surfaces during gate boot via the existing migration runner. No new logic here — leverage the existing migration error path. |
|
||||
|
||||
## Edge cases
|
||||
|
||||
| Case | Behavior |
|
||||
|---|---|
|
||||
| Chat binds via `/new` *after* `ApprovalRequested` was published | Not in bindings snapshot at broadcast time → no prompt. `/pending` covers the gap. |
|
||||
| Chat is unbound between request and decision | Edits still post to the recorded `(chat_id, message_id)`; Telegram doesn't care about our `bindings` state. |
|
||||
| Group chat: bound by allowlisted operator + has non-allowlisted members | Callback re-checks `from.username` / `from.id`; only allowlisted taps proceed. |
|
||||
| Two operators tap simultaneously | First `approval_decide` wins; second hits "no pending approval found" → toast + self-correcting edit. |
|
||||
| Same operator taps Approve-once then Deny in quick succession | Same path — second tap loses. |
|
||||
| Core restart between request and decision | `pending_map` is lost. Old buttons in Telegram still call back. Allowlist still works (loaded from config). `approval_decide` returns "no pending approval found" → toast + tap-local edit only. Other chats stay stale. **Acceptable**: the parked future itself was lost on restart already; this slice doesn't change that. |
|
||||
| `expires_at` reached while message is live | No active expiry sweep. The next tap returns "no pending approval found" and we render `⏱ already decided or expired`. |
|
||||
| `mention_only` mode is on | Does not apply to `callback_query` — those are direct button taps, not message mentions. Always processed if allowlist passes. |
|
||||
| `pairing_code_active` (no chats bound yet) | Broadcast path early-returns (`bindings.is_empty()`). Future approvals reach chats once binding completes. |
|
||||
| Concurrent `ApprovalRequested` + `ApprovalDecided` ordering | Mitigated by pre-inserting an empty `Vec` into `pending_map` *before* awaited sends, so a `Decided` arriving mid-broadcast still finds an entry (possibly partial) to drain and remove. |
|
||||
|
||||
## Security
|
||||
|
||||
| Concern | Mitigation |
|
||||
|---|---|
|
||||
| Non-allowlisted user taps a button | Re-check `from.username` + `from.id` against the allowlist on every callback. Non-allowlisted → toast "Not authorized" + log. Identical to the message-path check. |
|
||||
| `callback_data` spoofing | Telegram guarantees `from` on `callback_query` is the real user; allowlist re-check covers spoofed senders. `request_id` is still validated against the gate, which checks session + existence. |
|
||||
| PII / secrets in prompt body | `action_summary` and `args_redacted` are already scrubbed by `approval/redact.rs`. The prompt body uses only `action_summary` — no raw args, no chat content. |
|
||||
| Logging | Stable prefix `[telegram-approval]`. Log `request_id`, `tool_name`, `decision`, `chat_id`, `actor`, `surface`. Never log message bodies, args, or full `action_summary`. |
|
||||
| Bot token in logs | Existing telegram code redacts; new send helpers reuse the existing `api_url(...)` helper, which already keeps the token out of error strings. |
|
||||
| Callback flood from a misbehaving client | Allowlist gates this. No per-user rate limiting in this slice — out of scope. |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Target: ≥80% diff coverage on changed lines. Behavior over implementation. No real network.
|
||||
|
||||
### Rust unit — `approval_bus_tests.rs` (new)
|
||||
|
||||
Built on a fake `TelegramChannel` (constructor takes a `mockito` URL; send helpers wrap `reqwest` against it — same pattern in existing `channel_tests.rs`). Each test uses an isolated `tempdir` for `workspace_dir` + `TelegramSessionStore`.
|
||||
|
||||
| Test | What it proves |
|
||||
|---|---|
|
||||
| `broadcasts_to_all_bound_chats` | Two bound chats; publish `ApprovalRequested`; mock receives two `sendMessage` calls with matching inline_keyboard; `pending_map` has 2 entries. |
|
||||
| `no_bound_chats_skips_broadcast` | Zero bindings → zero sends, no map entry, no panic. |
|
||||
| `partial_send_failure_still_records_successes` | First chat 200, second chat 500. `pending_map` has only the successful chat. Warn logged. |
|
||||
| `pre_insert_avoids_lost_decided_race` | Confirm `pending_map.insert(rid, Vec::new())` runs **before** awaited sends; simulate `ApprovalDecided` mid-broadcast and assert no missed-entry log. |
|
||||
| `edits_all_chats_on_decided` | Two bound; broadcast; `ApprovalDecided{actor=Some("@op"), surface=Some("telegram")}` → two `editMessageText` calls; body contains "approved by @op via telegram"; keyboard removed. |
|
||||
| `decided_event_for_unknown_id_is_noop` | `Decided` for an id not in `pending_map` → zero sends, debug log. |
|
||||
| `decided_event_renders_surface_unknown_gracefully` | `actor=None, surface=None` → body says "approved" with no "by" suffix. |
|
||||
| `expires_in_rendered_when_set` | `ApprovalRequested` with `expires_at = now+300s` → outgoing text contains `Expires in 5m`. With `expires_at=None` → no expiry line. |
|
||||
| `prompt_text_does_not_leak_args` | Pass `action_summary` containing only a redacted summary; assert outgoing text contains *only* that, no `args_redacted`. |
|
||||
|
||||
### Rust unit — `channel_recv` callback_query (extended `channel_tests.rs`)
|
||||
|
||||
| Test | What it proves |
|
||||
|---|---|
|
||||
| `callback_query_from_allowed_user_dispatches_decide` | Build update with `data="appr:o:<rid>"`, allowed user; stub `approval_decide`; assert one call with `decision=ApproveOnce, actor=Some("@user"), surface=Some("telegram")`. |
|
||||
| `callback_query_from_unauthorized_user_is_rejected` | Same but unauthorized `from`; assert zero `approval_decide` calls, one `answerCallbackQuery` text≈"Not authorized". |
|
||||
| `callback_query_malformed_data_toasts_invalid` | `data="garbage"` → no decide, one toast text≈"Invalid". |
|
||||
| `callback_query_already_decided_toasts_and_attempts_edit` | Stub `approval_decide` → `Err("no pending approval found")` → one toast + one `editMessageText` on the tapped (chat_id, message_id). |
|
||||
| `callback_query_does_not_fall_through_to_message_handler` | Update with `callback_query` and no `message.text` → existing inbound-message path not invoked; no `ChannelMessageReceived`. |
|
||||
| `callback_query_allowlist_lookup_uses_id_and_username` | Username not in allowlist but `from.id` is → permitted. Mirrors message-path behavior. |
|
||||
|
||||
### Rust unit — `remote_control` `/pending` (extended `remote_control_tests.rs`)
|
||||
|
||||
| Test | What it proves |
|
||||
|---|---|
|
||||
| `pending_command_with_no_approvals_replies_empty` | Empty `approval_list_pending` → reply ≈"No approvals pending". |
|
||||
| `pending_command_renders_each_row_with_buttons` | Three pending → three sends with full keyboard; each recorded in `pending_map`. |
|
||||
| `pending_command_caps_at_5_rows` | Ten pending → first 5 + `_… and 5 more_` line. |
|
||||
| `pending_command_is_chat_local_not_broadcast` | Calling `/pending` from chat A while B is bound → only A receives prompts. |
|
||||
| `pending_command_dedupes_per_chat` | `pending_map[rid]` already has a PostedPrompt for chat A; `/pending` in chat A → that row is skipped (zero new sends for that rid), other rids still rendered. |
|
||||
|
||||
### Approval-domain tests (extended)
|
||||
|
||||
| File | Test |
|
||||
|---|---|
|
||||
| `src/openhuman/approval/rpc.rs` (or new `rpc_tests.rs`) | `approval_decide_threads_actor_and_surface_into_event` — call with `Some` values; capture `ApprovalDecided`; assert both propagate. |
|
||||
| `src/openhuman/approval/store.rs` | `audit_row_round_trips_actor_and_surface`; `audit_row_round_trips_nullable_actor_and_surface`. |
|
||||
| `src/openhuman/approval/gate.rs` tests | `decide_records_actor_and_surface_to_audit` — end-to-end through the gate. |
|
||||
|
||||
### JSON-RPC E2E — `tests/json_rpc_e2e.rs`
|
||||
|
||||
| Scenario | What it proves |
|
||||
|---|---|
|
||||
| `approval_decide_accepts_actor_and_surface_optional_params` | Hit `approval_decide` over JSON-RPC with both fields, `Some` and absent. Both succeed. Existing response shape preserved (additive only). |
|
||||
|
||||
### Frontend tests
|
||||
|
||||
| File | Test |
|
||||
|---|---|
|
||||
| `app/src/services/api/__tests__/approvalApi.test.ts` | `approval_decide_passes_surface_desktop_by_default` — caller omits both → wrapper sends `surface: "desktop"`. |
|
||||
| `app/src/components/channels/__tests__/TelegramConfig.test.tsx` | New "Remote approvals: enabled …" copy renders when Telegram is connected. |
|
||||
|
||||
### Deterministic Telegram E2E — `telegram_integration.rs`
|
||||
|
||||
`approval_round_trip_via_callback_query`:
|
||||
|
||||
1. Boot TelegramChannel + TelegramApprovalSubscriber against `mockito`.
|
||||
2. Bind one chat via `/new` (mock returns success).
|
||||
3. Publish synthetic `ApprovalRequested`.
|
||||
4. Assert mock received `sendMessage` with inline_keyboard.
|
||||
5. Feed synthetic `callback_query` with valid `data` from allowed user.
|
||||
6. Assert `approval_decide` invoked → `ApprovalDecided` published → mock received `editMessageText` + `answerCallbackQuery`.
|
||||
|
||||
This is the closest deterministic equivalent to real Telegram automation the project supports today.
|
||||
|
||||
### Coverage estimate
|
||||
|
||||
| Area | New lines (approx) | Tested lines |
|
||||
|---|---|---|
|
||||
| `approval_bus.rs` | ~250 | ≥240 |
|
||||
| `channel_send.rs` (3 new helpers) | ~120 | ≥110 |
|
||||
| `channel_recv.rs` callback branch | ~80 | ≥75 |
|
||||
| `remote_control.rs` `/pending` | ~60 | ≥55 |
|
||||
| `approval/gate.rs` + `store.rs` + `rpc.rs` + event + schemas | ~70 | ≥65 |
|
||||
|
||||
Diff coverage should land comfortably above 80%; the uncovered tail is defensive error-logging arms.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria (mapping to #1805)
|
||||
|
||||
This slice satisfies the following acceptance criteria from #1805:
|
||||
|
||||
- ☑ "Users can answer agent questions and **approve/deny permission requests** from Telegram inline." (permission half only — Q&A is non-goal for this slice.)
|
||||
- ☑ "Core functionality is exposed through registered controllers and schemas, not Telegram-specific branches in transport adapters." — Telegram only adds an event-bus subscriber that calls existing `approval_decide` RPC; no Telegram branches in `approval/` domain.
|
||||
- ☑ "`src/openhuman/about_app/` is updated so the capability catalog reflects the expanded Telegram feature set."
|
||||
- ☑ "New/changed flows include substantial debug logging in Rust and app code with grep-friendly prefixes and no secret leakage." — `[telegram-approval]` prefix throughout.
|
||||
- ☑ "Rust unit/integration coverage is added for the new Telegram control flows and JSON-RPC/controller surfaces."
|
||||
- ☑ "App unit tests are added for Telegram configuration/management UI changes." (minimal, scoped to actual changes.)
|
||||
- ☑ "Desktop E2E coverage … or the nearest deterministic harness equivalent" — covered by the `telegram_integration.rs` scenario.
|
||||
- ☑ "Diff coverage ≥ 80%."
|
||||
|
||||
Out of scope for this slice (to be addressed in later phases):
|
||||
- live activity ticks during runs / `/abort` / `/detach`
|
||||
- `/task` / `/tasklist`
|
||||
- `/files` / `/ls`
|
||||
- inbound attachments / structured outbound file delivery
|
||||
- `/models` / `/mode`
|
||||
- `/projects` / `/worktree`
|
||||
|
||||
---
|
||||
|
||||
## Implementation order (for the plan)
|
||||
|
||||
1. **Core/approval changes first** — extend event, RPC, gate, audit, store + tests. Independently mergeable.
|
||||
2. **Telegram send helpers** — `send_with_inline_keyboard`, `edit_message_text`, `answer_callback_query` + tests against mockito.
|
||||
3. **`TelegramApprovalSubscriber`** + unit tests.
|
||||
4. **`channel_recv.rs` callback branch** + unit tests.
|
||||
5. **`/pending` command** + unit tests.
|
||||
6. **Wiring in `startup.rs`** + downcast helper.
|
||||
7. **Catalog update** + frontend touch + tests.
|
||||
8. **`telegram_integration.rs` round-trip** + `json_rpc_e2e.rs` scenario.
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Existing desktop frontend calls `approval_decide` without the new params | Server defaults `surface = Some("desktop")` when both are absent. Frontend update is additive. |
|
||||
| `Arc<dyn Channel>` → `Arc<TelegramChannel>` downcast may need a small trait change | Plan covers a `try_downcast` helper option first; falls back to `as_any` only if needed. |
|
||||
| Real Telegram users tap stale buttons after core restart | Acceptable — "already decided or expired" toast + best-effort edit covers it. |
|
||||
| `callback_data` length cap (64 bytes) is exceeded by long request_ids | Request ids are uuids (36 bytes); `appr:<o|a|d>:` adds 7. Total 43. Comfortable headroom. Validate at construction time in `approval_keyboard` to fail loud if this ever changes. |
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
None at design approval. Implementation may surface:
|
||||
- Exact mockito URL injection pattern in the new `approval_bus_tests.rs` — defer to plan-writing to confirm the existing test harness shape carries over.
|
||||
- Whether the frontend `approval_decide` wrapper currently lives in `services/api/` or somewhere else — defer to a brief grep at plan-writing time.
|
||||
@@ -1,380 +0,0 @@
|
||||
# Global Push-to-Talk Hotkey — Design
|
||||
|
||||
**Issue:** [tinyhumansai/openhuman#3090](https://github.com/tinyhumansai/openhuman/issues/3090) — "Global push-to-talk keybind + screen share while tabbed out / in background."
|
||||
|
||||
**Scope of this spec:** the *push-to-talk* half only. Background screen capture for the agent is acknowledged in the issue and tracked as a follow-up PR — same domain (voice / agent context), different surface area (host-screen sampling, fullscreen-game compatibility, image-token budget). Keeping them separate keeps each PR reviewable and coverage-gateable.
|
||||
|
||||
**Outcome:** the user holds a configurable global hotkey while OpenHuman is *not* the focused window (mid-game, in their IDE, on a Slack call), speaks, releases the key, and the agent answers via TTS — without OpenHuman ever stealing focus.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
- A user-configurable hold-to-talk hotkey that works while OpenHuman is in the background.
|
||||
- Mic opens on press, closes on release; transcript is auto-posted to the active chat thread and the agent's reply is spoken aloud.
|
||||
- Audible + visual feedback (chime + small always-on-top overlay) so the user knows the mic is hot without alt-tabbing.
|
||||
- Works on macOS, Windows, and Linux/X11 in v1. Wayland: documented unsupported with a clear in-app message.
|
||||
|
||||
## Non-goals (v1)
|
||||
|
||||
- Background screen capture for the agent. (Follow-up issue spawned from #3090.)
|
||||
- Streaming partial transcripts during the hold.
|
||||
- Per-thread PTT routing (always routes to the active thread).
|
||||
- A DXGI-exclusive-fullscreen overlay workaround. (Documented caveat only; chime still plays.)
|
||||
- Toggle-style PTT (we ship hold-to-talk only — the existing dictation toggle remains for press-once-press-again users).
|
||||
|
||||
---
|
||||
|
||||
## Architecture overview
|
||||
|
||||
```
|
||||
[User holds hotkey]
|
||||
│
|
||||
[Tauri shell: tauri-plugin-global-shortcut]
|
||||
│ ShortcutState::Pressed
|
||||
▼
|
||||
[app/src-tauri/src/ptt_hotkeys.rs]
|
||||
│ emit("ptt://start", { session_id })
|
||||
▼
|
||||
[app/src/services/pttService.ts] ─┐
|
||||
│ voice/audio_capture.start │ hold phase
|
||||
│ playChime("open") │
|
||||
│ invoke("show_ptt_overlay", { active }) │
|
||||
│ armWatchdog(10s) │
|
||||
─┘
|
||||
|
||||
[User releases hotkey]
|
||||
│ ShortcutState::Released
|
||||
▼
|
||||
[ptt_hotkeys.rs] emit("ptt://stop", { session_id })
|
||||
│
|
||||
[pttService.onStop]
|
||||
│ voice/audio_capture.finalize → Buffer
|
||||
│ playChime("close") + hide overlay
|
||||
│ dictationListener.transcribe(buf) → text
|
||||
│ chatRuntime.sendMessage({ text, speakReply: true, source: "ptt" })
|
||||
▼
|
||||
[Core: openhuman.channel_web_chat]
|
||||
│ normal agent turn
|
||||
│ on assistant final text:
|
||||
│ voice::reply_speech.synthesize_and_play(text) // if speak_reply
|
||||
▼
|
||||
[User hears reply; OpenHuman window state never changes]
|
||||
```
|
||||
|
||||
The bulk of the work is in the **Tauri shell** (hotkey + overlay window) and the **renderer service layer** (state machine + glue). The Rust core gets exactly one additive change: a `speak_reply: bool` flag on `channel.web_chat` so TTS reply routing doesn't require the renderer to be focused or even running its normal chat UI.
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Tauri shell — `app/src-tauri/src/`
|
||||
|
||||
#### `ptt_hotkeys.rs` *(new)*
|
||||
|
||||
Owns global hotkey registration for PTT. Mirrors `dictation_hotkeys.rs` in shape, with two key differences: it listens for **both** `Pressed` and `Released`, and rejects pure-modifier shortcuts.
|
||||
|
||||
```rust
|
||||
pub(crate) struct PttHotkeyState {
|
||||
pub(crate) shortcut: Mutex<Vec<String>>, // expanded variants registered
|
||||
pub(crate) is_held: AtomicBool, // CAS-guarded press/release
|
||||
pub(crate) session_counter: AtomicU64,
|
||||
}
|
||||
|
||||
pub(crate) fn expand_ptt_shortcuts(shortcut: &str) -> Result<Vec<String>, PttError>;
|
||||
// - returns Err(EmptyShortcut) if trimmed empty
|
||||
// - returns Err(ModifierOnlyShortcut) if every token is a modifier (Ctrl/Cmd/Shift/Alt/Meta)
|
||||
// - returns Err(InvalidShortcut(...)) if the plugin parser rejects it
|
||||
// - otherwise returns 1 or 2 expanded variants (macOS CmdOrCtrl → [Cmd, Ctrl])
|
||||
|
||||
pub(crate) enum PttError {
|
||||
EmptyShortcut,
|
||||
ModifierOnlyShortcut,
|
||||
InvalidShortcut(String),
|
||||
AccessibilityRequired, // macOS
|
||||
ShortcutInUse(String), // Windows
|
||||
UnsupportedOnWayland,
|
||||
ConflictsWithDictation(String),
|
||||
RegistrationFailed(String),
|
||||
}
|
||||
```
|
||||
|
||||
#### `lib.rs` — two new IPC commands
|
||||
|
||||
```rust
|
||||
#[tauri::command]
|
||||
async fn register_ptt_hotkey(app: AppHandle<AppRuntime>, shortcut: String) -> Result<(), String>;
|
||||
|
||||
#[tauri::command]
|
||||
async fn unregister_ptt_hotkey(app: AppHandle<AppRuntime>) -> Result<(), String>;
|
||||
```
|
||||
|
||||
Behavior on `register_ptt_hotkey`:
|
||||
|
||||
1. Expand & validate via `expand_ptt_shortcuts`.
|
||||
2. Check overlap with the currently-registered dictation shortcut(s); on overlap return `ConflictsWithDictation`.
|
||||
3. Unregister any previously-registered PTT shortcut (rollback-safe — same pattern as the dictation registration).
|
||||
4. Register each expanded variant with a closure that:
|
||||
- On `Pressed`: CAS `is_held: false → true`; on success, increment `session_counter` and emit `ptt://start { session_id }`. On failure (CAS lost — auto-repeat or stuck state), drop.
|
||||
- On `Released`: CAS `is_held: true → false`; on success, emit `ptt://stop { session_id }` with the *current* counter value. On failure, drop.
|
||||
5. Persist the registered variants in `PttHotkeyState`.
|
||||
|
||||
`unregister_ptt_hotkey` unregisters all currently-registered variants and clears state. Also called on shutdown (`unregister_all` already covered by the plugin's drop).
|
||||
|
||||
#### `ptt_overlay.rs` *(new)* — dedicated overlay window
|
||||
|
||||
Lazy-create-on-first-register, destroyed on `unregister`. Window config:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| `label` | `"ptt-overlay"` |
|
||||
| `url` | `/#/ptt-overlay` (HashRouter route, mounted only in this window) |
|
||||
| `decorations` | `false` |
|
||||
| `transparent` | `true` |
|
||||
| `always_on_top` | `true` |
|
||||
| `skip_taskbar` | `true` |
|
||||
| `focus` | `false` (never accepts focus) |
|
||||
| `resizable` | `false` |
|
||||
| `shadow` | `false` |
|
||||
| `visible_on_all_workspaces` | `true` |
|
||||
| `accept_first_mouse` | `false` |
|
||||
| `size` | `160 × 56` |
|
||||
| `position` | bottom-right of primary display, 24px inset (hard-coded in v1) |
|
||||
|
||||
IPC command: `show_ptt_overlay({ active: bool, session_id: u64 })` — hides/shows the window with a 250ms fade on close. Window-local React state in `/#/ptt-overlay` toggles a pulsing red dot when `active: true`.
|
||||
|
||||
### Rust core — `src/openhuman/`
|
||||
|
||||
#### `voice/bus.rs` *(new)*
|
||||
|
||||
Per the canonical module shape, the voice domain currently has no `bus.rs`. Add one with a single subscriber-less event publisher and a new variant on `DomainEvent`:
|
||||
|
||||
```rust
|
||||
// in src/core/event_bus/events.rs
|
||||
pub enum VoiceEvent {
|
||||
PttTranscriptCommitted {
|
||||
thread_id: ThreadId,
|
||||
session_id: u64,
|
||||
text_len: usize, // never log raw transcript
|
||||
held_ms: u64,
|
||||
finalized_by_watchdog: bool,
|
||||
},
|
||||
// ...future variants
|
||||
}
|
||||
|
||||
// in DomainEvent
|
||||
Voice(VoiceEvent),
|
||||
```
|
||||
|
||||
Subscribers will be added in the follow-up screen-capture PR (the screen-intelligence domain will hook here to grab a frame when a PTT turn commits). For v1 we publish, nobody subscribes — the test asserts publish reaches a test subscriber.
|
||||
|
||||
#### Chat-send schema — `speak_reply` flag
|
||||
|
||||
The user→agent ingress RPC is **`openhuman.channel_web_chat`** (web channel provider — `src/openhuman/channels/providers/web.rs`, schema in `schemas("chat")`, handler `channel_web_chat`, dispatch through `start_chat`). The frontend already calls this from `app/src/services/chatService.ts::chatSend`. Three additive optional fields:
|
||||
|
||||
```rust
|
||||
// In the channel.web_chat input schema (web.rs schemas())
|
||||
#[serde(default)]
|
||||
pub speak_reply: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub source: Option<String>, // "ptt" | "dictation" | "type" | ...
|
||||
#[serde(default)]
|
||||
pub session_id: Option<u64>, // PTT correlation key
|
||||
```
|
||||
|
||||
Non-breaking — all fields `Option`. The flags flow through `channel_web_chat → start_chat → spawn_progress_bridge`. The progress bridge buffers `AgentProgress::TextDelta` chunks during the turn; on `AgentProgress::TurnCompleted`, if `speak_reply == Some(true)`, it calls `voice::reply_speech::synthesize_and_play(buffered_text).await`. This is the **only** Rust-core code path change beyond the schema and the bus event.
|
||||
|
||||
`source` and `session_id` are persisted on the user message metadata (via the message-record path already used by `start_chat`) and included in the `VoiceEvent::PttTranscriptCommitted` bus event for the screen-capture follow-up PR.
|
||||
|
||||
#### `about_app` capability catalog
|
||||
|
||||
Add entry:
|
||||
|
||||
```rust
|
||||
Capability {
|
||||
id: "voice.ptt",
|
||||
label: "Global push-to-talk",
|
||||
supported_on: &[Platform::MacOS, Platform::Windows, Platform::LinuxX11],
|
||||
requires: &["microphone", "global_shortcut"],
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend — `app/src/`
|
||||
|
||||
#### `services/pttService.ts` *(new singleton)*
|
||||
|
||||
State machine:
|
||||
|
||||
```
|
||||
Idle ──[ptt://start]──▶ Capturing ──[ptt://stop]──▶ Finalizing ──▶ Idle
|
||||
▲ │
|
||||
│ ├──[10s no stop]──▶ Finalizing (watchdog=true)
|
||||
│ │
|
||||
│ └──[mic-fail / preempt / register]──▶ Aborted ──▶ Idle
|
||||
```
|
||||
|
||||
API surface:
|
||||
|
||||
```ts
|
||||
interface PttService {
|
||||
init(): void; // subscribes to Tauri ptt://* events
|
||||
destroy(): void;
|
||||
// exposed for tests:
|
||||
onStart(session_id: number): Promise<void>;
|
||||
onStop(session_id: number): Promise<void>;
|
||||
cancel(reason: "preempted_by_ptt" | "mic_failure" | "user_cancel"): void;
|
||||
}
|
||||
```
|
||||
|
||||
`onStart` (in order):
|
||||
1. If a session is already active → call `cancel("preempted_by_ptt")`.
|
||||
2. `playChime("open")`.
|
||||
3. `invoke("show_ptt_overlay", { active: true, session_id })`.
|
||||
4. `voice/audio_capture.start({ session_tag: "ptt:" + session_id })`.
|
||||
5. `armWatchdog(10_000, () => this.onStop(session_id))`.
|
||||
|
||||
`onStop`:
|
||||
1. Disarm watchdog.
|
||||
2. `const buf = await voice/audio_capture.finalize()`.
|
||||
3. `playChime("close")`.
|
||||
4. `invoke("show_ptt_overlay", { active: false, session_id })`.
|
||||
5. If `buf.duration_ms < 250` → drop session, play `"no-speech"` double-click chime, log `dropped_reason: "empty_audio"`, return.
|
||||
6. `const text = await dictationListener.transcribe(buf)`.
|
||||
7. If `!text.trim()` → drop, log `dropped_reason: "empty_transcript"`, return.
|
||||
8. Resolve `activeThreadId`:
|
||||
- If `chatRuntime.activeThread` exists → use it.
|
||||
- Else → create a new thread titled `"Voice"` via `openhuman.thread_create`, mark `source: "ptt"`, use its ID.
|
||||
9. `chatRuntime.sendMessage({ threadId, body: text, metadata: { source: "ptt", session_id }, speakReply: state.ptt.speakReplies })`.
|
||||
10. Zero the audio buffer.
|
||||
|
||||
`cancel`:
|
||||
- Disarm watchdog, finalize-and-discard the audio buffer (zero it), hide overlay, play error chime, log with reason. No chat message posted.
|
||||
|
||||
Errors during the session — handled per the table in **§ Error handling** below.
|
||||
|
||||
#### `store/slices/ptt.ts` *(new redux slice)*
|
||||
|
||||
```ts
|
||||
interface PttState {
|
||||
shortcut: string | null; // null = unbound (default)
|
||||
speakReplies: boolean; // default true
|
||||
showOverlay: boolean; // default true
|
||||
isHeld: boolean; // not persisted
|
||||
}
|
||||
```
|
||||
|
||||
Persisted (except `isHeld`) via the existing redux-persist config. Re-registers the hotkey on rehydration via a sibling `useEffect` to the existing dictation init.
|
||||
|
||||
#### `pages/settings/voice/PttSettingsPanel.tsx` *(new)*
|
||||
|
||||
- Hotkey-capture widget (same component family as the dictation key picker).
|
||||
- Toggle: "Speak agent replies" (`speakReplies`).
|
||||
- Toggle: "Show overlay while held" (`showOverlay`).
|
||||
- Inline help: "Push-to-talk is off — pick a hotkey to enable." when `shortcut == null`.
|
||||
- Inline error: surfaces `PttError::ConflictsWithDictation`, `ShortcutInUse`, `AccessibilityRequired` (with a "Open Accessibility settings" button on macOS), `UnsupportedOnWayland`.
|
||||
- Inline hint: "In exclusive-fullscreen games the overlay won't render — you'll only hear the chime. Switch to borderless fullscreen for the overlay."
|
||||
|
||||
#### `pages/PttOverlayPage.tsx` *(new — rendered only in the overlay window)*
|
||||
|
||||
Borderless 160×56 region: small mic glyph, label ("Listening…"), pulsing red dot when `state.active`. Reads `active` from a local React state updated by a `useEffect` that listens for `show_ptt_overlay`-relayed events. No redux access — the overlay window has its own React root.
|
||||
|
||||
#### `ChatRuntimeProvider` — forward `speak_reply`
|
||||
|
||||
`chatService.chatSend` (already the single call site for `openhuman.channel_web_chat`) accepts `speakReply?: boolean`, `source?: string`, `sessionId?: number` and forwards them as the new optional fields. `ChatRuntimeProvider`'s `sendMessage` plumbs them through from `pttService`.
|
||||
|
||||
#### Chimes
|
||||
|
||||
- `app/src/assets/audio/ptt-open.wav` — short rising tone, ~80ms.
|
||||
- `app/src/assets/audio/ptt-close.wav` — short falling tone, ~80ms.
|
||||
- `app/src/assets/audio/ptt-error.wav` — double-click, ~120ms.
|
||||
- `app/src/assets/audio/README.md` — CC0 attribution.
|
||||
|
||||
LUFS-normalized to roughly match the existing in-app notification sound. Played via a plain `Audio` element from `pttService`.
|
||||
|
||||
#### i18n
|
||||
|
||||
New keys under a `pttSettings` / `pttOverlay` namespace in `app/src/lib/i18n/en.ts`, real translations added to all 13 non-English locale files (`ar`, `bn`, `de`, `es`, `fr`, `hi`, `id`, `it`, `ko`, `pl`, `pt`, `ru`, `zh-CN`). `pnpm i18n:check` and `pnpm i18n:english:check` gate this.
|
||||
|
||||
---
|
||||
|
||||
## Data flow / sequence diagram
|
||||
|
||||
See the architecture overview above. The key invariants:
|
||||
|
||||
- **No focus stealing.** No window is `show()`-ed with focus; `show_ptt_overlay` shows a `focus: false` window. The agent reply plays via TTS without any window-state mutation.
|
||||
- **Single mic at a time.** `voice::audio_capture` enforces this. PTT preempts in-flight dictation; dictation cannot start during a PTT session.
|
||||
- **Session ID is the correlation key.** Logged in shell + renderer + bus event + chat metadata.
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
| Failure | Behavior |
|
||||
| --- | --- |
|
||||
| Mic permission denied (`MicPermissionDenied`) | Error chime, hide overlay, log `[ptt] mic_denied`. Next time the user opens `/settings/voice`, a sticky banner links to OS mic settings. No mid-game modal. |
|
||||
| Mic stream drops mid-session (USB unplug) | `cancel("mic_failure")`. No chat message posted. |
|
||||
| STT call fails (network / model timeout) | Post message anyway as `[Voice — transcription failed]` so the user has a breadcrumb. Subsequent agent turn handles it normally. |
|
||||
| Agent turn errors | Existing chat-error UI. TTS reply just doesn't play. Overlay already hidden by this point. |
|
||||
| `ptt://stop` never arrives (OS swallowed release) | 10s watchdog finalizes. Session tagged `finalized_by_watchdog: true`. Logged at `warn`. |
|
||||
| App backgrounded during hold | Hotkey still fires (global). Overlay still shows. Chime still plays. By design. |
|
||||
| Empty / sub-threshold audio (< 250ms) | Drop session, play `no-speech` chime, log `dropped_reason: "empty_audio"`. No message posted. |
|
||||
| Empty transcript (STT returned blank) | Same as above with `dropped_reason: "empty_transcript"`. |
|
||||
| Shortcut conflict with dictation | Registration returns `ConflictsWithDictation`. Settings panel shows the inline error. |
|
||||
| Wayland session | `UnsupportedOnWayland`. Settings panel surfaces a clear message. Logged once per session. |
|
||||
|
||||
**Logging** (per the debug-logging rule): all logs use `[ptt]` prefix. Fields per session: `session_id`, `shortcut`, `held_ms`, `transcript_len`, `dropped_reason`, `finalized_by_watchdog`. PII-safe — never log transcript text or audio buffers, only lengths/durations. Audio buffers are zeroed after finalize.
|
||||
|
||||
**Telemetry**: one new analytics event `ptt_session` mirroring the log fields (no transcript), gated by the existing analytics opt-in.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
- **No `Config` TOML schema change.** All PTT settings live in the renderer's `ptt` redux slice (persisted), mirroring how dictation is configured today.
|
||||
- **Default `shortcut: null`** (unbound). No hard-coded default key — every possible default conflicts with something common.
|
||||
- **Default `speakReplies: true`**, **`showOverlay: true`**.
|
||||
- **Boot path:** on rehydration, if `state.ptt.shortcut` is non-null, call `register_ptt_hotkey`. On settings change, unregister-then-register. Independent of the existing dictation init.
|
||||
|
||||
---
|
||||
|
||||
## Migration
|
||||
|
||||
Brand-new state. No migration. Existing users on `0.53.45+` see the new `/settings/voice` PTT section after upgrade with everything default-off until they bind a key.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
| Layer | What | Where |
|
||||
| --- | --- | --- |
|
||||
| Rust unit | `expand_ptt_shortcuts`: empty, modifier-only, valid combos, `CmdOrCtrl` expansion (dual-variant on macOS, single on Win/Linux) | `app/src-tauri/src/ptt_hotkeys.rs` inline `#[cfg(test)]` |
|
||||
| Rust unit | `speak_reply` / `source` / `session_id` round-trip through `channel.web_chat` schema serde; default behavior unchanged when all omitted | `src/openhuman/channels/providers/web_tests.rs` |
|
||||
| Rust unit | `DomainEvent::Voice::PttTranscriptCommitted` publishes; test subscriber receives it | `src/openhuman/voice/bus.rs` inline tests |
|
||||
| Rust E2E | `tests/json_rpc_e2e.rs` — call `channel.web_chat` with `speak_reply: true` and assert `reply_speech::synthesize_and_play` is invoked via a test seam at the progress-bridge's `TurnCompleted` boundary | `tests/json_rpc_e2e.rs` extension |
|
||||
| Vitest unit | `pttService` state machine: start→stop happy path, watchdog timeout, empty-audio drop, empty-transcript drop, dictation-preempt, double-press idempotency, mic-permission-denied path | `app/src/services/pttService.test.ts` (new) |
|
||||
| Vitest unit | `ptt` redux slice: shortcut set/clear, toggle settings, rehydration | `app/src/store/slices/ptt.test.ts` (new) |
|
||||
| Vitest unit | `PttSettingsPanel` — render, hotkey capture, conflict-with-dictation error, mic-denied banner, Wayland banner | `app/src/pages/settings/voice/PttSettingsPanel.test.tsx` (new) |
|
||||
| Vitest unit | `PttOverlayPage` — renders idle vs active states, listens for active event | `app/src/pages/PttOverlayPage.test.tsx` (new) |
|
||||
| i18n gate | All new keys present in all 13 locales, no untranslated English values | `pnpm i18n:check` + `pnpm i18n:english:check` (existing CI) |
|
||||
| WDIO E2E | Desktop spec: register a hotkey via settings UI, simulate the hotkey via `tauri-driver` key injection, assert overlay window appears, assert chat thread receives a message. STT mocked via the shared mock backend returning a fixed transcript. | `app/test/e2e/specs/ptt-flow.spec.ts` (new) |
|
||||
| Manual smoke | Hold-while-game-in-foreground on macOS + Windows; mic permission denied flow; Wayland fallback message | PR body checklist |
|
||||
|
||||
**Coverage gate.** Every changed line in the new files + the `channel.web_chat` schema delta ships with ≥ 80% diff coverage per the existing merge gate. Untested escape valves (the real `Audio.play()` call, the real `tauri-driver` key injection) are isolated behind thin wrappers that can be mocked.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (named explicitly)
|
||||
|
||||
- **Background screen capture for the agent** — separate follow-up PR off the same issue.
|
||||
- **PTT-while-dictation-mid-flight** polish beyond "preempt with reason."
|
||||
- **DXGI exclusive-fullscreen overlay rendering** — documented caveat only.
|
||||
- **Streaming partial transcripts during hold.**
|
||||
- **Per-thread PTT routing** (v1 always uses active thread; if none, creates a `"Voice"` thread).
|
||||
- **Native platform overlays** (NSWindow / Win32 layered / X11 override-redirect) — Tauri overlay window covers v1 needs.
|
||||
- **PTT toggle-mode** — out; dictation toggle covers that pattern already.
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
None at spec time. If implementation surfaces blockers (e.g. `tauri-plugin-global-shortcut` `Released` semantics regress on a specific OS version), revisit with a small spec amendment rather than a silent design drift.
|
||||
@@ -1,122 +0,0 @@
|
||||
# Memory Tree — per-integration health strip (issue #2763)
|
||||
|
||||
**Status:** approved, ready for implementation plan
|
||||
**Issue:** [tinyhumansai/openhuman#2763](https://github.com/tinyhumansai/openhuman/issues/2763)
|
||||
**Parent umbrella:** #1856 Part 3
|
||||
**Depends on (already merged):** #2719 (status panel), #1250 (per-source sync status)
|
||||
|
||||
## Goal
|
||||
|
||||
Give operators a quick at-a-glance per-integration health view, directly under the four-tile Memory Tree status panel, so a stalled / low-volume tree can be traced to a single integration (e.g. Gmail) rather than the pipeline as a whole.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing or shrinking `MemorySourcesRegistry` (the full sources list stays below as-is — it owns add / sync / remove flows).
|
||||
- Per-provider `Error` attribution. This needs new core work (job → source linkage) and is **deferred** to a follow-up issue tracked in the PR body.
|
||||
- Team-scoped wiki silos (Part 2 of #1856 — blocked on FR9, not in scope here).
|
||||
|
||||
## Architecture
|
||||
|
||||
Frontend-only diff. Reuses the existing `openhuman.memory_sync_status_list` RPC (shipped by #1250) as the data source. The 575-line `MemorySourcesRegistry` remains as the canonical configurable-sources view; this strip is a smaller, health-focused readout colocated with the pipeline-status tiles.
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MemoryTreeStatusPanel │
|
||||
│ ┌─Status─┬─LastSync─┬─Chunks─┬─Wiki──┐ │
|
||||
│ │ … │ … │ … │ … │ │
|
||||
│ └────────┴──────────┴────────┴───────┘ │
|
||||
│ │
|
||||
│ ┌── Per-integration health ──────────────────────────────────┐ │
|
||||
│ │ [icon] slack 5,231 chunks · 3 min ago ● Active │ │
|
||||
│ │ [icon] gmail 842 chunks · 2 hr ago ● Stale │ │
|
||||
│ │ [icon] notion 45 chunks · 5 d ago ● Stale │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Auto-sync toggle ──────────────────────────────[switch]──┐ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MemorySourcesRegistry (existing, unchanged) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Data flow
|
||||
|
||||
1. `useMemoryTreeStatus` (existing hook in `MemoryTreeStatusPanel.tsx`) extends `fetchOnce` to call both:
|
||||
- `memoryTreePipelineStatus()` (existing)
|
||||
- `memorySyncStatusList()` (existing — wraps `openhuman.memory_sync_status_list`)
|
||||
in parallel via `Promise.all`. Returned object gains an `integrations: MemorySyncStatus[]` field.
|
||||
2. Adaptive polling (1.5s while syncing, 4s otherwise) — the existing cadence — drives both fetches. One timer, one re-render.
|
||||
3. A new internal sub-component `<IntegrationHealthStrip>` (same file, not exported) consumes `integrations` and renders the list. Lives inside `MemoryTreeStatusPanel` between the tile grid and the auto-sync toggle row.
|
||||
|
||||
## Status mapping (pure TS, no core change)
|
||||
|
||||
```ts
|
||||
type IntegrationHealth = 'active' | 'stale';
|
||||
|
||||
function classifyIntegration(s: MemorySyncStatus): IntegrationHealth {
|
||||
return s.freshness === 'active' ? 'active' : 'stale';
|
||||
}
|
||||
```
|
||||
|
||||
- `freshness === 'active'` → **Active** (chunk within last 30 s)
|
||||
- `freshness === 'recent' | 'idle'` → **Stale**
|
||||
- **Error** state intentionally omitted; see "Deferred" below.
|
||||
|
||||
## UI details
|
||||
|
||||
- One row per `MemorySyncStatus`. Icon from a small built-in `PROVIDER_ICONS` map inside `MemoryTreeStatusPanel.tsx` (keyed by sync-provider name — `slack` / `gmail` / `notion` / …, distinct from `SOURCE_KIND_ICONS` which keys by `SourceKind`); fallback to a generic `🔌` glyph for unknown providers.
|
||||
- Provider name: friendly label via `SOURCE_KIND_LABEL_KEYS[provider]` when present, else the raw `provider` string.
|
||||
- "5,231 chunks · 3 min ago" — chunk count + relative time, reusing the existing `formatRelativeMs()` helper from `MemoryTreeStatusPanel.tsx`.
|
||||
- Status pill: dot color reuses `statusDotClass` semantics — sage-400 for `active`, stone-400 for `stale`.
|
||||
- Empty state: `data-testid="memory-tree-integrations-empty"`, single line "No integrations connected" matching `MemorySources.tsx` convention.
|
||||
- Scroll: `max-h-48 overflow-y-auto` once past ~5 rows so the strip never dominates the panel.
|
||||
|
||||
## i18n
|
||||
|
||||
New keys colocated with `memoryTree.status.*` in `app/src/lib/i18n/en.ts`:
|
||||
|
||||
| key | English |
|
||||
| --- | --- |
|
||||
| `memoryTree.status.integrationsTitle` | Per-integration health |
|
||||
| `memoryTree.status.integrationsEmpty` | No integrations connected |
|
||||
| `memoryTree.status.integrationActive` | Active |
|
||||
| `memoryTree.status.integrationStale` | Stale |
|
||||
| `memoryTree.status.integrationChunks` | {count} chunks |
|
||||
|
||||
All 13 non-English locales (`ar`, `bn`, `de`, `es`, `fr`, `hi`, `id`, `it`, `ko`, `pl`, `pt`, `ru`, `zh-CN`) get **real translations** in the same PR, per CLAUDE.md i18n rule. `pnpm i18n:check` and `pnpm i18n:english:check` must pass.
|
||||
|
||||
## Testing
|
||||
|
||||
**Vitest** (`MemoryTreeStatusPanel.test.tsx`):
|
||||
|
||||
1. Renders integration rows from `memory_sync_status_list` (mocked).
|
||||
2. Renders empty state when list is empty.
|
||||
3. Status mapping: a row with `freshness='active'` shows `Active`; `freshness='recent'` and `freshness='idle'` both show `Stale`.
|
||||
4. Icon fallback for unknown provider doesn't throw.
|
||||
5. Relative-time label uses `formatRelativeMs` (frozen clock).
|
||||
6. Shared poll: both `memoryTreePipelineStatus` and `memorySyncStatusList` are called on the same tick (mock both, advance fake timers, assert call counts).
|
||||
|
||||
**Coverage:** changed-lines ≥ 80 % (CI gate). The mapping + render branches are small and trivially testable; achievable.
|
||||
|
||||
**No new Rust tests** — no Rust changed. Existing `memory_sync_status_list` test coverage continues to validate the wire shape.
|
||||
|
||||
**No new E2E spec** — covered by the existing intelligence smoke test plus the unit tests above. (E2E for a render-only sub-component is overkill.)
|
||||
|
||||
## Deviations from issue acceptance criteria
|
||||
|
||||
Will be noted explicitly in PR body so reviewers see them up front:
|
||||
|
||||
1. **AC #1** says `memory_tree_pipeline_status` returns an `integrations` array. We **don't** extend that RPC; we consume `memory_sync_status_list` instead. Same data, cleaner contract, no schema bump.
|
||||
2. **AC #2** says status is `Active / Stale / Error`. We ship **Active / Stale** only. Per-provider `Error` requires new core work (`mem_tree_jobs` has no `source_kind` / `source_id` column today; we'd have to parse `payload_json` per row or add a column). Deferred to a follow-up issue filed alongside this PR.
|
||||
|
||||
Remaining ACs (list renders below status panel, empty state, polling shares parent, i18n parity, ≥80 % coverage) are met as specified.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Visual crowding** if many providers are connected. Mitigated by `max-h-48 overflow-y-auto`.
|
||||
- **Empty `memory_sync_status_list` when chunks haven't flowed yet** — the strip will render empty even for a freshly-installed integration. Acceptable for v1 (the issue's same gap); when per-provider error tracking lands, "configured but never produced chunks" can be its own state.
|
||||
|
||||
## Out of scope (filed as follow-up)
|
||||
|
||||
- Per-provider `Error` state. Open follow-up issue: "Per-provider error attribution for Memory Tree" — proposes either parsing `payload_json` for failed jobs to extract `source_id`, or adding a typed `source_kind` column to `mem_tree_jobs` (probably the latter, with a one-shot migration).
|
||||
@@ -1,53 +0,0 @@
|
||||
# Telegram (and all channels) honor `chat_provider` workload routing
|
||||
|
||||
**Issue:** [#3098](https://github.com/tinyhumansai/openhuman/issues/3098), sub-issue 1.
|
||||
**Scope:** Sub-issue 1 only. Sub-issues 2 (Telegram file commits — by-design question), 3 (skills not running), and 4 (Brave Search) are out of scope.
|
||||
|
||||
## Problem
|
||||
|
||||
A user picks Ollama in **Settings → AI** (which writes `chat_provider = "ollama:<model>"` to config), but the Telegram bot still routes through the managed OpenHuman cloud backend and the user's `config.default_model`. The Ollama selection is silently ignored by every channel (Telegram, Discord, Slack, iMessage, Mattermost).
|
||||
|
||||
## Root cause
|
||||
|
||||
`src/openhuman/channels/runtime/startup.rs:171-183, 267-270, 682-695` builds the channels runtime provider via `create_intelligent_routing_provider(…)` — an exclusively cloud-backed chain (`OpenHumanBackendProvider` wrapped in `ReliableProvider` and the legacy hint-based `IntelligentRoutingProvider`). It pairs that provider with `ctx.model = config.default_model.unwrap_or(DEFAULT_MODEL)`.
|
||||
|
||||
It never consults `provider_for_role("chat", &config)` or `Config::workload_local_model("chat")` — the documented single source of truth for "is this workload local?" (`src/openhuman/config/schema/types.rs:511-544`). The unified workload factory `inference::provider::create_chat_provider` (`src/openhuman/inference/provider/factory.rs:206-217`) already knows how to build the right `(provider, model_id)` for any chat-workload string (`"ollama:…"`, `"lmstudio:…"`, `"<byok-slug>:…"`, `"openhuman"`, `"cloud"`). The channel runtime simply doesn't use it.
|
||||
|
||||
## Fix
|
||||
|
||||
In `runtime/startup.rs`, branch on `provider_for_role("chat", &config)` once during runtime setup:
|
||||
|
||||
- **Workload override path** (`chat_provider` is `"ollama:…"`, `"lmstudio:…"`, `"<byok-slug>:…"`, or `"claude_agent_sdk[:…]"`): build the provider via `inference::provider::create_chat_provider("chat", &config)`. Use the returned `model_id` as `ctx.model`. The cache key (`ctx.default_provider`) becomes the slug portion of the provider string (e.g. `"ollama"`).
|
||||
- **Default path** (unset, `"cloud"`, or `"openhuman"`): keep the existing `create_intelligent_routing_provider` chain verbatim. `ctx.model` continues to come from `config.default_model`. Zero behavior change for cloud users.
|
||||
|
||||
The branch happens once during channel-runtime startup. All channels share `ChannelRuntimeContext`, so the fix uniformly covers Telegram, Discord, Slack, iMessage, Mattermost (and Matrix when feature-gated on).
|
||||
|
||||
## Behavior matrix
|
||||
|
||||
| `chat_provider` | Today | After fix |
|
||||
|---|---|---|
|
||||
| unset / `"cloud"` / `"openhuman"` | Cloud + `default_model` | Cloud + `default_model` (unchanged) |
|
||||
| `"ollama:llama3.2"` | Cloud + `default_model` (bug) | Ollama + `llama3.2` |
|
||||
| `"openai:gpt-4o"` (BYOK) | Cloud + `default_model` (bug) | OpenAI + `gpt-4o` |
|
||||
|
||||
## Tests
|
||||
|
||||
Unit-level coverage in the existing channels runtime test surface:
|
||||
|
||||
1. `chat_provider` unset → `ctx.model == config.default_model` (regression guard for cloud users).
|
||||
2. `chat_provider = "ollama:llama3.2"` → `ctx.model == "llama3.2"` and `ctx.default_provider == "ollama"`.
|
||||
3. `chat_provider = "cloud"` → identical to (1).
|
||||
|
||||
Tests use the existing `test_support.rs` harness and `Config` builders. No new mocks required; the workload factory is already exercised by `factory_tests.rs`.
|
||||
|
||||
## Non-goals (deliberate)
|
||||
|
||||
- **`/model <id>` per-conversation override** — `routes.rs:169-211`'s `get_or_create_provider` ignores the provider name and always rebuilds a cloud provider. This is a pre-existing limitation unrelated to this fix; addressing it would expand scope significantly. Once the default is correct, the `/model` command becomes a model-name-only override against whichever provider the channel runtime was constructed with, which is a reasonable interim state.
|
||||
- Sub-issues 2, 3, 4 of #3098 — separate root causes, will each get their own PR if/when triaged.
|
||||
- Any change to `local_ai.usage.*` (the deprecated legacy hint-based routing) — explicitly out of scope per the comment in `config/schema/types.rs:520-523`.
|
||||
|
||||
## Blast radius
|
||||
|
||||
- One file edited: `src/openhuman/channels/runtime/startup.rs` (~15 lines changed).
|
||||
- Cloud-only users: zero behavior change (the default branch is the existing code path).
|
||||
- Local-model users: gain a working Telegram/Discord/Slack/etc. experience with their selected Ollama (or BYOK) provider.
|
||||
@@ -1,349 +0,0 @@
|
||||
# AgentBox Marketplace Integration
|
||||
|
||||
**Issue:** [tinyhumansai/openhuman#3620](https://github.com/tinyhumansai/openhuman/issues/3620)
|
||||
**Status:** Draft — design approved, plan pending
|
||||
**Date:** 2026-06-12
|
||||
|
||||
## Background
|
||||
|
||||
OpenHuman has been onboarded to the **AgentBox POC** on GMI Cloud — an agent marketplace where containerized agents are registered, deployed, and distributed. The platform invokes containers over HTTP with a polling-based long-running contract and injects an OpenAI-compatible LLM (GMI MaaS) at runtime.
|
||||
|
||||
This design covers the code-side work to make `openhuman-core` a valid AgentBox container. Operational tasks (console registration, image push, deployment, API-key handling in a secrets manager, end-to-end marketplace validation) are out of scope of this document and remain the operator's responsibility.
|
||||
|
||||
## AgentBox contract (as documented)
|
||||
|
||||
Verified from `https://docs.gmicloud.ai/agentbox-marketplace/`:
|
||||
|
||||
- `POST /run`
|
||||
- Request: `{ "payload": <agent-defined> }`
|
||||
- Response: `202 { "job_id": "<uuid>" }`
|
||||
- `GET /jobs/{job_id}`
|
||||
- Response: `200 { "status": "pending" | "running" | "completed" | "failed", "result": {...}, "error": "..." }`
|
||||
- `404` for unknown ids.
|
||||
- Pattern is **polling-only**. No SSE / WebSockets / chunked streaming.
|
||||
- Platform-injected env vars at runtime:
|
||||
- `GMI_MAAS_BASE_URL` — OpenAI-compatible base URL.
|
||||
- `GMI_MAAS_API_KEY` — MaaS API key (must remain unset in image).
|
||||
- `GMI_MODELS` — model id to call.
|
||||
- Container image source: any public/private Docker registry.
|
||||
- Listening port, healthcheck endpoint, and `payload` shape are agent-defined.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Stand up `POST /run` and `GET /jobs/{job_id}` in `openhuman-core`, conforming to the AgentBox contract.
|
||||
2. Drive each `/run` invocation through the **full agent runtime** (skills, tools, memory) — the same path chat uses.
|
||||
3. Plumb `GMI_MAAS_*` env vars into the inference provider catalog so the agent actually uses GMI MaaS at runtime.
|
||||
4. Keep the change zero-impact on the desktop build (routes off by default).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Persistent job store. Jobs are kept in-memory; lost on container restart.
|
||||
- Streaming responses. AgentBox's contract is polling-only.
|
||||
- Marketplace registration / image push / API-key storage / e2e marketplace validation — operator tasks.
|
||||
- A separate Dockerfile. The existing one is reused with new env defaults.
|
||||
- Backwards-compatibility shims — `OPENHUMAN_AGENTBOX_MODE` is new; no migration needed.
|
||||
|
||||
## Architecture
|
||||
|
||||
A new domain `src/openhuman/agentbox/` with the canonical module shape:
|
||||
|
||||
| File | Role |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `mod.rs` | Export-only: `pub mod` + `pub use`. No business logic. |
|
||||
| `types.rs` | `RunRequest`, `RunResponse`, `JobStatus`, `JobRecord`, `RunPayload`, `RunResult`. |
|
||||
| `store.rs` | `JobStore` wrapping `DashMap<JobId, JobRecord>` behind `Arc`. Insert / get / update / sweep evicted terminal jobs.|
|
||||
| `ops.rs` | `submit_run`, `get_job`, `run_job` worker, sweep task. |
|
||||
| `http.rs` | Axum sub-router with `POST /run` and `GET /jobs/{job_id}`. Handler functions delegate to `ops.rs`. |
|
||||
| `env.rs` | `register_gmi_provider_if_present()` — reads `GMI_MAAS_*` at startup and wires the OpenAI-compatible provider. |
|
||||
| `http_tests.rs`| Axum `TestServer` integration tests. |
|
||||
|
||||
The router is mounted in `src/core/jsonrpc.rs::build_core_http_router` only when `OPENHUMAN_AGENTBOX_MODE=1`. Default is off, so desktop builds are unaffected.
|
||||
|
||||
### Why a new domain
|
||||
|
||||
Per `AGENTS.md`: "New functionality → dedicated subdirectory." AgentBox is a transport surface specific to GMI Cloud's marketplace contract. It is not a generic concern of the core HTTP server, so it does not belong in `src/core/`. It is not a domain-of-substance like `agent` or `threads`; it is an adapter layer that delegates inward.
|
||||
|
||||
### Mounting point
|
||||
|
||||
In `build_core_http_router`:
|
||||
|
||||
```rust
|
||||
let router = Router::new()
|
||||
.route("/", get(root_handler))
|
||||
.route("/health", get(health_handler))
|
||||
// ... existing routes ...
|
||||
.nest("/v1", crate::openhuman::inference::http::router());
|
||||
|
||||
let router = if std::env::var("OPENHUMAN_AGENTBOX_MODE").as_deref() == Ok("1") {
|
||||
router.merge(crate::openhuman::agentbox::http::router(job_store.clone()))
|
||||
} else {
|
||||
router
|
||||
};
|
||||
```
|
||||
|
||||
The `JobStore` is created once at startup in `run_server_embedded` and shared with both the AgentBox router and the sweep task.
|
||||
|
||||
## HTTP contract
|
||||
|
||||
### `POST /run`
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{ "payload": { "message": "<string>", "thread_id": "<string?>" } }
|
||||
```
|
||||
|
||||
`thread_id` is optional. When omitted, a new thread is created for the job. When supplied, the job runs in the existing thread (allowing multi-turn flows if the marketplace consumer threads state).
|
||||
|
||||
Successful response — `202 Accepted`:
|
||||
```json
|
||||
{ "job_id": "<uuid>" }
|
||||
```
|
||||
|
||||
Error responses:
|
||||
- `400 Bad Request` — `payload` missing, `message` missing or empty, or body not valid JSON. Body: `{ "error": "<reason>" }`.
|
||||
|
||||
### `GET /jobs/{job_id}`
|
||||
|
||||
Successful response — `200 OK`:
|
||||
```json
|
||||
{
|
||||
"status": "pending" | "running" | "completed" | "failed",
|
||||
"result": { "message": "<assistant reply>", "thread_id": "<string>" },
|
||||
"error": "<string>"
|
||||
}
|
||||
```
|
||||
|
||||
`result` is present only when `status == "completed"`. `error` is present only when `status == "failed"`. Field order matches AgentBox's documented response shape.
|
||||
|
||||
Error responses:
|
||||
- `404 Not Found` — unknown or evicted job id. Body: `{ "error": "job not found" }`.
|
||||
|
||||
### Authentication
|
||||
|
||||
Both routes are **unauthenticated at the container boundary** — AgentBox's edge handles auth before reaching us. They are added to the public path list in `src/core/auth.rs` so `rpc_auth_middleware` skips them.
|
||||
|
||||
The existing `/rpc` route stays bearer-protected. In AgentBox deployments the container binds to `0.0.0.0`, so the bearer-protected routes are reachable on the network — that is acceptable because the bearer is generated per-launch and never leaves the container's runtime memory (no env-var exposure). For added defense, AgentBox mode logs a warning at startup reminding the operator that only `/run`, `/jobs/*`, and `/health` are intended to be public.
|
||||
|
||||
### Healthcheck
|
||||
|
||||
Reuse existing `GET /health`. No new contract.
|
||||
|
||||
### Routes summary
|
||||
|
||||
| Path | Method | Auth | Added by |
|
||||
| ---------------- | ------ | -------- | ------------- |
|
||||
| `/health` | GET | none | existing |
|
||||
| `/run` | POST | none | this design |
|
||||
| `/jobs/{job_id}` | GET | none | this design |
|
||||
| `/rpc` | POST | bearer | existing |
|
||||
| `/v1/*` | * | (existing) | existing |
|
||||
|
||||
## Job execution
|
||||
|
||||
### Worker flow
|
||||
|
||||
`run_job(store, job_id, RunPayload { message, thread_id })`:
|
||||
|
||||
1. Update job status `pending → running` in the store.
|
||||
2. Resolve thread: if `thread_id` supplied, fetch via `threads::ops`; otherwise create a new thread via existing thread-creation ops. Capture the thread id for the result.
|
||||
3. Invoke the agent dispatcher with the user message. This goes through the **full agent runtime** — same entrypoint as chat: skills, tools, memory, prompt injection guard, the lot. Reuses the existing dispatcher's public submission API.
|
||||
4. Await dispatcher completion. Capture the final assistant turn's text content.
|
||||
5. Write `JobRecord { status: Completed, result: Some(RunResult { message: assistant_text, thread_id }), error: None }` to the store.
|
||||
6. On any error (dispatcher error, thread-resolve error, timeout): write `JobRecord { status: Failed, result: None, error: Some(err_string) }`.
|
||||
|
||||
### Cancellation & timeout
|
||||
|
||||
- Hard cap configurable via `OPENHUMAN_AGENTBOX_JOB_TIMEOUT_SECS`. Default: `600` (10 minutes).
|
||||
- Wrapped in `tokio::time::timeout`. On expiry, the future is dropped; the agent dispatcher's existing tokio cancellation semantics handle cleanup.
|
||||
- On timeout: `status = "failed"`, `error = "job timeout after <N>s"`.
|
||||
|
||||
### Job retention
|
||||
|
||||
- Terminal jobs (`completed` / `failed`) are retained for **1 hour** after completion, then evicted by a background sweep.
|
||||
- A `tokio::spawn` background task wakes every 60 seconds and removes jobs whose `terminal_at` is older than the retention window.
|
||||
- This bounds memory under sustained traffic without forcing persistence in v1.
|
||||
- If a polling client misses the retention window, they get `404 job not found` — acceptable per AgentBox's contract (no docs claim indefinite retention).
|
||||
|
||||
### Concurrency
|
||||
|
||||
- The `JobStore` is `DashMap`-backed and safe for concurrent insert/get/update.
|
||||
- `submit_run` always spawns the worker via `tokio::spawn` and returns immediately. There is no semaphore in v1 — the underlying agent runtime already handles concurrency at its level.
|
||||
- Future: if concurrent job pressure becomes a problem, add a configurable semaphore around `tokio::spawn`. Out of scope here.
|
||||
|
||||
## GMI MaaS provider bridge
|
||||
|
||||
### Startup
|
||||
|
||||
`env.rs::register_gmi_provider_if_present()` runs once during `run_server_embedded` initialization, before the agent runtime accepts work.
|
||||
|
||||
Reads:
|
||||
- `GMI_MAAS_BASE_URL` — required.
|
||||
- `GMI_MAAS_API_KEY` — required.
|
||||
- `GMI_MODELS` — required, single model id (AgentBox docs example: `deepseek-ai/DeepSeek-V4-Pro`).
|
||||
|
||||
Behavior:
|
||||
- All three present → register an OpenAI-compatible cloud provider named `"gmi-maas"` into the inference provider catalog using the existing `compatible_provider_impl`. The model from `GMI_MODELS` becomes its default. Mark `"gmi-maas"` as the active provider for agent runtime invocations.
|
||||
- Any missing → log a single `warn!` line listing which vars are missing, skip registration, continue startup. The core still runs (useful for local testing of the `/run` route with a stubbed inference layer), but agent calls that need an LLM will fail until a provider is configured by other means.
|
||||
|
||||
### Logging rules
|
||||
|
||||
Per repo logging conventions: log the base URL and model id at `info!` level when registered; never log the API key (not even truncated). Use the stable `[agentbox]` prefix for AgentBox-side logs and `[agentbox::gmi]` for the provider bridge.
|
||||
|
||||
## Data shapes
|
||||
|
||||
```rust
|
||||
// types.rs
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RunRequest {
|
||||
pub payload: RunPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RunPayload {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub thread_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RunResponse {
|
||||
pub job_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum JobStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RunResult {
|
||||
pub message: String,
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JobView {
|
||||
pub status: JobStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<RunResult>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JobRecord {
|
||||
pub status: JobStatus,
|
||||
pub result: Option<RunResult>,
|
||||
pub error: Option<String>,
|
||||
pub created_at: std::time::Instant,
|
||||
pub terminal_at: Option<std::time::Instant>,
|
||||
}
|
||||
```
|
||||
|
||||
`JobView` is the serialization-only projection returned by `GET /jobs/{job_id}`. `JobRecord` is the internal state.
|
||||
|
||||
## Configuration surface
|
||||
|
||||
New env vars:
|
||||
|
||||
| Var | Default | Role |
|
||||
| ------------------------------------- | ------------ | ------------------------------------------------- |
|
||||
| `OPENHUMAN_AGENTBOX_MODE` | `0` | `1` enables `/run` + `/jobs/*` routes. |
|
||||
| `OPENHUMAN_AGENTBOX_JOB_TIMEOUT_SECS` | `600` | Hard cap per job invocation. |
|
||||
| `GMI_MAAS_BASE_URL` | unset | OpenAI-compatible base URL (AgentBox-injected). |
|
||||
| `GMI_MAAS_API_KEY` | unset | MaaS API key (AgentBox-injected at runtime). |
|
||||
| `GMI_MODELS` | unset | Model id (AgentBox-injected). |
|
||||
|
||||
All five are documented in `.env.example` with comments tying them back to the AgentBox console wizard.
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit tests (inline `#[cfg(test)] mod tests`)
|
||||
|
||||
- `store.rs`
|
||||
- `insert` / `get` round-trip.
|
||||
- `mark_completed` and `mark_failed` update status and `terminal_at`.
|
||||
- `sweep` evicts terminal jobs older than the retention window; leaves running and recent terminal jobs untouched.
|
||||
- `ops.rs`
|
||||
- `submit_run` returns a valid v4 uuid and a job in `Pending` status.
|
||||
- `run_job` happy path with a mocked dispatcher → `Completed` with the assistant message captured.
|
||||
- `run_job` dispatcher-error path → `Failed` with the error string captured.
|
||||
- `run_job` timeout path → `Failed` with `"job timeout..."`.
|
||||
- `env.rs`
|
||||
- All three vars present → provider registered, `info!` logged with base URL and model (no key).
|
||||
- Any one missing → no provider registered, `warn!` logged listing missing vars.
|
||||
- `GMI_MAAS_API_KEY` value never appears in captured logs.
|
||||
|
||||
### HTTP integration tests (`http_tests.rs`)
|
||||
|
||||
Uses Axum's `TestServer`:
|
||||
|
||||
- `POST /run` with valid body → `202` and a uuid in `job_id`.
|
||||
- `POST /run` with missing `message` → `400` and `{ "error": ... }` body.
|
||||
- `POST /run` with malformed JSON → `400`.
|
||||
- `GET /jobs/{job_id}` for unknown id → `404` and `{ "error": "job not found" }`.
|
||||
- End-to-end with a fast mocked dispatcher: submit `/run`, poll `/jobs/{job_id}` until `completed`, assert result shape.
|
||||
- Routes are not registered when `OPENHUMAN_AGENTBOX_MODE` is unset → `POST /run` returns `404`.
|
||||
|
||||
### Rust integration test in `tests/`
|
||||
|
||||
A `tests/agentbox_e2e.rs` against the real core binary started by `scripts/test-rust-with-mock.sh`:
|
||||
|
||||
- Boot core with `OPENHUMAN_AGENTBOX_MODE=1` and stubbed `GMI_MAAS_*`.
|
||||
- Submit a `/run` with a canned message that the mock provider answers deterministically.
|
||||
- Poll until `completed`, assert the assistant reply matches.
|
||||
|
||||
### Out of scope for tests
|
||||
|
||||
- E2E against the real AgentBox marketplace. That is the operator's manual acceptance step from the issue.
|
||||
|
||||
## Docker
|
||||
|
||||
The existing `Dockerfile` is unchanged structurally. Two additions:
|
||||
|
||||
1. Default `ENV OPENHUMAN_AGENTBOX_MODE=0` so desktop bundles built from the same Dockerfile do not flip on the public routes.
|
||||
2. `EXPOSE 7788` stays — AgentBox does not require a specific port; the value is passed through during console registration.
|
||||
|
||||
Operator workflow at deploy time (set in the AgentBox console wizard's "Env Variables" step):
|
||||
- `OPENHUMAN_AGENTBOX_MODE=1`
|
||||
- `GMI_MAAS_BASE_URL` / `GMI_MAAS_API_KEY` / `GMI_MODELS` are injected automatically by AgentBox.
|
||||
- Optional: `OPENHUMAN_AGENTBOX_JOB_TIMEOUT_SECS` override.
|
||||
|
||||
No image rebuild needed to switch modes.
|
||||
|
||||
## Documentation
|
||||
|
||||
Add `gitbooks/developing/agentbox-deployment.md` — a short runbook covering:
|
||||
|
||||
1. The 4-step AgentBox console wizard, mapped to our env vars.
|
||||
2. The image tag/registry conventions to use for releases destined for the marketplace.
|
||||
3. The polling-only contract recap (`/run` + `/jobs/*`) for anyone debugging from the AgentBox console's test panel.
|
||||
4. Timeout & retention defaults and how to tune them.
|
||||
|
||||
No CLAUDE.md / AGENTS.md change — AgentBox is a transport surface, not a core convention.
|
||||
|
||||
## Risks & open questions
|
||||
|
||||
- **Job-store memory growth under burst traffic.** Mitigation: 1-hour retention + 60-second sweep. If marketplace traffic shape is unknown at deploy time, an operator can shorten retention via a future `OPENHUMAN_AGENTBOX_JOB_RETENTION_SECS` env var. Out of scope for v1.
|
||||
- **`thread_id` semantics across calls.** v1 trusts the caller's id verbatim. If a marketplace user supplies an id that does not resolve in this deployment's workspace, the `/run` handler still queues the job (the sync handler does not block on thread lookup), and the worker writes `status: "failed"` with `error: "thread not found"`. No cross-tenant leakage because each deployment owns its workspace.
|
||||
- **Healthcheck contract.** AgentBox docs do not specify one. We expose `/health` and rely on AgentBox console configuring it. If marketplace validation requires a different path, that's a small follow-up.
|
||||
- **No persistence on restart.** First deployment takes 10–25 minutes per the issue, so restarts are rare — acceptable trade-off. A later PR can swap the in-memory store for a SQLite-backed one without touching the HTTP layer.
|
||||
- **Approval gate interaction.** The existing approval gate parks interactive chat turns; AgentBox jobs are "background/cron" by nature and should pass through. The worker submission tags the request as background origin so the gate does not park it. To confirm during implementation.
|
||||
|
||||
## Acceptance criteria for this PR (code side only)
|
||||
|
||||
- `cargo check` and `pnpm rust:check` pass.
|
||||
- New `src/openhuman/agentbox/` domain compiles and is wired into `build_core_http_router` behind `OPENHUMAN_AGENTBOX_MODE=1`.
|
||||
- `POST /run` and `GET /jobs/{job_id}` match the contract in this doc (verified by `http_tests.rs`).
|
||||
- `GMI_MAAS_*` env vars register the OpenAI-compatible provider at startup.
|
||||
- All new code paths logged with `[agentbox]` / `[agentbox::gmi]` prefixes; secrets never logged.
|
||||
- Unit + integration test suites pass with `pnpm test:rust`.
|
||||
- `.env.example` updated.
|
||||
- `gitbooks/developing/agentbox-deployment.md` added.
|
||||
- Coverage gate (≥80% on changed lines) satisfied.
|
||||
|
||||
Operational acceptance criteria from the issue (marketplace listing visible, container deploys, agent responds, API key stored securely) are explicitly **out of scope of this PR** and remain the operator's checklist.
|
||||
@@ -1,94 +0,0 @@
|
||||
# 00 — Baseline: crate, features, native links
|
||||
|
||||
Current status (2026-07-03): baseline dependency alignment is complete in both
|
||||
Cargo worlds. `tinyagents 1.5.0` is resolved with the `sqlite` feature,
|
||||
OpenHuman pins `rusqlite = "=0.40.0"`, both worlds patch through
|
||||
`vendor/rusqlite-0.40.0` and `vendor/libsqlite3-sys-0.38.0`, and the SDK-gaps
|
||||
inventory has been refreshed against the published 1.3.0 crate source.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Bump `tinyagents` to `"1.5.0"`** (done in both Cargo worlds — root and
|
||||
`app/src-tauri/`). Known 1.1→1.2 break already handled
|
||||
(`MessageDelta::text` ctor). Note: the `openai` crate feature was removed
|
||||
after 1.2.0 (1.2.1+ features are only `sqlite`/`repl`) — we never enabled
|
||||
it, so no impact. See "1.3.0 delta" below for new API this plan uses.
|
||||
2. **Align rusqlite to 0.40** in both worlds (`Cargo.toml` root and
|
||||
`app/src-tauri/Cargo.toml`). OpenHuman pins `rusqlite = "=0.40.0"` and
|
||||
enables `tinyagents = { version = "1.5.0", features = ["sqlite"] }`.
|
||||
Compatibility notes:
|
||||
- `rusqlite 0.40` and `libsqlite3-sys 0.38` are consumed directly from
|
||||
crates.io. Their build scripts use the `cfg_select!` macro (stable from
|
||||
Rust 1.96 — `rust-toolchain.toml` pins `1.96.1`), so the earlier vendored
|
||||
copies under `vendor/` that backported `cfg_select!` to `#[cfg]` for the
|
||||
old 1.93 toolchain have been deleted.
|
||||
- The `channel-matrix` feature (and its `matrix-sdk` dependency) was dropped,
|
||||
which removes `matrix-sdk-sqlite` from the tree entirely. That crate pinned
|
||||
`rusqlite 0.37` and previously had to be vendored/patched onto the `0.40`
|
||||
line to avoid a second native sqlite chain; with Matrix gone the patch is
|
||||
deleted.
|
||||
- `whatsapp-rust/sqlite-storage` is disabled because its Diesel storage
|
||||
links sqlite independently; `whatsapp-web` temporarily uses
|
||||
`wacore::store::InMemoryBackend` and logs the non-durable session mode.
|
||||
3. **Unlocks:** crate `SqliteCheckpointer` → later deletion of
|
||||
`src/openhuman/tinyagents/checkpoint.rs` (`SqlRunLedgerCheckpointer`,
|
||||
250 lines) once graphs are re-pointed and `graph_checkpoints` rows are
|
||||
migrated or expired (see `04-sessions/`).
|
||||
4. **Do NOT enable `openai` feature** — OpenHuman providers stay the product
|
||||
source of truth for credentials/billing (spec non-goal).
|
||||
5. Update the "TinyAgents crate: features & compatibility" section in
|
||||
`gitbooks/developing/architecture/agent-harness.md` and the Cargo.toml
|
||||
comment (lines 46–55) after the flip.
|
||||
6. Mark `docs/tinyagents-sdk-gaps.md` items 1, 2, 3, 4, 7, 10, 11, 12
|
||||
as shipped in 1.2.0–1.3.0 (verified against crate source); keep only the
|
||||
residuals, re-verified against 1.3.0: no free-form `ToolSchema` metadata
|
||||
map, no reasoning field on the middleware-facing `harness::model::ModelDelta`,
|
||||
no `root_run_id` on `RunConfig`, no USD field on `Usage`.
|
||||
|
||||
## 1.3.0 delta (verified from the published crate source)
|
||||
|
||||
Same `rusqlite ^0.40 bundled` pin and feature set as 1.2.1. OpenHuman currently
|
||||
pins the compatible patch release locally. New API this plan's workstreams
|
||||
should use directly:
|
||||
|
||||
- `AgentEvent::ToolsFiltered { by, excluded, remaining }` — exposure decisions
|
||||
are now event-native (01.3).
|
||||
- `AgentEvent::{BudgetReserved, BudgetReconciled}` + `BudgetLimits.
|
||||
max_cached_input_tokens` + reservation tracking — pre-spend
|
||||
reserve/reconcile (06).
|
||||
- `AgentEvent::ControlApplied` + `MiddlewareControl::kind()/precedence()` —
|
||||
typed middleware control outcomes with defined precedence (sdk-gaps §13
|
||||
closed).
|
||||
- `ContextualToolSelectionMiddleware::inheriting(...)` — parent→child
|
||||
narrowing composition built in (01.3, 07).
|
||||
- `ToolPolicyMiddleware::{require_sandbox, require_approval,
|
||||
enforce_result_bytes}` builders (01.1).
|
||||
- `ParallelOptions::{with_item_timeout, with_total_timeout,
|
||||
with_cancellation}` (08.1/08.2).
|
||||
- `graph::testkit` TaskStore conformance contracts
|
||||
(`taskstore_concurrent_contract`, `taskstore_replay_contract`) (07.2, 11).
|
||||
- `WorkspaceDescriptor::enforce(path, events)` — violation check that also
|
||||
emits events (08.5).
|
||||
- `ModelSelection.allow_retired`; OpenAI-compat runtime model listing
|
||||
(`ModelListing`/`ModelListWire`) for catalog discovery (02.1/02.4).
|
||||
- Registry: `ComponentKind::{Middleware, Checkpointer, TaskStore, Listener}`,
|
||||
alias diagnostics (`AliasBinding`, cross-kind name-reuse) (10).
|
||||
- `EventRecord::with_stream_id` — stream ids on event records (05.1).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Both Cargo worlds `cargo check` clean with `sqlite` feature on:
|
||||
`cargo check --manifest-path Cargo.toml`,
|
||||
`cargo check --manifest-path Cargo.toml --all-features`, and
|
||||
`cargo check --manifest-path app/src-tauri/Cargo.toml`.
|
||||
- One duplicate-free `cargo tree -i libsqlite3-sys` per world, rooted at
|
||||
`vendor/libsqlite3-sys-0.38.0`.
|
||||
- Docs updated; sdk-gaps marked.
|
||||
|
||||
Verified commands:
|
||||
|
||||
- `cargo check --manifest-path Cargo.toml --message-format=short`
|
||||
- `cargo check --manifest-path Cargo.toml --all-features --message-format=short`
|
||||
- `cargo check --manifest-path app/src-tauri/Cargo.toml --message-format=short`
|
||||
- `cargo tree --manifest-path Cargo.toml --all-features -i libsqlite3-sys`
|
||||
- `cargo tree --manifest-path app/src-tauri/Cargo.toml --all-features -i libsqlite3-sys`
|
||||
@@ -1,62 +0,0 @@
|
||||
# 01.1 — ToolPolicy round-trip
|
||||
|
||||
The spec's "SDK gap: ToolSchema has no metadata map" is obsolete: the crate
|
||||
`Tool` trait has `policy() -> ToolPolicy` and `ToolRegistry::policies()`.
|
||||
|
||||
Current status (2026-07-02): adapters expose classified SDK policies, the
|
||||
crate `ToolPolicyMiddleware` is installed from `harness.tools().policies()`, and
|
||||
`ToolOutputMiddleware` now reads per-tool result caps from that registry
|
||||
snapshot instead of rebuilding a `name -> Arc<dyn Tool>` lookup. The remaining
|
||||
crate-internal OpenHuman lookups are behavior-preserving overlays for data the
|
||||
crate policy snapshot cannot represent yet: args-aware external effects,
|
||||
CLI/RPC-only scope, args-aware permission level, and generated-tool runtime
|
||||
context. The local `tinyagents/middleware.rs` module is crate-internal; turn
|
||||
config types stay crate-visible only through `openhuman::tinyagents` re-exports.
|
||||
The `ToolAdapter`/`SharedToolAdapter` execution wrappers are crate-internal
|
||||
implementation details of the shared runner.
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `src/openhuman/tinyagents/tools.rs` (`ToolAdapter`/`SharedToolAdapter`),
|
||||
implement `policy()` by mapping OpenHuman trait methods
|
||||
(`src/openhuman/tools/traits.rs`): permission level → `ToolAccess
|
||||
{ approval_required, background_safe }`; `external_effect` →
|
||||
`ToolSideEffects`; timeout policy → `ToolRuntime.timeout_ms`;
|
||||
concurrency safety → `ToolRuntime.idempotent`; `max_result_size_chars`
|
||||
→ `ToolRuntime.max_result_bytes`; sandbox mode → `SandboxMode`.
|
||||
Every adapter-produced policy sets `classified: true`.
|
||||
2. Partially done: rework `ApprovalSecurityMiddleware`, `CliRpcOnlyMiddleware`,
|
||||
`ToolPolicyMiddleware`, and `ToolOutputMiddleware`
|
||||
(`src/openhuman/tinyagents/middleware.rs`) to read `ToolPolicy` from the
|
||||
registry instead of the shared `name → Arc<dyn Tool>` side-lookup.
|
||||
`ToolOutputMiddleware` is registry-backed. Keep the crate-internal OpenHuman
|
||||
overlays the static crate policy cannot yet express: `external_effect_with_args`,
|
||||
`ToolScope::CliRpcOnly`, `permission_level_with_args`, and
|
||||
`generated_runtime_context`.
|
||||
3. Partially done: install crate `ToolPolicyMiddleware` in
|
||||
`assemble_turn_harness` (`src/openhuman/tinyagents/mod.rs`) with
|
||||
`require_sandbox(true)` only. TinyAgents 1.3 exposes classification,
|
||||
approval, and result-byte gates, but OpenHuman still keeps args-aware
|
||||
approval/audit and legacy result-cap wording in local overlays. Enable the
|
||||
stricter crate gates only after those overlays have equivalent policy
|
||||
coverage or can be deleted. Assert every registered tool is classified in the
|
||||
adapter-inventory test when those gates become fail-closed.
|
||||
4. Done for output budgeting: `TurnContextMiddleware.install` takes the SDK
|
||||
policy snapshot instead of the `&tool_sets` parameter.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Deleted: `ToolOutputMiddleware`'s `name → Arc<dyn Tool>` policy snapshot in
|
||||
`tinyagents/middleware.rs`.
|
||||
- Remaining as crate-internal overlays by design until the SDK policy surface
|
||||
grows equivalent metadata: `external_effect_with_args`, `ToolScope::CliRpcOnly`,
|
||||
`permission_level_with_args`, and `generated_runtime_context` overlays.
|
||||
- Deleted: redundant per-call trait re-queries in the legacy
|
||||
`agent_tool_exec.rs` policy chain.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `ToolRegistry::policies()` snapshot test: all tools classified, policies
|
||||
serialize stably.
|
||||
- Approval/security/output behavior parity tests still green
|
||||
(middleware suite ~22 tests).
|
||||
@@ -1,34 +0,0 @@
|
||||
# 01.2 — Unknown-tool recovery via RunPolicy
|
||||
|
||||
Crate `RunPolicy.unknown_tool` now has `UnknownToolPolicy::{Fail,
|
||||
ReturnToolError, Rewrite { tool_name }}` plus an `AgentEvent::UnknownToolCall`
|
||||
variant — the sentinel is deletable.
|
||||
|
||||
Current status (2026-07-02): `run_policy_for` sets
|
||||
`UnknownToolPolicy::ReturnToolError`, `OpenhumanEventBridge` projects
|
||||
`AgentEvent::UnknownToolCall` into grep-friendly diagnostics, and the old
|
||||
sentinel/middleware symbols are gone from source.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done: `run_policy_for` sets `RunPolicy.unknown_tool =
|
||||
UnknownToolPolicy::ReturnToolError`, preserving the legacy "model corrects
|
||||
itself" behavior with the original requested name).
|
||||
2. Done: no wording-only sentinel shim remains in `tinyagents/middleware.rs`.
|
||||
3. Done: `AgentEvent::UnknownToolCall` reaches `OpenhumanEventBridge` and is
|
||||
projected distinctly from "tool executed and failed" in debug logs.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Deleted: `UNKNOWN_TOOL_SENTINEL` + sentinel tool registration in
|
||||
`src/openhuman/tinyagents/tools.rs`.
|
||||
- Deleted: `UnknownToolRewriteMiddleware` in `src/openhuman/tinyagents/middleware.rs`
|
||||
(+ its tests, rewritten as RunPolicy behavior tests).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Hallucinated tool name -> recoverable tool error, run continues; event
|
||||
stream records the original requested name; no sentinel appears in
|
||||
transcripts or advertised schemas.
|
||||
- Sub-agent and top-level tests accept TinyAgents' fixed recoverable-tool-error
|
||||
wording while preserving the original requested tool name in events/logs.
|
||||
@@ -1,67 +0,0 @@
|
||||
# 01.3 — Dynamic tool exposure as middleware
|
||||
|
||||
Replace scattered pre-filtering with crate selection middleware.
|
||||
|
||||
Current OpenHuman surfaces: `agent/harness/tool_filter.rs` (mechanics),
|
||||
`tools/user_filter.rs`, sub-agent `tool_prep.rs`, dynamic delegation refresh in
|
||||
`session/turn/tools.rs`, channel permission ceiling checks.
|
||||
|
||||
Current status: the shared TinyAgents runner still registers only the
|
||||
OpenHuman-computed callable tools, preserving hidden-tool execution semantics.
|
||||
It now tags OpenHuman run contexts and emits a diagnostics-only
|
||||
`AgentEvent::ToolsFiltered` via `openhuman_tool_visibility` when that existing
|
||||
allowlist withholds candidate tools. Full middleware-owned selection remains.
|
||||
Production tool exposure now goes exclusively through
|
||||
`run_turn_via_tinyagents_shared` + `SharedToolAdapter`; the old non-shared
|
||||
`ToolAdapter` wrapper is test-only.
|
||||
|
||||
`agent/harness/tool_filter.rs` is still live for `integrations_agent` toolkit
|
||||
spawns: the runner uses it to choose a compact Composio action set before
|
||||
registering dynamic tools.
|
||||
`src/openhuman/agent/harness/subagent_runner/tool_prep.rs` is also still live,
|
||||
and it currently mixes filtering (`filter_tool_indices`, nested-delegation
|
||||
stripping, denylist checks, toolkit top-K budgets) with non-filter helpers
|
||||
(`load_prompt_source`, text-mode protocol instructions). Delete it only after
|
||||
`ContextualToolSelectionMiddleware` owns child/toolkit selection and those
|
||||
non-filter helpers have moved to narrower modules. The `tool_prep` helper
|
||||
surface has been narrowed to `subagent_runner` except for the text-mode protocol
|
||||
renderer used by prompt debug dumps. The turn-local parent-context/progress and
|
||||
cached integration refresh helpers in `session/turn/tools.rs` are scoped to the
|
||||
turn module, while the cross-surface integration fetch and delegation refresh
|
||||
entrypoints remain live.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Express agent `tool_allowlist`/`tool_denylist`, sub-agent tool scope,
|
||||
MCP tool visibility, and the channel permission ceiling as a composed
|
||||
`ToolAllowlistMiddleware` + one OpenHuman
|
||||
`ContextualToolSelectionMiddleware`. TinyAgents 1.3.0's selection context
|
||||
carries run id, depth, tags, and requested model; OpenHuman-specific agent
|
||||
id, task kind, security tier, and channel either stay in local middleware
|
||||
state or are encoded into tags. Inheritance rule: children can only narrow —
|
||||
use `ContextualToolSelectionMiddleware::inheriting(...)`.
|
||||
2. Fail closed when policy metadata is missing (unclassified → not exposed);
|
||||
exposure decisions are event-native via `AgentEvent::ToolsFiltered
|
||||
{ by, excluded, remaining }` (1.3.0) — projected into the bridge as
|
||||
structured diagnostics today.
|
||||
3. Move the visible-set computation, dynamic delegation refresh, and skill-event
|
||||
catalogue reconciliation out of
|
||||
`src/openhuman/agent/harness/session/turn/tools.rs` (616 lines) plus
|
||||
`subagent_runner/tool_prep.rs` (344 lines) into the middleware; the turn code
|
||||
only declares candidate tool sets and parent execution context.
|
||||
4. Keep product policy sources (registry definitions, security tier tables)
|
||||
in OpenHuman — the middleware consumes them.
|
||||
|
||||
## Deletions
|
||||
|
||||
- `agent/harness/tool_filter.rs` mechanics (299 lines; keep policy tables if
|
||||
any inline).
|
||||
- `subagent_runner/tool_prep.rs` (344 lines).
|
||||
- Filtering/refresh blocks in `session/turn/tools.rs` (file shrinks to
|
||||
parent-context and assembly glue).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Sub-agents can never see a tool the parent couldn't grant (test).
|
||||
- Exposure decision visible in run events.
|
||||
- CLI/RPC-only denial (`CliRpcOnlyMiddleware`) unaffected or folded in.
|
||||
@@ -1,52 +0,0 @@
|
||||
# 01.4 — Tool output budgets, summarizer, artifacts
|
||||
|
||||
Finish moving tool-result post-processing to `after_tool` middleware and
|
||||
delete the legacy hooks.
|
||||
|
||||
## Steps
|
||||
|
||||
Current status: OpenHuman's local `ToolOutputMiddleware` now runs on the
|
||||
TinyAgents harness as `after_tool` for payload summarization, TokenJuice
|
||||
`compact_output_with_policy`, per-tool policy-derived output caps, generic byte
|
||||
budgets, and action-workspace artifact spill for oversized session tool
|
||||
results. The SDK's closest built-in cap is `ToolPolicyMiddleware` result-byte
|
||||
enforcement, which remains disabled here because OpenHuman still owns the
|
||||
legacy marker/artifact behavior. Payload summarization and TokenJuice shrinks
|
||||
now emit TinyAgents `AgentEvent::Compressed`, and persisted action-workspace
|
||||
artifacts are indexed in `RunContext.stores` under
|
||||
`openhuman_tool_result_artifacts` while the existing `.txt` file and `file_read`
|
||||
envelope remain the source of truth. The live TinyAgents middleware already
|
||||
calls the parent-context-aware `PayloadSummarizer::maybe_summarize_in_parent`,
|
||||
and that implementation dispatches the summarizer through
|
||||
`SubAgent::invoke_in_parent` so child lineage/events inherit the parent
|
||||
TinyAgents context. The older direct-executor payload-summarizer hook is
|
||||
removed, the old `session/agent_tool_exec.rs` test-only parity shim is deleted,
|
||||
and the stale default-Full `tokenjuice::compact_tool_output` wrapper is removed.
|
||||
|
||||
1. `payload_summarizer.rs` (live in `src/openhuman/tinyagents/`; oversized-result
|
||||
compression via a `summarizer` sub-agent + circuit breaker):
|
||||
`ToolOutputMiddleware` now calls the parent-context-aware summarizer seam,
|
||||
and that live path uses `SubAgent::invoke_in_parent` for child depth/event
|
||||
lineage. Remaining work: emit any additional `SummaryRecord`-style
|
||||
provenance needed beyond `AgentEvent::Compressed`.
|
||||
2. `tokenjuice::compact_tool_output`: deleted after confirming
|
||||
`compact_output_with_policy` covers the live TinyAgents middleware and
|
||||
legacy direct executor paths; decision recorded in
|
||||
`src/openhuman/tokenjuice/README.md`.
|
||||
3. `harness/tool_result_artifacts/mod.rs` (588 lines, artifact spill):
|
||||
keep spill policy and action-workspace `.txt` writes in OpenHuman, but keep
|
||||
the run's `StoreRegistry` (`RunContext.stores`) populated with structured
|
||||
artifact metadata so replay can find the model-facing preview's full body.
|
||||
4. Done: `session/agent_tool_exec.rs` was deleted after the live turn path moved
|
||||
to TinyAgents middleware.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Deleted: `session/agent_tool_exec.rs` legacy direct-executor shim.
|
||||
- `tokenjuice::compact_tool_output`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Oversized tool result → compressed with provenance event; parity fixture
|
||||
vs old summarizer output shape.
|
||||
- No tool-result post-processing outside middleware.
|
||||
@@ -1,33 +0,0 @@
|
||||
# 01 — Tooling
|
||||
|
||||
Move tool metadata, policy enforcement, unknown-tool recovery, dynamic
|
||||
exposure, and output budgeting onto SDK primitives; delete the OpenHuman
|
||||
side-lookup pattern and legacy tool plumbing.
|
||||
|
||||
Target SDK surface (available across tinyagents 1.2.x–1.3.0; current repo lock
|
||||
is 1.5.0):
|
||||
|
||||
- `Tool::policy() -> ToolPolicy { side_effects, runtime, access }` —
|
||||
serializable safety metadata (read_only/writes_files/network/destructive/
|
||||
payment; timeout/retries/idempotent/sandbox/max_result_bytes; workspace
|
||||
access/trusted_roots/credentials/approval_required/background_safe).
|
||||
- `ToolPolicyMiddleware` (fail-closed on unclassified), `ToolAllowlistMiddleware`,
|
||||
`DynamicToolSelectionMiddleware`, `ContextualToolSelectionMiddleware`,
|
||||
`HumanApprovalMiddleware`.
|
||||
- `RunPolicy.unknown_tool: UnknownToolPolicy::{Fail, ReturnToolError, Rewrite}`.
|
||||
- `ToolRegistry::policies()`, `ToolExecutionContext.workspace:
|
||||
WorkspaceDescriptor`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-tool-policy.md` — implement `policy()` on the adapters; retire the
|
||||
`name → Arc<dyn Tool>` side-lookup.
|
||||
2. `02-unknown-tool.md` — replace the sentinel with `UnknownToolPolicy`.
|
||||
3. `03-dynamic-exposure.md` — allowlists/denylists/channel ceiling as
|
||||
selection middleware.
|
||||
4. `04-tool-output.md` — output budgets + payload summarizer as `after_tool`;
|
||||
delete legacy files.
|
||||
|
||||
Done when: no OpenHuman middleware queries tool trait methods ad hoc; the
|
||||
sentinel is gone; tool visibility decisions are middleware-owned and
|
||||
event-visible; deletions in `99-deletion-ledger.md` §tooling are done.
|
||||
@@ -1,35 +0,0 @@
|
||||
# 02.1 — Workload routes as ModelRegistry entries
|
||||
|
||||
Today `run_turn_via_tinyagents_shared` registers exactly one crate-internal
|
||||
`ProviderModel` per run. Target: register the run's full resolved route set so fallback and
|
||||
capability resolution happen inside the SDK.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Extend `assemble_turn_harness` to register one `ProviderModel` per
|
||||
workload route the turn may use (`chat`, `agentic`, `reasoning`, `coding`,
|
||||
`memory`, `subconscious`, `burst`, `summarization`, `vision` — from
|
||||
`provider/router.rs` tier names), each
|
||||
with its real `ModelProfile` (already built at construction; add
|
||||
structured-output/reasoning flags from provider capabilities as they
|
||||
gain accessors).
|
||||
2. Use `ModelRequest::with_required_capabilities` /
|
||||
`ModelSelection`/`ModelHint` so per-call needs (vision, tools, reasoning)
|
||||
reject unfit models pre-dispatch instead of failing at the provider.
|
||||
3. `router.rs` keeps owning tier-name → provider-string policy; the
|
||||
translation to registry entries lives in one adapter fn
|
||||
(`tinyagents/model.rs` or a new `tinyagents/routes.rs`).
|
||||
4. Record resolution in events (`ResolvedModel`/`ModelResolutionSource`
|
||||
already on `ModelResponse`).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Ad hoc model-capability checks scattered on turn paths (vision gates in
|
||||
`subagent_runner/ops/graph.rs`, `model_vision` param threading) once
|
||||
`CapabilitySet` covers them.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A turn requesting vision on a non-vision model fails pre-dispatch with a
|
||||
typed error; fallback picks the next capable route.
|
||||
- Adapter-inventory test asserts the registered route set.
|
||||
@@ -1,48 +0,0 @@
|
||||
# 02.2 — Retry/fallback ownership → RunPolicy
|
||||
|
||||
`provider/reliable.rs` (1215 lines + 1443 tests) wraps several production
|
||||
provider paths in retry/fallback. The crate loop is currently pinned to a single
|
||||
attempt (`RunPolicy.retry.max_attempts = 1`) to avoid double-retry while
|
||||
OpenHuman owns reliability. Audit then collapse to one owner: the SDK.
|
||||
|
||||
Current status (2026-07-02): do not delete `ReliableProvider` yet. Provider
|
||||
factory paths still wrap the OpenHuman backend in `ReliableProvider` for
|
||||
configured retries and model fallbacks, and `run_policy_for` explicitly sets
|
||||
`policy.retry.max_attempts = 1` so TinyAgents does not double-retry while that
|
||||
wrapper owns reliability. Deletion is blocked on moving the configured fallback
|
||||
chain into registered crate model routes plus event-visible retry/fallback
|
||||
parity. TinyAgents 1.3.0 `RunPolicy::fallback` can retry fallback model routes,
|
||||
but it does not by itself emit the OpenHuman `FallbackSelected` parity signal;
|
||||
that event parity requires `ModelFallbackMiddleware` or an equivalent bridge.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Map `ReliableProvider` behaviors onto crate primitives:
|
||||
transient 429/5xx → `RetryPolicy` (exp backoff) + `RateLimitMiddleware`;
|
||||
provider chain → `FallbackPolicy` (ordered model names, needs 02.1
|
||||
multi-registration); permanent config rejection / billing exhaustion →
|
||||
non-retryable `ProviderError { retryable: false }` mapping in
|
||||
`ProviderModel`.
|
||||
2. Keep OpenHuman's error classification (`ops/http_error.rs`,
|
||||
`billing_error`, `auth_error_registry`) as the mapper that fills
|
||||
`ProviderError`; it is product knowledge.
|
||||
3. Un-wrap `ReliableProvider` from `session/builder/factory.rs` (re-layered
|
||||
there 2026-06-30) once parity tests cover: transient retry count, retry
|
||||
events (`RetryScheduled`), fallback events (`FallbackSelected`), and NO
|
||||
double-retry (assert total attempt counts with `MockModel::call_count`).
|
||||
Keep `RunPolicy.retry.max_attempts = 1` until this swap happens; raising it
|
||||
earlier would reintroduce double-retry on top of `ReliableProvider`.
|
||||
4. Non-turn callers of `ReliableProvider` and its classifier helpers (memory-tree
|
||||
local summarizer, memory scoring, triage error classification): either route
|
||||
them through a minimal harness invoke or a small shared retry/classification
|
||||
utility — inventory call sites first; do not leave a fork.
|
||||
|
||||
## Deletions
|
||||
|
||||
- `src/openhuman/inference/provider/reliable.rs` + tests (rewrite the
|
||||
behaviorally-relevant cases against RunPolicy).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Attempt-count parity matrix (429, 500, config rejection, billing) green.
|
||||
- `FallbackSelected`/`RetryScheduled` visible in the event bridge.
|
||||
@@ -1,60 +0,0 @@
|
||||
# 02.3 — Native reasoning + tool-arg streaming
|
||||
|
||||
tinyagents 1.3.0 `MessageDelta` has a `reasoning` field and
|
||||
`StreamAccumulator::reasoning()`; `ModelStreamItem::ToolCallDelta` streams
|
||||
tool-arg fragments. The out-of-band `ThinkingForwarder` is not one-shot
|
||||
deletable: streaming reasoning can ride the crate stream, but OpenHuman still
|
||||
uses the forwarder for non-streaming post-hoc reasoning and tool-argument
|
||||
progress that preserves the UI's `tool_name`/start-event contract.
|
||||
|
||||
Current status (2026-07-02): streaming provider `ThinkingDelta` values are
|
||||
mapped to `MessageDelta::reasoning(...)`, and `OpenhumanEventBridge` projects
|
||||
`delta.reasoning` into the same parent/sub-agent thinking progress events as the
|
||||
old forwarder path. Tool-call **argument** fragments now ride the crate stream
|
||||
too: `forward_delta` maps each `ProviderDelta::ToolCallArgsDelta` onto a
|
||||
`ModelStreamItem::ToolCallDelta(ToolDelta { call_id, content })`, the crate
|
||||
agent-loop surfaces it as `MessageDelta.tool_call`, and `OpenhumanEventBridge`
|
||||
projects it into `AgentProgress::ToolCallArgsDelta`. Because the crate
|
||||
`ToolDelta` has only `call_id`/`content` (no `tool_name`), the tool-call
|
||||
**start** event — the empty-delta `ToolCallArgsDelta` that carries the tool name
|
||||
and opens the UI timeline row — still rides `ThinkingForwarder.note_tool_call`,
|
||||
which records the name into a `call_id → tool_name` map (`ToolNameMap`) *shared*
|
||||
with the bridge so the streamed fragments stay labelled. The forwarder remains
|
||||
live for that start marker and for non-streaming post-hoc reasoning; its
|
||||
`emit_tool_args` argument-fragment path has been removed. The `StreamAccumulator`
|
||||
treats the terminal `Completed` response as authoritative, so streaming the
|
||||
fragments never disturbs the final native tool calls.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done for streaming providers: `src/openhuman/tinyagents/model.rs` maps
|
||||
provider thinking deltas to `MessageDelta::reasoning(...)` on the crate
|
||||
stream, and `OpenhumanEventBridge` (`observability.rs`) projects
|
||||
`delta.reasoning` into OpenHuman progress events.
|
||||
2. Done: provider tool-call argument fragments map onto
|
||||
`ModelStreamItem::ToolCallDelta` (crate `ToolDelta { call_id, content }`) and
|
||||
are projected by `OpenhumanEventBridge`. `ToolDelta` carries no `tool_name`,
|
||||
so the `tool_name`/start-event half of the UI timeline contract stays on
|
||||
`ThinkingForwarder.note_tool_call`, which shares a `call_id → tool_name` map
|
||||
(`ToolNameMap`) with the bridge so the streamed fragments keep their label.
|
||||
Only `ThinkingForwarder.emit_tool_args` was removed.
|
||||
3. Verify sub-agent child thinking deltas still reach the scope-aware bridge
|
||||
(parity with the current forwarder behavior).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Reasoning side of `ThinkingForwarder`: moved for streaming providers; keep the
|
||||
non-streaming fallback until the non-streaming path has an equivalent event.
|
||||
- Argument-fragment side of `ThinkingForwarder` (`emit_tool_args`, in
|
||||
`tinyagents/model.rs`): removed — fragments ride `ToolCallDelta` and are
|
||||
projected by the bridge. The tool-call **start** marker
|
||||
(`note_tool_call`, empty-delta `ToolCallArgsDelta` with `tool_name`) stays,
|
||||
since the crate `ToolDelta` cannot carry a tool name; it will only move if a
|
||||
future crate revision adds `tool_name`/start semantics to `ToolDelta`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- UI receives visible-text, reasoning, and tool-arg deltas from crate stream
|
||||
items alone, including `tool_name`/start-event semantics; streaming e2e (chat
|
||||
- sub-agent) green.
|
||||
- Non-streaming providers emit post-hoc reasoning once.
|
||||
@@ -1,51 +0,0 @@
|
||||
# 02.4 — One model catalog
|
||||
|
||||
Model metadata is currently split across several sources: `cost/catalog.rs`
|
||||
(pricing + windows), `Config::model_registry` seeding/enrichment,
|
||||
`model_context.rs` tier/pattern/local fallbacks, provider capability accessors,
|
||||
provider `effective_context_window`, and `docs/inference-provider-catalog.md`.
|
||||
Crate `registry::ModelCatalog` is the normalized shape
|
||||
(`ModelCatalogEntry { provider, model_id, aliases, max_input_tokens,
|
||||
max_output_tokens, pricing, capabilities }` incl. cache/reasoning rates).
|
||||
|
||||
Current status (2026-07-02): `cost/catalog.rs` is a 622-line static pricing and
|
||||
window table plus registry-enrichment/estimate helpers. The unused
|
||||
`context_window` convenience wrapper is gone, the raw pricing table is private,
|
||||
and `context_window_for_model` now consults the richer `lookup` row for concrete
|
||||
model ids before falling back to legacy model-context patterns. OpenHuman's
|
||||
`ModelPrice` currently has input, cached-input, and output rates; TinyAgents
|
||||
`ModelPricing` also models cache-creation and reasoning-token rates, so the
|
||||
projection must preserve those gaps explicitly rather than pretending the local
|
||||
table is already a complete crate catalog. First projection helpers landed:
|
||||
`tinyagents_catalog_entry_for_model` maps one static OpenHuman row to a
|
||||
TinyAgents `ModelCatalogEntry`, and `tinyagents_catalog_snapshot` maps the whole
|
||||
table to `ModelCatalogSnapshot`, with per-token input/cache-read/output rates and
|
||||
context windows. Cache-creation/reasoning prices and most runtime capability
|
||||
flags remain unset until the catalog source can own them authoritatively.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Build a `ModelCatalogSnapshot` from OpenHuman's data: seed from crate
|
||||
`ModelCatalog::seed()`, overlay `cost/catalog.rs` rates/windows and
|
||||
remaining `model_context.rs` pattern fallbacks, plus local-model profiles discovered at
|
||||
runtime (ollama). Partially done: `cost/catalog.rs` can now project one
|
||||
static row into TinyAgents `ModelCatalogEntry` and all static rows into a
|
||||
`ModelCatalogSnapshot`; crate-seed merging and local-model overlays remain.
|
||||
2. Point consumers at the one projection: `estimate_cost_usd`
|
||||
(cost bridge), token budgeting / `effective_context_window`, model picker
|
||||
RPC, capability filter (02.1 profiles via
|
||||
`ModelProfile::from_catalog_entry`).
|
||||
3. Keep the catalog OpenHuman-refreshable (config/update path) — crate seed
|
||||
is a fallback, not the source.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Duplicate per-MTok rate tables and window constants outside the catalog
|
||||
(`cost/catalog.rs` becomes the loader/owner of the snapshot rather than a
|
||||
second schema, or is folded into it).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Pricing/window/capability lookups have exactly one code path; cost
|
||||
estimates unchanged (existing catalog tests re-pointed).
|
||||
- Local models appear in the catalog with runtime-discovered profiles.
|
||||
@@ -1,28 +0,0 @@
|
||||
# 02 — Models & Providers
|
||||
|
||||
Make tinyagents the model abstraction: registry-resolved models with real
|
||||
profiles, SDK-owned retry/fallback, native reasoning streaming, and catalog-
|
||||
driven capability/pricing data. OpenHuman keeps credentials, OAuth, billing
|
||||
classification, and provider construction (`factory.rs`).
|
||||
|
||||
Target SDK surface: `ModelRegistry::resolve(ModelSelection)`, `ModelProfile`/
|
||||
`CapabilitySet` (incl. `reasoning`), `RetryPolicy`/`FallbackPolicy` on
|
||||
`RunPolicy` + `ModelFallbackMiddleware`, `ProviderError` (normalized,
|
||||
`retryable` flag), `ModelStreamItem::MessageDelta { text, reasoning,
|
||||
tool_call }`, `registry::ModelCatalog{,Entry,Pricing,Capabilities}`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-model-registry.md` — workload routes as registry entries.
|
||||
2. `02-fallback-retry.md` — collapse `reliable.rs` double-retry.
|
||||
3. `03-streaming-reasoning.md` — stream reasoning natively; shrink
|
||||
`ThinkingForwarder` to the remaining tool-arg/non-streaming fallback seams.
|
||||
4. `04-catalog.md` — one catalog for pricing/windows/capabilities.
|
||||
|
||||
Done when: `provider/reliable.rs` is deleted, reasoning/tool-argument streaming
|
||||
rides the crate stream without OpenHuman side channels, model resolution is
|
||||
capability-checked pre-dispatch, and pricing/context-window data has one source.
|
||||
|
||||
Keep (product): `provider/factory.rs`, `provider/router.rs` route *names*,
|
||||
credential/OAuth/billing modules, `local/` daemon, `model_ids.rs` config
|
||||
policy.
|
||||
@@ -1,42 +0,0 @@
|
||||
# 03.1 — Delete superseded context reducers
|
||||
|
||||
`tinyagents/middleware.rs` doc-comments already note these were "effectively
|
||||
dead on the live loop" before being re-expressed as middlewares.
|
||||
|
||||
Current status: `harness/compaction/cache_align.rs` and the
|
||||
`harness/compaction/mod.rs` shim are deleted; the volatile-token detector now
|
||||
lives inside TinyAgents `CacheAlignMiddleware`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. `context/microcompact.rs` (269): deleted. The shared placeholder/default
|
||||
constants now live in `context/mod.rs`, and the live clearing logic is the
|
||||
TinyAgents `MicrocompactMiddleware`.
|
||||
2. `context/pipeline.rs` (454) + `context/guard.rs` (236): deleted. The
|
||||
minimal usage/session-memory state now lives in `context/stats.rs`, and live
|
||||
compression uses `ContextCompressionMiddleware` + `summarize.rs`
|
||||
(`SUMMARIZE_THRESHOLD_FRACTION` preserves the 0.90 trigger).
|
||||
3. `harness/compaction/cache_align.rs` (200) + `compaction/mod.rs` shim:
|
||||
superseded by `CacheAlignMiddleware`; directory deleted.
|
||||
4. `context/tool_result_budget.rs` (172): deleted. The UTF-8-safe fallback
|
||||
truncation helper now lives beside action-workspace artifact preview
|
||||
handling; the live TinyAgents path uses `ToolOutputMiddleware` per-tool
|
||||
caps.
|
||||
5. Legacy compaction-breaker fields in `ContextStatsState` / `ContextStats`:
|
||||
deleted after exact non-test call-path checks showed no production readers.
|
||||
The remaining stats projection is UI utilisation plus session-memory state.
|
||||
6. Summarization provenance: ensure every compression emits
|
||||
`AgentEvent::Compressed` with `SummaryRecord` data (source ids,
|
||||
before/after token estimates) — wire `ContextCompressionMiddleware::
|
||||
records()` into the run outcome/journal.
|
||||
|
||||
## Deletions
|
||||
|
||||
- `context/microcompact.rs`, `context/pipeline.rs`, `context/guard.rs`, and
|
||||
`harness/compaction/` are deleted.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `context/` line count drops ~1.2k; `stats()` UI footer parity test green;
|
||||
compression fires at 90% window on all three turn paths (existing
|
||||
summarize tests).
|
||||
@@ -1,35 +0,0 @@
|
||||
# 03.2 — Crate cache layer
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Prompt-prefix protection:** install `PromptCacheGuardMiddleware` in
|
||||
`assemble_turn_harness`; declare the stable prefix with
|
||||
`ModelRequest::cache_segments` (system prompt + tool schemas as
|
||||
`PromptSegment`s from the prompt builder). Route `CacheLayoutEvent`s to
|
||||
the event bridge as warnings — replaces the old cache_align warn-log with
|
||||
structured events. Set `CachePolicy.protect_prompt_prefix = true` on the
|
||||
turn `RunPolicy`.
|
||||
2. **Response cache:** attach a `ResponseCache` via
|
||||
`AgentHarness::with_response_cache` for deterministic internal calls
|
||||
(summarizer/triage/subconscious-style runs; NOT interactive chat).
|
||||
Start with `InMemoryResponseCache`; a `Store`-backed impl over
|
||||
`FileStore` is a follow-up if hit rates justify it. Gate per-request
|
||||
with `ModelRequest::with_cache_policy`.
|
||||
3. Assert prompt-prefix stability across turns in a fixture test:
|
||||
`PromptCacheLayout::from_request(...).is_prefix_stable_against(prev)` —
|
||||
volatile content (timestamps, memory, steering) must land in the tail.
|
||||
This formalizes the prompt-cache-stability decisions currently embedded
|
||||
in session prompt assembly.
|
||||
4. Surface `CacheHit`/`CacheMiss` counts in the cost footer projection.
|
||||
|
||||
## Deletions
|
||||
|
||||
- `CacheAlignMiddleware`'s bespoke detector once `PromptCacheGuardMiddleware`
|
||||
+ layout events cover it (keep OpenHuman's volatile-token *vocabulary* as
|
||||
test fixtures).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Prefix-stability fixture green over a 3-turn session with memory injection.
|
||||
- Response-cache hit serves an identical deterministic request (test with
|
||||
`MockModel::call_count`).
|
||||
@@ -1,32 +0,0 @@
|
||||
# 03 — Context management & caching
|
||||
|
||||
Live context reduction already runs on `ContextCompressionMiddleware` +
|
||||
`MessageTrimMiddleware` + `tinyagents/summarize.rs`; microcompact and
|
||||
cache-align live as TinyAgents middlewares. This workstream deletes the superseded
|
||||
originals and adopts the crate cache layer.
|
||||
|
||||
Current status (2026-07-02): the legacy reducers are gone. `context/` is 1237
|
||||
lines total, including 1051 lines across prompt assembly (`prompt.rs`, `channels_prompt.rs`),
|
||||
session-memory bookkeeping (`session_memory.rs`), and stats/config state
|
||||
(`manager.rs`, `stats.rs`). The inert legacy compaction-breaker stats have also
|
||||
been removed. Those files remain product-owned; cache correctness work now targets
|
||||
TinyAgents middleware and crate cache events.
|
||||
|
||||
Target SDK surface: `ResponseCache`/`InMemoryResponseCache` + `cache_key`,
|
||||
`CachePolicy { response_cache_enabled, protect_prompt_prefix }`,
|
||||
`PromptCacheLayout` + `PromptCacheGuardMiddleware` + `CacheLayoutEvent`,
|
||||
`AgentEvent::{CacheHit, CacheMiss, Compressed}`, `SummaryRecord`,
|
||||
`ModelRequest::cache_segments(Vec<PromptSegment>)`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-delete-legacy-context.md` — remove dead reducers, consolidate stats.
|
||||
2. `02-cache-layer.md` — response cache + prompt-prefix protection.
|
||||
|
||||
Done when: `context/` contains only OpenHuman-specific state (prompt
|
||||
assembly inputs, session-memory bookkeeping, stats projection) and cache
|
||||
correctness is asserted by crate events, not warn-logs.
|
||||
|
||||
Keep (product): `context/manager.rs` prompt-assembly surface,
|
||||
`session_memory.rs`, `channels_prompt.rs`, `context/stats` for the UI
|
||||
utilisation footer.
|
||||
@@ -1,33 +0,0 @@
|
||||
# 04.1 — Live turns write tinyagents records
|
||||
|
||||
Today new turns append to `session_raw/*.jsonl` via `transcript.rs`;
|
||||
the tinyagents store only receives one-time imports.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add a session store handle (same `FileStore`/`JsonlAppendStore` layout as
|
||||
`session_import`: `{workspace}/tinyagents_store/{kv,journal}`) to the
|
||||
session shell; register it on `RunContext.stores` in
|
||||
`assemble_turn_harness`.
|
||||
2. After each turn, append the turn's messages to
|
||||
`session.{stem}.messages` and upsert the session descriptor (ns
|
||||
`sessions`) — dual-write alongside the legacy JSONL append in
|
||||
`session/turn/session_io.rs`. Reuse `session_import/convert.rs`
|
||||
normalization so live and imported records are shape-identical.
|
||||
3. Adopt `StoreChatHistory` as the in-run history mechanism where the shell
|
||||
currently hand-threads message vectors (evaluate: it must preserve
|
||||
native tool-call envelopes and XML/P-format compat suffixes verbatim —
|
||||
if not, keep OpenHuman assembly and only persist through the store).
|
||||
4. Write-side parity test: run a turn, assert legacy JSONL and store stream
|
||||
render identical transcripts (reuse importer's JournalMessage parity
|
||||
helper).
|
||||
|
||||
## Deletions
|
||||
|
||||
None yet (dual-write phase). Deletion lands in 04.2.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Every new turn appears in the store journal with correct stem, thread_id,
|
||||
provider/model metadata, tool-call ids.
|
||||
- Dual-write behind one flag so 04.2 can flip reads independently.
|
||||
@@ -1,43 +0,0 @@
|
||||
# 04.2 — Read-side shadow, cutover, retirement
|
||||
|
||||
Import phases 2–4 from `../../tinyagents-session-migration-design.md`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Shadow (phase 2):** add a store-backed reader implementing the same
|
||||
surface as `transcript.rs` (`read_transcript`, history load in
|
||||
`session_io.rs`, `.md` render). Behind a flag, read BOTH and log
|
||||
divergence (grep-friendly `[session_import]` prefix); run the 11-fixture
|
||||
matrix + live dogfood.
|
||||
2. **Cutover (phase 3):** run `openhuman.session_import_run` automatically
|
||||
once at boot (idempotent marker `migrations/session_import_v1` already
|
||||
exists), flip default reads to the store, keep legacy JSONL as
|
||||
write-through backup for one release.
|
||||
3. **Retire (phase 4):** stop legacy writes; delete the legacy stack.
|
||||
4. Sub-agent transcript stems (`__` chains) and worker-thread mirrors must
|
||||
resolve through store descriptors — verify against
|
||||
`subagent_runner` mirroring and `agent_orchestration/subagent_sessions/`
|
||||
(that store also folds into descriptors here).
|
||||
5. Parser retirement gate: keep `agent/dispatcher.rs` and
|
||||
`harness/parse.rs` until no live provider path needs XML/P-format prompt
|
||||
dialects, native text-fallback parsing, TinyAgents text-mode response
|
||||
parsing, or sub-agent checkpoint cleanup.
|
||||
|
||||
## Deletions (phase 4)
|
||||
|
||||
- `session/transcript.rs` (1347) + `transcript_tests.rs`.
|
||||
- `session/migration.rs` (373) + tests — old date-folder migration, still
|
||||
invoked at startup through the `migrate_session_layout_if_needed` re-export
|
||||
until the read-side cutover has a replacement startup gate.
|
||||
- `session/turn/session_io.rs` (391) — replaced by store reader/writer.
|
||||
- `session_db/{ops,store,schemas,types}.rs` generic session parts (keep
|
||||
`run_ledger/` until 05).
|
||||
- `agent_orchestration/subagent_sessions/` (~650).
|
||||
- `src/openhuman/session_import/` (whole domain + RPC controller from
|
||||
`core/all.rs`) once a marker-versioned release has shipped.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- All fixtures byte/render-identical between readers before flip.
|
||||
- Old UI session lists still answer by OpenHuman session key (compat
|
||||
projection over descriptors).
|
||||
@@ -1,43 +0,0 @@
|
||||
# 04.3 — SqliteCheckpointer swap
|
||||
|
||||
Baseline is complete: OpenHuman is on tinyagents 1.5.0 with the `sqlite`
|
||||
feature and the compatible `rusqlite` pin. The remaining work is the
|
||||
OpenHuman-row migration/expiry decision.
|
||||
|
||||
Current status (2026-07-02): do not delete
|
||||
`src/openhuman/tinyagents/checkpoint.rs` yet. `SqlRunLedgerCheckpointer` is still
|
||||
the live durable checkpointer for delegation graphs, and it writes OpenHuman's
|
||||
`graph_checkpoints` run-ledger schema. Its adapter surface is now crate-internal.
|
||||
The crate `SqliteCheckpointer` uses its own checkpoint schema, so swapping it in
|
||||
requires an explicit row migration or documented expiry policy for in-flight
|
||||
durable graph runs.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Point durable graphs at crate `SqliteCheckpointer` only after the schema
|
||||
migration/expiry decision is implemented. Current production usage of
|
||||
`SqlRunLedgerCheckpointer` is the delegation graph; `FileCheckpointer`
|
||||
appears only in local TinyAgents delegation tests.
|
||||
2. Migration for existing `graph_checkpoints` rows in the session DB:
|
||||
either a one-time copy into the crate schema or documented expiry
|
||||
(checkpoints are resume-state; expiring in-flight runs at upgrade is
|
||||
acceptable ONLY if no long-lived durable runs exist — audit
|
||||
`workflow_runs` retention first).
|
||||
3. Keep `CheckpointMetadata` (thread_id/checkpoint_id/parent/namespace)
|
||||
projected into the run ledger for the command-center listing RPC —
|
||||
a read-side projection, not a second writer.
|
||||
4. Standardize `DurabilityMode` per graph (Sync for approval-interrupt
|
||||
graphs, Async for fanout).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Later: `src/openhuman/tinyagents/checkpoint.rs`
|
||||
(`SqlRunLedgerCheckpointer`, 250) + the `graph_checkpoints` table creation
|
||||
once migration/expiry ships. Retain it while delegation still points at the
|
||||
OpenHuman run-ledger schema.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Durable graph interrupt → process restart → resume from exact checkpoint
|
||||
(e2e per graph).
|
||||
- Command center still lists checkpoints/current node.
|
||||
@@ -1,26 +0,0 @@
|
||||
# 04 — Sessions & stores
|
||||
|
||||
Make tinyagents `Store`/`AppendStore` the durable session substrate and
|
||||
retire the legacy transcript stack. The Phase-1 write-only importer already
|
||||
exists (`src/openhuman/session_import/`, `openhuman.session_import_run`,
|
||||
design in `../../tinyagents-session-migration-design.md`).
|
||||
|
||||
Target SDK surface: `Store`/`FileStore`, `AppendStore`/`JsonlAppendStore`,
|
||||
`StoreRegistry` on `RunContext`, `ChatHistory`/`StoreChatHistory`,
|
||||
`SessionId`/`ThreadId`/`RunId`, `HarnessRunStatus`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-live-writes.md` — new turns write tinyagents records first.
|
||||
2. `02-read-cutover.md` — import phases 2–4: shadow reads, cutover, retire.
|
||||
3. `03-checkpointer.md` — SqliteCheckpointer swap (needs 00-baseline).
|
||||
|
||||
Done when: `session/transcript.rs`, `session/migration.rs`,
|
||||
`session/turn/session_io.rs`, and `session_import/` are deleted; readers
|
||||
serve from `tinyagents_store/`; run inspection uses crate lineage ids.
|
||||
|
||||
CONSTRAINT (from importer work): store/stream names are slash-free —
|
||||
per-session streams are `session.{stem}.messages`.
|
||||
|
||||
Risk: transcript shape is user data — every cutover step keeps the legacy
|
||||
reader until parity fixtures (11-fixture matrix in the design doc) pass.
|
||||
@@ -1,30 +0,0 @@
|
||||
# 05.1 — Durable journals + status stores
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `assemble_turn_harness`, attach a `StoreEventJournal` (over the
|
||||
04-sessions `JsonlAppendStore`) via a `JournalSink` on the run's
|
||||
`EventSink`, plus a `HarnessStatusStore` writer (start with the
|
||||
in-memory impl process-wide; durable Store-backed status is the target —
|
||||
write a small `Store`-backed `HarnessStatusStore` impl if the crate has
|
||||
none).
|
||||
2. Same for graphs: `JournalGraphSink` + `GraphStatusStore` on delegation/
|
||||
workflow/teams/fanout runs.
|
||||
3. Wrap UI-bound sinks with `RedactingSink`; define the redaction policy
|
||||
(prompts, tool args/results, secrets) in one OpenHuman module.
|
||||
4. Late-attach replay: an RPC (`agent.run_events`?) that reads
|
||||
`read_from(run_id, offset)` + status so the desktop can reconnect
|
||||
mid-run and backfill — replaces reliance on transient `AgentProgress`
|
||||
buffering.
|
||||
5. Feed `session_db/run_ledger` event/telemetry rows FROM journal records
|
||||
(projection job or write-through sink) instead of parallel publishes.
|
||||
|
||||
## Deletions
|
||||
|
||||
None directly; enables 05.2/05.3 deletions.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Kill the UI mid-turn, reattach, reconstruct the full timeline from
|
||||
journal + status (e2e).
|
||||
- `list_by_root` answers "every active descendant of this root run".
|
||||
@@ -1,39 +0,0 @@
|
||||
# 05.2 — One event bridge; delete engine/
|
||||
|
||||
Two progress paths remain: `OpenhumanEventBridge`
|
||||
(`tinyagents/observability.rs`) and scattered direct `publish_global` calls in
|
||||
session/turn code. The old shared `harness/engine/` progress/checkpoint seams
|
||||
are gone.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done for the reusable engine module: `ProgressReporter`/`TurnProgress` moved
|
||||
under `session/tool_progress.rs`, leaving no shared `harness/engine/`
|
||||
surface. The old session-local `agent_tool_exec` compatibility shim is
|
||||
deleted.
|
||||
2. Done: `engine/checkpoint.rs` is gone. `CapPauser` owns the graceful
|
||||
max-iteration stop, and the remaining sub-agent checkpoint summary is
|
||||
localized in `subagent_runner/ops/checkpoint.rs`.
|
||||
3. Sweep direct agent-run/tool/subagent lifecycle `DomainEvent` publishes on
|
||||
turn paths (session/turn, orchestration tools) — emit
|
||||
through the bridge or a typed helper so ordering/rate-limiting is
|
||||
single-owner.
|
||||
4. Current finding (2026-07-02): `agent/progress_tracing.rs` (722) plus
|
||||
`agent/progress_tracing/tests.rs` (616) are not currently redundant. The
|
||||
root module is the opt-in `observability.agent_tracing` exporter fed by the
|
||||
web progress bridge's `AgentProgress` stream. Retain it until the 05.1
|
||||
journal path can produce the same metadata-only OTel/Langfuse spans and
|
||||
append/export contract.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Deleted: `src/openhuman/agent/harness/engine/` (checkpoint + progress seams).
|
||||
- Redundant publishes found in step 3.
|
||||
- Retain `agent/progress_tracing.rs` + sibling tests for now; delete only after
|
||||
journal-backed span export proves parity with the current config contract.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Progress-event parity fixture: same `AgentProgress` sequence for a
|
||||
scripted turn before/after (tool timeline, thinking, cost footer).
|
||||
- No `engine::` imports remain.
|
||||
@@ -1,38 +0,0 @@
|
||||
# 05.3 — DomainEvent & AgentProgress as projections
|
||||
|
||||
`DomainEvent` (core event bus) and `AgentProgress` (UI vocabulary) stay —
|
||||
as compatibility projections fed from crate events, never as primary state.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Catalog current agent-domain `DomainEvent` publishers/subscribers
|
||||
(`agent/bus.rs`, triage, task_dispatcher, orchestration
|
||||
background_delivery/run_ledger_finalize). Current high-volume lifecycle
|
||||
publishers still include `Agent::run_single`, `SpawnSubagentTool::execute`,
|
||||
`ContinueSubagentTool::execute`, `dispatch_subagent`,
|
||||
`project_spawn_parallel_spawned`, and `project_spawn_parallel_result`.
|
||||
`RunLedgerFinalizeSubscriber::handle` remains a real global-bus subscriber:
|
||||
it consumes `SubagentCompleted`/`SubagentFailed` as authoritative detached-run
|
||||
terminal signals until journal/status-store replay covers those paths. For
|
||||
each event that mirrors a crate `AgentEvent`/`GraphEvent`, source it from the
|
||||
journal/bridge. Events with no crate analogue (triage decisions, channel
|
||||
routing) stay native DomainEvents — they are product semantics.
|
||||
2. `agent/bus.rs` native handler `agent.run_turn` stays (transport into the
|
||||
turn), but its progress mirroring moves to the bridge. Current live bridge
|
||||
symbols are `OpenhumanEventBridge::on_event` plus `ThinkingForwarder` for the
|
||||
non-streaming/tool-argument gaps that do not yet have crate event parity.
|
||||
3. Document the rule in `core/event_bus` README: new agent-run telemetry
|
||||
reads crate events/status first; DomainEvent is for cross-domain product
|
||||
signals.
|
||||
4. Do NOT remove `DomainEvent` variants until every subscriber is re-pointed
|
||||
(spec non-goal) — track per-variant status in this file as work lands.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Duplicate mirroring blocks identified in step 1 (list them here when
|
||||
cataloged).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Existing subscribers receive identical DomainEvents (bus tests).
|
||||
- New run-inspection RPCs read journals/status, not the bus.
|
||||
@@ -1,24 +0,0 @@
|
||||
# 05 — Events, status, observability
|
||||
|
||||
Make crate journals/status stores the canonical run record; everything
|
||||
OpenHuman-specific becomes a projection (AgentProgress, DomainEvent, run
|
||||
ledger, cost footer).
|
||||
|
||||
Target SDK surface: `HarnessEventJournal` (`StoreEventJournal` over
|
||||
`JsonlAppendStore`), `HarnessStatusStore` (`list_by_root`, `list_by_thread`,
|
||||
`list_active`), `AgentObservation { run_id, parent_run_id, root_run_id,
|
||||
offset }`, `GraphEventJournal`/`GraphStatusStore`, `RedactingSink`,
|
||||
`FanOutSink`, `RecordingListener`, Langfuse exporters (optional).
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-journals.md` — persist journals + status; late-attach replay.
|
||||
2. `02-bridge-consolidation.md` — one bridge; delete `engine/`.
|
||||
3. `03-domainevent-projection.md` — DomainEvent/AgentProgress as projections.
|
||||
|
||||
Done when: a UI can reconstruct a running/completed turn from persisted
|
||||
crate events without subscribing at start; run-ledger event rows are fed from
|
||||
journal records. `agent/harness/engine/` is already deleted.
|
||||
|
||||
Risk: crosses UI streaming, cost footer, desktop reconnect — parity tests
|
||||
before each deletion.
|
||||
@@ -1,87 +0,0 @@
|
||||
# 06 — Usage, cost, budgets
|
||||
|
||||
Finish moving accounting onto crate primitives; OpenHuman keeps the tracker
|
||||
DB, dashboard RPC, and pricing policy.
|
||||
|
||||
Target SDK surface: `Usage { input_tokens, output_tokens, total_tokens,
|
||||
cache_read_tokens, cache_creation_tokens, reasoning_tokens }`, `UsageTotals`,
|
||||
`CostTotals`, `BudgetMiddleware`/`BudgetLimits`/`BudgetTracker`,
|
||||
`AgentEvent::{UsageRecorded, CostRecorded, BudgetReserved, BudgetReconciled,
|
||||
BudgetWarning, BudgetExceeded}`,
|
||||
`ChildRun.usage`/`RunTree` lineage rollup, `ModelPricing` (catalog, incl.
|
||||
cache/reasoning rates).
|
||||
|
||||
The $0-cost bug and the unobserved-turn tracker gap are already fixed.
|
||||
Remaining:
|
||||
|
||||
Current status (2026-07-02): tinyagents 1.3.0 exposes the target budget and
|
||||
usage primitives, but OpenHuman has not wired `BudgetMiddleware`,
|
||||
`BudgetLimits`, `ChildRun`, or `RunTree` into the shared runner. TinyAgents
|
||||
runtime already emits `AgentEvent::UsageRecorded` after each model call, so the
|
||||
OpenHuman gap is ownership/de-duplication of that event stream, not a separate
|
||||
usage-accounting middleware install. Keep the current OpenHuman cost seams live
|
||||
for now:
|
||||
crate-internal `CostBudgetMiddleware` gates already-exceeded daily/monthly
|
||||
budgets on every shared turn, `OpenhumanEventBridge::record_usage` records
|
||||
`UsageRecorded` into the global tracker, and crate-internal
|
||||
`turn_subagent_usage` folds child spend into the parent turn footer and
|
||||
in-memory `LastTurnUsage` web footer payload. Transcript/session metadata
|
||||
persistence is separate. The bridge now logs TinyAgents budget/cost events
|
||||
(`BudgetReserved`, `BudgetReconciled`, `BudgetWarning`, `BudgetExceeded`,
|
||||
`CostRecorded`) without feeding them into OpenHuman accounting. Installing the
|
||||
crate budget middleware before event de-duplication would risk double-counting
|
||||
`UsageRecorded`.
|
||||
|
||||
Local inventory: there is no local `src/openhuman/tinyagents/cost*` adapter
|
||||
module; TinyAgents itself has `tinyagents::harness::cost`. The current local
|
||||
seams are crate-internal `tinyagents/middleware.rs::CostBudgetMiddleware`
|
||||
(inside the 1861-line middleware module), crate-internal
|
||||
`agent/harness/turn_subagent_usage.rs` (176) for task-local parent-turn rollup,
|
||||
and crate-internal `agent/cost.rs::TurnCost` for the web footer payload, budget
|
||||
stop hooks, and legacy progress compatibility.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Normalize records:** carry `cached_input_tokens`, cache-creation tokens,
|
||||
`reasoning_tokens` (crate `Usage` has it), image/audio/embedding usage where
|
||||
providers report; add `cost_source: estimated | provider_charged` to
|
||||
`TokenUsage`; keep provider-charged USD via an out-of-band carry (crate
|
||||
`Usage` has no USD field — residual upstream gap). Current `TokenUsage`
|
||||
persists cached-input provenance and `cost_source`; reasoning and
|
||||
cache-creation tokens remain zero until providers report them.
|
||||
2. **Crate budget middleware:** replace bespoke `CostBudgetMiddleware`
|
||||
daily/monthly pre-checks with `BudgetMiddleware` where limits map; add
|
||||
per-run and per-thread budgets (new `CostConfig` fields + thread-id
|
||||
threading). TinyAgents `BudgetTracker` is run/tree-local, so it does not
|
||||
directly replace OpenHuman's persistent daily/monthly `CostTracker`
|
||||
semantics. Preflight `BudgetExceeded { blocked: true }` fails a model call
|
||||
before dispatch; post-spend `BudgetExceeded { blocked: false }` is only an
|
||||
event unless OpenHuman installs an explicit pause/failure policy. 1.3.0 also
|
||||
emits `BudgetReserved`/`BudgetReconciled`, but current preflight estimates
|
||||
input tokens and blocks against configured limits rather than pre-reserving
|
||||
cached tokens or projected USD for the next provider call. First build an
|
||||
OpenHuman limits adapter and define `UsageRecorded` ownership so the bridge,
|
||||
crate accounting middleware, and unobserved-turn fallback cannot record the
|
||||
same model call twice.
|
||||
3. **Lineage rollup:** stamp cost records with `run_id`/`root_run_id` from
|
||||
the observation stream (needs `TokenUsage` schema fields); parent totals
|
||||
via `UsageTotals`/`ChildRun` instead of the `turn_subagent_usage`
|
||||
task-local where lineage suffices; keep the task-local only for detached
|
||||
children the tree can't see (or move them to TaskStore rollup — 07.2).
|
||||
4. **Embedding usage:** embedding calls record usage/cost with provider,
|
||||
model, dimensions, vector count (ties into 09).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Later only: crate-internal `CostBudgetMiddleware` after crate budget mapping
|
||||
covers OpenHuman daily/monthly stop behavior, `agent/cost.rs::TurnCost` only
|
||||
where `UsageTotals` covers all current web-footer, stop-hook, TokenJuice, and
|
||||
audit callers, and `turn_subagent_usage.rs` only after 07.2 rollup parity. Do
|
||||
not delete any of these until duplicate `UsageRecorded` semantics are resolved
|
||||
and run-tree/thread-budget integration covers the current stop-hook and
|
||||
parent-turn rollup paths.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Dashboard totals identical before/after on a scripted multi-child turn
|
||||
(no double count); budget-exceeded stops a recursive run pre-spend.
|
||||
@@ -1,56 +0,0 @@
|
||||
# 07.1 — Sub-agent pipeline as a subgraph
|
||||
|
||||
Convert `subagent_runner/` "prepare prompt → filter tools → run child →
|
||||
checkpoint/handback → mirror transcript" into an explicit graph.
|
||||
|
||||
Current status (2026-07-02): `tinyagents/subagent_graph.rs` defines and exports
|
||||
the fixed pipeline topology, and `run_typed_mode` now runs that graph as a
|
||||
best-effort diagnostic skeleton before continuing through the procedural runner.
|
||||
The skeleton records the named phases with `GraphTracingSink`; the node effects
|
||||
remain pending and can be moved over one phase at a time without changing the
|
||||
external sub-agent behavior. The live child turn loop is still
|
||||
`run_subagent_via_graph`: it already uses the shared TinyAgents harness for
|
||||
steering, early exit, cap summaries, transcript persistence, and usage
|
||||
aggregation, but it is not yet a `SubAgentSession`/`subagent_node` graph
|
||||
implementation. The standalone `ops/usage.rs` glue file is deleted; the
|
||||
remaining `AggregatedUsage` bridge lives with the graph route that produces it,
|
||||
and the oversized-result `apply_handoff` helper now lives beside
|
||||
`ResultHandoffCache` in `handoff.rs` instead of under `ops/`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Partially done: define `build_subagent_pipeline_graph` (now in
|
||||
`tinyagents/subagent_graph.rs` or under `subagent_runner/`): nodes
|
||||
`resolve_definition` (registry lookup + allowlist) → `prepare_context`
|
||||
(parent ctx, memory, action root, sandbox) → `assemble_prompt` →
|
||||
`expose_tools` (uses 01.3 selection middleware) → `run_child`
|
||||
(harness leaf via `subagent_node` or direct
|
||||
`run_turn_via_tinyagents_shared`) → `finalize` (checkpoint/
|
||||
awaiting-user handback, worker-thread mirror, handoff cache).
|
||||
Follow the established `build_*_graph` + `*_topology()` pattern; topology
|
||||
export exists, but node effects are still diagnostic placeholders.
|
||||
2. Partially done: `run_typed_mode` (`ops/runner.rs`, the single chokepoint)
|
||||
invokes the compiled subgraph for diagnostic tracing before continuing
|
||||
through the procedural runner; `AgentGraph::Custom` runners still plug in as
|
||||
alternate `run_child` leaves.
|
||||
3. Child lineage: run with parent depth/events (`invoke_in_parent`
|
||||
semantics) so `root_run_id` rollup + `SubAgentStarted/Completed/Reused`
|
||||
events are native; usage rollup via `ChildRun.usage` (feeds 06.3).
|
||||
4. Fold `ops/{provider,prompt,checkpoint}.rs`
|
||||
plumbing into node implementations; `ops/graph.rs` shrinks to the leaf.
|
||||
5. Reusable child sessions: map the follow-up/continue flows onto
|
||||
`SubAgentSession` (`send`, `transcript()`, `reset()`).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Deleted: `subagent_runner/ops/usage.rs` glue.
|
||||
- Deleted: `subagent_runner/ops/handoff_helper.rs` glue; `apply_handoff` moved
|
||||
to `subagent_runner/handoff.rs`.
|
||||
- Remaining: parts of `ops/runner.rs`/`ops/graph.rs` absorbed by nodes (target:
|
||||
`ops/*` shrinks to policy nodes + tests).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Sub-agent e2e parity (ops_tests 1689-line suite green or rewritten
|
||||
against graph events); topology export validates; checkpoint/handback
|
||||
and worker-thread mirroring unchanged from the outside.
|
||||
@@ -1,72 +0,0 @@
|
||||
# 07.2 — Detached sub-agents on durable TaskStore
|
||||
|
||||
`running_subagents.rs` already mirrors lifecycle into the crate TaskStore but
|
||||
still owns watch channels, abort handles, task lookup, and ownership checks. The
|
||||
crate now has `JsonlTaskStore` (durable), `OrchestrationTaskSpec::with_lineage`,
|
||||
filters, and a `SteeringRegistry`.
|
||||
|
||||
Current status (2026-07-02): detached sub-agent lifecycle records now open a
|
||||
per-workspace durable `JsonlTaskStore` at
|
||||
`<workspace_dir>/.openhuman/orchestration_tasks.jsonl` on first spawn, falling
|
||||
back to `InMemoryTaskStore` only if that workspace log cannot be created/opened.
|
||||
Records carry TinyAgents lineage (`parent_run_id`/`root_run_id`), timeout,
|
||||
parent session, parent thread, durable `subagent_session_id`, and workspace
|
||||
metadata, and terminal/cancelled mirrors now resolve the same workspace-scoped
|
||||
store that recorded the spawn. `wait_subagent` still prefers the live registry,
|
||||
but can now resolve `subagent_session_id`, resume metadata, and terminal or
|
||||
still-running status from the workspace-scoped TaskStore after a live-registry
|
||||
miss. `steer_subagent` now uses the same workspace-scoped session-id resolver
|
||||
before delivering to the live executor. Reusable-session close/fresh paths also
|
||||
resolve session cancellation through the workspace store before aborting any live
|
||||
executor. The rest of the executor/control path still uses OpenHuman's watch
|
||||
channels, abort handles, task lookup, and `RunQueue`; the unused future
|
||||
`running_subagents::close` hook has been removed and the test-only typed ledger
|
||||
snapshot plus finished background completion/delivery queues are crate-internal.
|
||||
`running_subagents` and `subagent_sessions` are now crate-only; the live
|
||||
`harness-subagent-audit` binary uses the narrow public `harness_audit` facade
|
||||
for durable session reads and mid-run steer probes. Restart reconciliation,
|
||||
durable store polling, and steering-registry replacement remain pending.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done: detached lifecycle now opens `JsonlTaskStore::open` under the
|
||||
workspace store dir, with `InMemoryTaskStore` only as the open-failure
|
||||
fallback. Task records now carry lineage
|
||||
(`with_lineage(parent_run_id, root_run_id)`), the default detached wait
|
||||
timeout, and thread/session metadata.
|
||||
2. In progress: `wait_subagent` now falls back to TaskStore reads after the
|
||||
live registry misses, `list_subagents` overlays stale durable session
|
||||
summaries with TaskStore status for each `current_task_id`, and
|
||||
`steer_subagent` resolves durable session ids from the same workspace store
|
||||
before attempting live delivery. Close/fresh reusable-session paths use the
|
||||
same workspace-scoped session lookup before cancelling a live task. Continue
|
||||
re-expressing controls on crate semantics: wait →
|
||||
`orchestrate_await`-style store polling/wait handle; cancel/kill →
|
||||
`CancellationToken` + terminal record; steer →
|
||||
`SteeringRegistry` (`TaskId → SteeringHandle`) replacing the RunQueue lookup
|
||||
plumbing. Keep abort-handle hard-kill as the OpenHuman executor detail.
|
||||
3. Keep OpenHuman ownership checks + durable session rows as policy over
|
||||
the store; cancelled/failed states become terminal `OrchestrationTaskRecord`s.
|
||||
4. Restart/resume: on boot, reconcile `JsonlTaskStore` live records against
|
||||
actual executors (orphans → failed-with-restart marker); prove desktop
|
||||
restart parity vs today's behavior. Run the crate's 1.3.0 testkit
|
||||
contracts (`taskstore_concurrent_contract`, `taskstore_replay_contract`)
|
||||
against the chosen store.
|
||||
5. Evaluate exposing crate `orchestrate_*` tools to the orchestrator agent
|
||||
as the internal engine under `spawn_agent`/`wait_agents`/etc. — the
|
||||
product tools stay as the model-visible surface (names/output shapes are
|
||||
product contract).
|
||||
6. While here: root-cause the 2 known-failing detached-orchestrator e2e
|
||||
hangs (`e2e_orchestrator_answers_coding_agent_question...` — child stays
|
||||
Running past timeout; see memory 2026-06-30f).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Watch-channel/task-lookup mechanics in `running_subagents.rs` (1250)
|
||||
(target ≤ ~300 lines of policy + executor glue).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Spawn detached → restart process → list/wait/cancel still work.
|
||||
- running_subagents suite (12) + orchestration tools suite (111) green,
|
||||
incl. the 2 currently-failing e2e.
|
||||
@@ -1,70 +0,0 @@
|
||||
# 07.3 — Steering surface + one recursion authority
|
||||
|
||||
## Steering
|
||||
|
||||
Current status (2026-07-02): `harness/run_queue/` is still live. OpenHuman
|
||||
drains its own `Steer`/`Collect` lanes and forwards them as TinyAgents
|
||||
`SteeringCommand::InjectMessage`; TinyAgents drains those `SteeringHandle`
|
||||
commands at loop checkpoints. The queue is also the product adapter for
|
||||
web-channel `followup` and `parallel` semantics and for detached sub-agent
|
||||
steer/collect lookup through `running_subagents`. The local
|
||||
`openhuman::tinyagents::orchestration` seam now re-exports `SteeringRegistry`,
|
||||
`TaskId`, `SteeringCommand`, `SteeringPolicy`, and `SteeringHandle`, and
|
||||
sub-agent TinyAgents runs register their live `SteeringHandle` in a
|
||||
process-local `SteeringRegistry` while the run is active. Detached
|
||||
`steer`/`collect` controls now prefer that registry handle and fall back to
|
||||
`RunQueue` when no handle is available. The shared OpenHuman turn seam installs
|
||||
a local steering policy that accepts only `InjectMessage` and `Pause` until a
|
||||
product control surface owns additional crate command kinds. Do not delete the
|
||||
directory until the remaining detached control modes and the web-channel
|
||||
followup/parallel lanes either have TinyAgents-owned equivalents or move into
|
||||
local owners. Hard cancel/prune paths also deregister task handles so aborted
|
||||
runs do not leave stale registry entries.
|
||||
The unused `DomainEvent::RunQueueMessageDelivered` projection has been removed;
|
||||
queued/interrupt/followup events remain live.
|
||||
|
||||
1. Map product tools onto crate commands: `steer_subagent` →
|
||||
`SteeringCommand::InjectMessage` via the registered `SteeringRegistry` is
|
||||
live for active TinyAgents sub-agent runs, with `RunQueue` fallback. Next:
|
||||
map redirect/pause/resume/cancel modes to corresponding variants while
|
||||
preserving delivery-at-safe-boundaries semantics (crate drains before each
|
||||
model call).
|
||||
2. Tighten policy by run class: the shared turn seam now installs an
|
||||
`InjectMessage`/`Pause` allowlist; future background-only runs can accept
|
||||
Cancel without also accepting transcript injection.
|
||||
3. Accepted/rejected steering emits `AgentEvent::Steered`; the bridge now logs
|
||||
accepted/rejected command kinds, and UI projection remains pending. Delete
|
||||
bespoke acknowledgment plumbing in `run_queue/` after parity.
|
||||
4. `harness/run_queue/` (317 total; 174 non-test): first split it by ownership.
|
||||
Detached sub-agent `Steer`/`Collect` should collapse to a `SteeringRegistry`
|
||||
lookup/registration path; web-channel `Followup`/`Parallel` remain product
|
||||
turn orchestration unless a crate-owned replacement is introduced.
|
||||
|
||||
## Recursion
|
||||
|
||||
5. One cap: crate `RunLimits.max_depth`/`RecursionPolicy` becomes
|
||||
authoritative; crate-internal `spawn_depth_context.rs` (66) becomes a
|
||||
reader/projector of crate depth (`ToolExecutionContext.depth`) for product
|
||||
error wording, or is deleted if the wording can wrap
|
||||
`TinyAgentsError::SubAgentDepth`. Current code now threads
|
||||
`MAX_SPAWN_DEPTH = 3` into TinyAgents `RunPolicy.limits.max_depth`,
|
||||
`RunConfig.max_depth`, and MCP `agent.run_subagent` via
|
||||
`mcp_server/subagent_depth.rs`, but OpenHuman's own
|
||||
`spawn_depth_context` still rejects beyond the same cap before the
|
||||
TinyAgents run.
|
||||
6. One error shape: `SubAgentDepth`/`RecursionLimit` now preserve the existing
|
||||
`SpawnDepthExceeded` error surface before the subagent graph wraps provider
|
||||
failures.
|
||||
|
||||
## Deletions
|
||||
|
||||
- `harness/run_queue/` queue mechanics, only after detached steer/collect no
|
||||
longer use `Arc<RunQueue>` and web-channel followup/parallel behavior has a
|
||||
replacement or local owner.
|
||||
- crate-internal `harness/spawn_depth_context.rs` (if step 5 wording-wrap
|
||||
suffices).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Mid-flight steer lands only at loop boundaries (existing steering tests);
|
||||
depth-exceeded produces the same user-facing error as today.
|
||||
@@ -1,28 +0,0 @@
|
||||
# 07 — Sub-agents, steering, recursion
|
||||
|
||||
Re-express the sub-agent pipeline on crate primitives; collapse the
|
||||
detached-run registry onto durable task stores; one recursion authority.
|
||||
|
||||
Target SDK surface: `SubAgent::invoke_in_parent` (lineage/depth/event
|
||||
inheritance), `SubAgentSession` (multi-turn reuse), `SubAgentTool` (depth
|
||||
guard), `graph::subagent_node` + `SubAgentPolicy`/`SubAgentBudget`,
|
||||
`graph::subgraph` (`shared_subgraph_node`/`adapter_subgraph_node`),
|
||||
`TaskStore`/`JsonlTaskStore` + 10 `orchestrate_*` tools +
|
||||
`SteeringRegistry`, `SteeringCommand`/`SteeringHandle`/`SteeringPolicy`,
|
||||
`RecursionPolicy`/`RecursionStack`, `CancellationToken`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-subagent-pipeline.md` — build pipeline as a subgraph.
|
||||
2. `02-detached-taskstore.md` — running_subagents onto durable TaskStore.
|
||||
3. `03-steering-recursion.md` — steering tools + one recursion cap.
|
||||
|
||||
Keep (product): agent definition lookup, allowlist enforcement, archetype
|
||||
prompt assembly, memory context, sandbox/action-root narrowing,
|
||||
worker-thread mirroring, integrations preflight, handoff cache. These become
|
||||
named nodes/adapters, not deleted.
|
||||
|
||||
NOTE (memory, P5 2026-06-30): wrapping crate `SubAgentTool` around a bare
|
||||
`Arc<AgentHarness>` was DECLINED because it discarded the pipeline. The
|
||||
subgraph approach keeps the pipeline as explicit nodes — that's the
|
||||
difference that makes it viable now.
|
||||
@@ -1,48 +0,0 @@
|
||||
# 08.1 — `run_parallel_fanout` → `graph::parallel::map_reduce`
|
||||
|
||||
The crate now has the ordered/bounded map-reduce helper the OpenHuman
|
||||
helper was written to fill (`ParallelOptions`, `FailurePolicy`,
|
||||
per-item `ItemOutcome`, cancellation).
|
||||
|
||||
Current status (2026-07-02): landed. Live fanout callers now invoke
|
||||
`tinyagents::graph::parallel::map_reduce` directly: `spawn_parallel_agents`,
|
||||
workflow phase fanout (`workflow_runs/engine.rs`), and model_council fanout.
|
||||
`tinyagents/orchestration.rs::run_parallel_fanout` has been removed. The
|
||||
remaining TaskStore re-export seam is crate-internal for detached-subagent
|
||||
lifecycle bookkeeping. Workflow phase fanout now shares the durable workflow
|
||||
stop signal with `ParallelOptions::with_cancellation`, so SDK cancellation maps
|
||||
back to the existing `Interrupted` run state. Usage-rollup parity has direct
|
||||
coverage in `turn_subagent_usage::map_reduce_fanout_preserves_scope`: TinyAgents
|
||||
1.3.0 `map_reduce` drives bounded futures through `buffer_unordered` and
|
||||
reorders in the same async call path, so OpenHuman's tokio task-local
|
||||
parent-turn collector stays visible to inline `spawn_parallel_agents` workers.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done: compare `tinyagents/orchestration.rs::run_parallel_fanout` semantics
|
||||
(input-order results, bounded concurrency, take-once payload cell,
|
||||
graph events) with `map_reduce`.
|
||||
2. Done: re-point callers: `spawn_parallel_agents`, workflow phase intra-phase
|
||||
fanout (`workflow_runs/engine.rs`), model_council fanout.
|
||||
3. Done: preserve the task-local usage-collector behavior. The direct
|
||||
`turn_subagent_usage::map_reduce_fanout_preserves_scope` regression proves
|
||||
records made inside `map_reduce` workers still roll into the active parent
|
||||
turn collector. The longer-term 06-cost work can replace this task-local
|
||||
bridge with `UsageTotals`/`ChildRun` lineage once lineage parity lands.
|
||||
4. Done: choose `FailurePolicy` per caller (council = collect-all; workflow
|
||||
phase = fail-fast or per-phase config). Use the 1.3.0 options:
|
||||
`with_item_timeout`/`with_total_timeout`/`with_cancellation`
|
||||
(a per-item timeout is behavior the old helper never had — set it
|
||||
deliberately per caller, not by default). Workflow phase fanout now uses
|
||||
`with_cancellation`; timeout policy remains unset by design.
|
||||
|
||||
## Deletions
|
||||
|
||||
- `run_parallel_fanout` + the move-once cell machinery in
|
||||
`tinyagents/orchestration.rs` (file keeps the TaskStore re-exports until
|
||||
07.2 relocates them).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Deterministic input-order results with out-of-order completion; task-local
|
||||
usage rollup parity on a fanout turn.
|
||||
@@ -1,75 +0,0 @@
|
||||
# 08.2 — `spawn_parallel_agents` as a graph tool
|
||||
|
||||
The spec's Phase 6 node shape is now live on SDK graph helpers; remaining work
|
||||
is parity evidence and cancellation coverage.
|
||||
|
||||
Current status (2026-07-02): `spawn_parallel_agents` already fans prepared
|
||||
workers through `tinyagents::graph::parallel::map_reduce`, exports the fixed
|
||||
`validate -> dispatch -> worker -> collect -> finalize` topology, and the graph
|
||||
entrypoint now owns `Config.action_dir` resolution for worktree-isolated workers.
|
||||
Each result now carries explicit per-task lineage (`parentSession`,
|
||||
`rootSession`, `childTaskId`) so graph/status consumers can connect child task
|
||||
ids back to the root run. The graph now owns the live
|
||||
`validate`/`dispatch`/`worker`/`collect`/`finalize`
|
||||
phases: the graph entrypoint resolves parent context and registry state,
|
||||
validate parses task requests and enforces the parent `max_parallel_tools`
|
||||
limit, dispatch validates/preflights workers from an owned agent-definition
|
||||
snapshot, and dispatch now rejects shared-workspace workers that can use
|
||||
write/execute tools unless the target definition is `sandbox_mode = "read_only"`,
|
||||
the task explicitly requests `isolation = "worktree"`, or the task provides
|
||||
non-overlapping `files:` ownership for the shared-workspace serial fallback.
|
||||
Worker fanout still uses the SDK `map_reduce` helper for parallel-safe batches,
|
||||
shared write fallback runs the prepared batch in deterministic task order and
|
||||
now checks the graph cancellation token between serial workers, collect still
|
||||
projects compatibility `DomainEvent`/`AgentProgress`, and finalize still
|
||||
returns the existing JSON shape. Parallel batches pass the same token into SDK
|
||||
`ParallelOptions::with_cancellation`, so cancelled map-reduce runs surface as a
|
||||
graph `Cancelled` outcome instead of an opaque fanout error. The tool wrapper
|
||||
now inherits the live TinyAgents run cancellation token through OpenHuman's
|
||||
scoped harness context and passes it into the graph entrypoint, falling back to a
|
||||
local token only for direct/manual tool calls outside a run. The tool wrapper
|
||||
still owns `ToolResult` translation so
|
||||
malformed-argument and public error shapes stay unchanged. The unused pre-graph
|
||||
public wrappers have been removed, internal graph helpers have been narrowed,
|
||||
and the remaining shrink target is the 1280-line graph implementation.
|
||||
|
||||
## Nodes
|
||||
|
||||
- `validate`: parse tasks, enforce min/max count, and preserve malformed-args
|
||||
error ordering before parent-context lookup.
|
||||
- `dispatch`: resolve per-task agent definitions, `subagents.allowlist`,
|
||||
toolkit requirements, ownership prompt boundaries, and worktree preflight.
|
||||
- `worker`: the 07.1 subagent pipeline subgraph with inherited policy,
|
||||
optional `worktree_action_dir`, child task id, bounded budgets
|
||||
(`SubAgentBudget`).
|
||||
- `collect` (reducer/`ChannelSet`): successes, failures, elapsed,
|
||||
iterations, worktree status, changed files, stale-read markers —
|
||||
deterministic task order.
|
||||
- `finalize`: compatibility `DomainEvent`/`AgentProgress` projections,
|
||||
overlap warnings, existing `parallel_agents` JSON shape.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done: `build_spawn_parallel_graph` + topology export (established pattern).
|
||||
2. Done: tool wrapper in `agent_orchestration/tools/spawn_parallel_agents.rs` is now
|
||||
a thin shell: schema → run graph → translate `ToolResult`.
|
||||
3. Partially done: policy (spec "ownership and scheduling"): read-only workers
|
||||
may share workspace; worktree-isolated writers stay on `map_reduce`;
|
||||
shared-workspace writers with disjoint `files:` ownership are admitted but
|
||||
force a deterministic serial batch fallback; shared writers without
|
||||
parseable/disjoint ownership are rejected. Cancellation now has a graph-level
|
||||
token seam, named-node boundary checks, serial fallback checks between
|
||||
workers, SDK map-reduce cancellation wiring, and live parent run cancellation
|
||||
inheritance through the tool path. No widening of tools/model/sandbox/budget.
|
||||
4. Checkpointing optional (fanout = Async durability).
|
||||
|
||||
## Deletions
|
||||
|
||||
- Orchestration mechanics inside `spawn_parallel_agents.rs` have moved into
|
||||
`agent_orchestration/spawn_parallel_graph.rs`; overlap detection lives in the
|
||||
collect/finalize graph path.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Required before completion: spawn_parallel suite green against identical JSON output;
|
||||
status consumers render per-task lineage.
|
||||
@@ -1,27 +0,0 @@
|
||||
# 08.3 — Approvals as durable graph interrupts
|
||||
|
||||
Today approval pauses are steering-channel pauses (park the turn) and the
|
||||
approval gate parks interactive chat turns with a 10-min TTL.
|
||||
|
||||
## Steps
|
||||
|
||||
1. For durable graphs (delegation review gate, workflow human-review
|
||||
phases): emit `NodeResult::Interrupt { id, node, payload }` with the
|
||||
approval request as payload; persist via checkpointer (Sync durability).
|
||||
2. Resume path: approval RPC decision → `Command { resume: Some(decision) }`
|
||||
at the stored `ResumeTarget`; process restart between request and
|
||||
decision must survive (that's the point).
|
||||
3. Keep the interactive chat-turn approval gate as-is (steering pause) —
|
||||
chat turns are not durable graphs; document the boundary here.
|
||||
4. Surface pending interrupts in command center (`GraphRunStatus` +
|
||||
interrupt records); TTL expiry → resume-with-deny.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Bespoke pause/park bookkeeping in workflow/delegation paths where the
|
||||
interrupt record replaces it.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- e2e: delegation review pause → restart process → approve → run resumes
|
||||
from exact checkpoint; deny/TTL paths covered.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 08.4 — Per-agent graphs: stubs out, bespoke in
|
||||
|
||||
Default-only `agent_registry/agents/*/graph.rs` stubs return
|
||||
`AgentGraph::Default` (~13 lines each). Delete the boilerplate; keep the
|
||||
seam; land at least one real custom graph before declaring the migration
|
||||
complete so the cleanup does not remove an unproven seam.
|
||||
|
||||
Current status (2026-07-02): `BuiltinAgent.graph_fn` is optional and the
|
||||
loader supplies `AgentGraph::Default` when it is absent. All 32
|
||||
`agent_registry/agents/*/graph.rs` default stubs are gone, along with the five
|
||||
default-only non-registry graph modules (`tinyplace_agent`, skill setup,
|
||||
skill executor, agent memory, subconscious). `researcher` is now the first
|
||||
bespoke production per-agent graph: it resolves to `AgentGraph::Custom`, runs a
|
||||
compiled `route_research -> run_research_turn -> finalize` topology, exports
|
||||
that topology through `agent.graph_topologies`, and delegates the actual
|
||||
model/tool loop to the shared default sub-agent leaf so transcript persistence,
|
||||
progress, handoff, cap-summary, and usage-rollup parity stay intact. Other
|
||||
production orchestration graph topologies (delegation/workflow/team/
|
||||
spawn-parallel) also exist.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Done: land the first bespoke per-agent graph. `researcher/graph.rs` is an
|
||||
`AgentGraph::Custom` runner over a compiled graph with topology export.
|
||||
Remaining route sophistication: split search/read/synthesize/cite decisions
|
||||
into richer nodes once those policies move out of prompt text.
|
||||
2. Done: make `graph_fn` optional on `BuiltinAgent` (default =
|
||||
`AgentGraph::Default` supplied by the loader/registry); delete every default
|
||||
stub `graph.rs` whose agent has no custom graph.
|
||||
3. Done: `agent.graph_topologies` returns an `agents` array showing which graph
|
||||
(`default`/`custom`) each built-in resolves to.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Deleted: all default-only `agent_registry/agents/*/graph.rs` files and the
|
||||
`graph_fn` boilerplate wiring for them.
|
||||
- Deleted: default-only graph modules for `tinyplace_agent`, `skill_setup`,
|
||||
`skill_executor`, `agent_memory`, and `subconscious`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Registry loads all agents with correct graph resolution (test);
|
||||
first bespoke graph is in production and exported; richer route tests +
|
||||
topology snapshot remain before declaring 08.4 complete.
|
||||
@@ -1,97 +0,0 @@
|
||||
# 08.5 — Worktrees behind `WorkspaceIsolation`
|
||||
|
||||
The crate has the isolation seam the sdk-gaps doc asked for:
|
||||
`WorkspaceIsolation` trait, `WorkspaceDescriptor { root, trusted_roots,
|
||||
policy_id, sandbox }` (+ `allows(path)`), `ToolExecutionContext.workspace`,
|
||||
and `WorkspacePrepared/Violation/Cleanup` events.
|
||||
|
||||
Current status (2026-07-02): do not delete
|
||||
`src/openhuman/agent/harness/worktree_context.rs` yet. `spawn_parallel_agents`
|
||||
still threads `worktree_action_dir` into the sub-agent runner as a fallback,
|
||||
async subagent session roots now prefer `ToolExecutionContext.workspace`, and
|
||||
shell/git acting tools read the task-local `current_action_dir_override()` to
|
||||
execute inside the isolated checkout when no descriptor is present. The module
|
||||
is now hidden behind crate-internal harness re-exports. Its task-local carrier
|
||||
and accessor helpers are no longer public beyond the crate.
|
||||
`agent_orchestration::worktree::GitWorktreeIsolation` now implements the
|
||||
TinyAgents `WorkspaceIsolation` trait over OpenHuman's existing git-worktree
|
||||
create/remove policy and returns `WorkspaceDescriptor` roots for isolated
|
||||
workers. Sub-agent run options now carry an optional `WorkspaceDescriptor`, and
|
||||
the TinyAgents shared turn seam attaches it to `RunContext::with_workspace`.
|
||||
For current `spawn_parallel_agents` worktree workers, dispatch now calls
|
||||
`GitWorktreeIsolation::prepare` and carries the returned `WorkspaceDescriptor`
|
||||
into `SubagentRunOptions`; the old `worktree_action_dir` task-local remains as
|
||||
the live fallback until every acting tool reads the descriptor directly.
|
||||
`spawn_parallel_agents` also reads the parent `ToolExecutionContext.workspace`
|
||||
at the tool boundary, so shared workers inherit the caller workspace root and
|
||||
isolated fanout workers prepare relative to that root before falling back to
|
||||
`Config.action_dir`. `spawn_subagent` now preserves the same descriptor when it
|
||||
routes to reusable async delegation and when a blocking sub-agent run is
|
||||
requested, and `continue_subagent` passes the current caller descriptor into
|
||||
resumed checkpoint runs. `spawn_worker_thread` does the same for persisted
|
||||
worker-thread sub-agent runs. The archetype and integrations delegation tools
|
||||
now also forward the descriptor through their shared sub-agent dispatcher, and
|
||||
`agent_prepare_context` passes it into the read-only context scout when invoked
|
||||
as a tool. `call_memory_agent` and `delegate_to_personality` likewise carry the
|
||||
descriptor into their spawned sub-agents.
|
||||
The TinyAgents tool adapter now forwards `ToolExecutionContext` into OpenHuman
|
||||
tools, and shell/git plus core filesystem tools (`file_read`, `list`,
|
||||
`file_write`, `edit`, `apply_patch`, `grep`, `glob`, `csv_export`) and
|
||||
shell-family runtime tools (`node_exec`, `npm_exec`) use
|
||||
`ToolExecutionContext.workspace.root` as their effective action directory when
|
||||
present. Generated media tools (`media_generate_image`, `media_generate_video`)
|
||||
also persist artifacts under that descriptor root for isolated fanout workers.
|
||||
Codegraph tools (`codegraph_index`, `codegraph_search`) likewise resolve their
|
||||
repo boundary and index store from the descriptor root during TinyAgents runs.
|
||||
For shell and the runtime tools this also covers sandbox policy and
|
||||
sandbox cwd. Delete the legacy task-local only after the remaining acting tools
|
||||
also resolve roots from the carried crate `WorkspaceDescriptor`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Partially done: implement `WorkspaceIsolation` over
|
||||
`agent_orchestration/worktree.rs` (git-worktree create/cleanup, dirty-
|
||||
worktree safeguards stay OpenHuman policy). `spawn_parallel_agents` now uses
|
||||
the adapter's prepare path and passes the descriptor into the runner;
|
||||
cleanup remains a separate dirty-worktree policy slice.
|
||||
2. In progress: thread `WorkspaceDescriptor` into tool execution. TinyAgents
|
||||
owns the descriptor carrier, while acting tools resolve their allowed root
|
||||
from `ToolExecutionContext.workspace` instead of task-local
|
||||
`worktree_context.rs`/action-dir globals. The carrier is now threaded
|
||||
through sub-agent run options, `RunContext::with_workspace`, the OpenHuman
|
||||
tool adapter, `spawn_subagent`, `continue_subagent`, `spawn_worker_thread`,
|
||||
archetype/integrations delegation dispatch, `spawn_async_subagent`
|
||||
reuse/session roots, `spawn_parallel_agents` shared-worker roots,
|
||||
`agent_prepare_context`, `call_memory_agent`, `delegate_to_personality`,
|
||||
`delegate_graph`, shell, git operations, `file_read`, `list`, `file_write`,
|
||||
`edit`, `apply_patch`, `grep`, `glob`, `csv_export`, `read_diff`,
|
||||
`run_linter`, `run_tests`, `update_memory_md`, `node_exec`, `npm_exec`,
|
||||
`media_generate_image`, `media_generate_video`, `codegraph_index`, and
|
||||
`codegraph_search`.
|
||||
Remaining acting tools still need to read it. OpenHuman
|
||||
`SecurityPolicy` remains the enforcement authority — the descriptor is the
|
||||
carrier, not the policy.
|
||||
`ParentExecutionContext` now also carries the descriptor as an ambient
|
||||
fallback for internal/background fanout: the sub-agent runner scopes it into
|
||||
child turns, and `AgentOrchestrationSession::spawn_agent` inherits it while
|
||||
still letting explicit per-spawn descriptors win. Parent-context snapshots now
|
||||
preserve an ambient descriptor when one is active, and post-turn session
|
||||
memory extraction reuses the turn's parent snapshot instead of rebuilding a
|
||||
default-only context after the scope has ended.
|
||||
3. Emit `WorkspacePrepared/Violation/Cleanup` through the bridge; violations
|
||||
also feed the security audit trail. Use 1.3.0
|
||||
`WorkspaceDescriptor::enforce(path, events)` so the check and the
|
||||
violation event are one call.
|
||||
4. Sandbox descriptor: map `sandbox_mode` onto the descriptor's sandbox
|
||||
field so sandboxed runs are inspectable.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Later: `harness/worktree_context.rs` (74) task-local once shell/git and other
|
||||
acting tools read the descriptor; per-tool ad hoc root plumbing.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Parallel edit-capable workers run in isolated worktrees with descriptor-
|
||||
scoped tool roots; a path outside `allows()` is blocked AND evented;
|
||||
worktree tests (235) green.
|
||||
@@ -1,35 +0,0 @@
|
||||
# 08 — Orchestration & graphs
|
||||
|
||||
Finish graph adoption: durable interrupts for approvals, workspace isolation
|
||||
hooks, and the first per-agent `AgentGraph::Custom` runner. Parallel fanout,
|
||||
`spawn_parallel_agents`, and graph-stub deletion already run on SDK graph
|
||||
helpers / optional graph wiring.
|
||||
|
||||
Current status (2026-07-02): graph adapter internals such as the delegation
|
||||
state machine and production delegation glue stay crate-internal; product
|
||||
orchestration modules own the public RPC/tool surfaces and output shapes.
|
||||
|
||||
Target SDK surface: `graph::parallel::map_reduce` + `ParallelOptions`/
|
||||
`FailurePolicy`/`ItemOutcome`, `Send`/`Command`/reducers/`ChannelSet`,
|
||||
`Interrupt`/`ResumeTarget`/`Command::resume`, `WorkspaceIsolation`/
|
||||
`WorkspaceDescriptor` + workspace events, `GraphTopology` export,
|
||||
graph testkit.
|
||||
|
||||
Steps:
|
||||
|
||||
1. `01-map-reduce.md` — landed: `run_parallel_fanout` removed; callers use
|
||||
the SDK helper directly.
|
||||
2. `02-spawn-parallel-graph.md` — landed: spawn_parallel_agents as validate→
|
||||
dispatch→worker→collect→finalize.
|
||||
3. `03-interrupt-resume.md` — approval pauses as durable interrupts.
|
||||
4. `04-graph-stubs.md` — default stubs deleted; land the first per-agent
|
||||
bespoke `AgentGraph::Custom` runner.
|
||||
5. `05-worktree-isolation.md` — worktrees behind `WorkspaceIsolation`.
|
||||
|
||||
Done when: every long-running orchestration has named nodes, topology export,
|
||||
cancellation checks, the boilerplate stubs stay gone, and at least one
|
||||
production agent owns a bespoke custom graph; fanout/parallel code paths stay
|
||||
SDK-owned with deterministic order + failure policy.
|
||||
|
||||
Keep (product): workflow_runs/agent_teams/command_center product state + RPC
|
||||
shapes; delegate/orchestration tool output formats; worktree policy.
|
||||
@@ -1,48 +0,0 @@
|
||||
# 09 — Embeddings & retrieval
|
||||
|
||||
Adapt OpenHuman embeddings/memory retrieval to crate interfaces; OpenHuman
|
||||
stores stay authoritative.
|
||||
|
||||
Target SDK surface: `EmbeddingModel` trait, `VectorStore`, concrete `Retriever`,
|
||||
`ScoredDoc`, `cosine_similarity`, `MemoryScope`, `AgentEvent::
|
||||
{MemoryLoaded, MemorySaved}`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Adapter: implement crate `EmbeddingModel` over
|
||||
`embeddings/provider_trait.rs::EmbeddingProvider` (voyage/openai/cohere/
|
||||
ollama/cloud/noop impls unchanged underneath). Keep OpenHuman
|
||||
`factory.rs`/rate-limit/retry as construction policy.
|
||||
2. Retriever facade: adapt the memory search surface the harness uses
|
||||
(`harness/memory_context.rs` recall injection,
|
||||
`agent_memory::memory_loader`) to TinyAgents `EmbeddingModel`/`VectorStore`,
|
||||
using the concrete crate `Retriever` where applicable and returning
|
||||
`ScoredDoc`s; the harness loads retrieval context through the facade so
|
||||
retrieval becomes swappable and event-visible (`MemoryLoaded`).
|
||||
3. Memory source identity: preserve `metadata.path_scope` dedupe semantics
|
||||
(CLAUDE.md rule) in the facade mapping.
|
||||
4. Usage/cost: embedding calls emit crate `Usage` + catalog pricing (06.4).
|
||||
5. Done: the vestigial `agent/memory_loader.rs` facade and unwired
|
||||
`agent/tree_loader.rs` eager digest prefetch module were deleted; callers
|
||||
use `agent_memory::memory_loader` directly.
|
||||
6. Done: `embeddings::mod` no longer re-exports `memory_store::vectors`, and
|
||||
`memory_search::vector::store` was deleted. Vector storage now has one
|
||||
canonical owner (`memory_store::vectors`); `memory_search::vector` only owns
|
||||
MMR/diversity selection.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Done: `agent/memory_loader.rs` (tracked in `99-deletion-ledger.md`).
|
||||
- Done: `agent/tree_loader.rs` (tracked in `99-deletion-ledger.md`).
|
||||
- Done: `memory_search::vector::store` compatibility shim; direct callers import
|
||||
`memory_store::vectors::cosine_similarity`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Recall-injection parity on a scripted turn (same citations via
|
||||
`collect_recall_citations`); embedding usage appears in cost records.
|
||||
|
||||
Keep (product): memory stores, retrieval ranking policy,
|
||||
`memory_context_safety.rs` gating, archivist (orthogonal durable memory —
|
||||
assessed KEEP 2026-06-30d; optional later: re-hang as `after_agent`
|
||||
middleware without changing behavior).
|
||||
@@ -1,60 +0,0 @@
|
||||
# 10 — CapabilityRegistry projection
|
||||
|
||||
One policy-aware lookup over OpenHuman's many registries (agents, tools,
|
||||
MCP, models, graphs).
|
||||
|
||||
Current status (2026-07-02): `agent.graph_topologies` still exposes the
|
||||
structure-only graph snapshot as JSON-RPC, but the underlying
|
||||
`tinyagents/topology.rs` helper/report surface is now crate-internal. A full
|
||||
`CapabilityRegistry` projection remains pending.
|
||||
|
||||
Target SDK surface: `CapabilityRegistry` (`register_model`, `register_tool`,
|
||||
`register_graph_blueprint`, `register_agent`, `register_descriptor`, `alias`,
|
||||
`snapshot()`, `diagnostics()`, `names_including_aliases()`,
|
||||
`to_model_registry()`, `to_tool_registry()`, `capability_resolver()`),
|
||||
`ComponentId`/`ComponentKind`/`ComponentMetadata { id, kind, description, tags,
|
||||
aliases }`, `RegistrySnapshot` (`to_dot()`),
|
||||
`RegistryDiagnostic`/`DiagnosticSeverity`, `ModelCatalog`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Build-per-run (or cached) `CapabilityRegistry` projection in
|
||||
`assemble_turn_harness`: the effective per-turn model/provider profile from
|
||||
the assembled turn, tools from the turn's tool sets, graph descriptors from
|
||||
the current topology reports
|
||||
(delegation/scheduler/member/subagent-pipeline/spawn-parallel), agents from
|
||||
both the runtime `AgentDefinitionRegistry` (`agent/harness/definition.rs`)
|
||||
and the user-facing `openhuman::agent_registry` default/custom/config
|
||||
entries. Wider model route/catalog projection happens earlier from config
|
||||
and model routing, not inside `assemble_turn_harness` alone. `.rag`
|
||||
`Blueprint` registration comes later when OpenHuman has real blueprint
|
||||
artifacts; the current graph exports are structure snapshots, not compiled
|
||||
blueprints. `AgentHarness` does not yet expose a registry-replacement setter,
|
||||
so `to_model_registry()`/`to_tool_registry()` start as validation/projection
|
||||
helpers before they can replace the live `register_model`/`register_tool`
|
||||
blocks in `tinyagents/mod.rs`.
|
||||
2. Diagnostics fail-closed pre-dispatch: duplicate tool names across
|
||||
native/MCP/Composio/generated tools, unsafe aliases → registry
|
||||
diagnostic errors (today: duplicate handling is scattered across generated
|
||||
tools, MCP, and native registration instead of one SDK diagnostic stream).
|
||||
TinyAgents 1.5.0 is pinned and exposes `AliasBinding`, alias diagnostics,
|
||||
cross-kind name-reuse detection, and `ComponentKind::{Middleware,
|
||||
Checkpointer, TaskStore, Listener}`; OpenHuman still needs to project those
|
||||
SDK diagnostics into its runtime.
|
||||
3. Introspection RPC: extend the existing `agent.graph_topologies` surface or
|
||||
add a sibling RPC for `RegistrySnapshot` (JSON + DOT) so a UI/CLI can show
|
||||
every active component.
|
||||
4. This is also the enabler for `.rag` blueprints later (registry-bound
|
||||
capability resolution) — out of scope for this wave, note only.
|
||||
|
||||
## Deletions
|
||||
|
||||
- Hand-rolled duplicate-name checks scattered in tool assembly (moved to
|
||||
diagnostics). Do not delete the live registration glue in `tinyagents/mod.rs`
|
||||
until `AgentHarness` can consume the projected registry directly.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Duplicate tool name → typed failure before model dispatch (test);
|
||||
snapshot RPC lists models/tools/graphs/agents with metadata;
|
||||
adapter-inventory test re-pointed at the registry.
|
||||
@@ -1,93 +0,0 @@
|
||||
# 11 — Testing & conformance (run last, per user preference)
|
||||
|
||||
Tests are deferred to the end of each workstream slice, with this final
|
||||
consolidation pass.
|
||||
|
||||
## Parity matrix (crate route)
|
||||
|
||||
Chat turn, channel turn, sub-agent turn, unknown-tool recovery (RunPolicy),
|
||||
approval denial, streaming text + reasoning + tool-arg deltas, early-exit
|
||||
pause, model-call cap checkpoint, budget stop, fallback selection,
|
||||
compression at 90% window, cache hit/miss, steering inject/cancel,
|
||||
detached spawn→restart→wait, worktree-isolated parallel run.
|
||||
Extend `src/openhuman/tinyagents/tests.rs`, `tests/agent_harness_e2e.rs`, and
|
||||
`tests/agent_tool_loop_raw_coverage_e2e.rs`.
|
||||
|
||||
Current execution mode for this migration branch: tests are intentionally
|
||||
deferred while code/docs are still moving quickly. Use docs drift and cargo
|
||||
checks for small slices; run the behavioral suites in the final conformance
|
||||
pass before declaring a workstream done.
|
||||
|
||||
## Conformance pass result (2026-07-02)
|
||||
|
||||
The deferred suites were re-enabled and run end-to-end after the adapter-first
|
||||
migration slices landed. Outcome:
|
||||
|
||||
- The full test target **compiles** again (it had drifted out of compilation
|
||||
while tests were deferred — `execute_tool_call`, `context::guard`,
|
||||
`agent::cost`, `tree_loader` removals plus new
|
||||
`workspace_descriptor`/`deterministic_cacheable`/capability params and the
|
||||
`cache_creation_tokens`/`reasoning_tokens` `UsageInfo` fields).
|
||||
- **Lib suite: green** — `13700 passed; 0 failed; 29 ignored` under the
|
||||
canonical `RUST_MIN_STACK=16777216 … --test-threads=1` (the default
|
||||
test-thread stack overflows the deep sub-agent dispatch future; the project
|
||||
runner and CI pin the larger stack).
|
||||
- **All integration/e2e binaries: green single-threaded** (parity matrix in
|
||||
`agent_harness_e2e` / `agent_tool_loop_raw_coverage_e2e`, plus the raw
|
||||
coverage and `json_rpc_e2e` suites).
|
||||
- Two genuine regressions were caught and fixed during triage: (1)
|
||||
`context_window_for_model` routed through `cost::catalog::lookup` began
|
||||
shadowing precise pattern-table windows with rounded catalog rows — fixed
|
||||
with a boundary-aware substring match and corrected `gpt-4.1{,-mini}` rows to
|
||||
`1_047_576`; (2) the new same-family cross-route fallback masked terminal
|
||||
provider errors in single-error test doubles — the doubles now fail on every
|
||||
route so error propagation is still exercised.
|
||||
- Behavioral changes intentionally introduced by the migration were re-baselined
|
||||
in the assertions (unknown tools now recover through crate
|
||||
`UnknownToolPolicy::ReturnToolError` emitting `unknown tool \`X\`` and bound at
|
||||
the iteration cap rather than early-halting; multi-route model/middleware
|
||||
inventory counts; `MAX_SPAWN_DEPTH` unified to 3; `session_db` RPC count;
|
||||
`npm_exec` workspace-aware CWD).
|
||||
|
||||
Known **pre-existing** parallel-isolation flakes (green single-threaded, fail
|
||||
only under `cargo test` default multi-threading because they mutate shared
|
||||
process state — unrelated to this migration): `composio_set_api_key_validates_
|
||||
candidate_key_even_when_stored_key_exists`, `resolved_daily_request_limit_
|
||||
honors_env`, `resolve_sync_interval_honors_per_toolkit_env`,
|
||||
`openai_codex_models_url_includes_client_version_query`,
|
||||
`concurrent_acquire_grants_exactly_one_slot`, and the app_state/backend-
|
||||
validation + `run_subagent_surfaces_provider_errors_and_can_be_cancelled`
|
||||
timing tests. These need `serial_test`-style guards, tracked separately.
|
||||
|
||||
The mandatory deletion-ledger rows remain the follow-up: each gated deletion
|
||||
(`reliable.rs`, `ThinkingForwarder`, `tool_filter.rs`/`tool_prep.rs`,
|
||||
`worktree_context.rs`, `CostBudgetMiddleware`, `SqlRunLedgerCheckpointer`, the
|
||||
session cutover set) can now proceed one row at a time against this green
|
||||
baseline, re-running the relevant suite after each removal.
|
||||
|
||||
## Testkit adoption
|
||||
|
||||
Port legacy loop-wording assertions to crate testkit: `MockModel`
|
||||
(`with_responses`, `with_tool_call`, `call_count`), graph `assert_graph`/
|
||||
`GraphEventRecorder`/node doubles (`scripted_route_node`,
|
||||
`interrupting_node`, `subagent_fake_node`), `RecordingListener` for event
|
||||
assertions.
|
||||
|
||||
## Conformance
|
||||
|
||||
- Checkpointer: File vs Sqlite same interrupt/resume behavior (04.3).
|
||||
- TaskStore: InMemory vs Jsonl lifecycle/filter/restart parity (07.2).
|
||||
- Graph: `Send` fanout order, failure policy, recursion caps, resume
|
||||
(08.x); fuzz-style small-graph composition if time allows.
|
||||
|
||||
## Known debts to clear here
|
||||
|
||||
- Previously flaky detached-orchestrator e2e timeout/hang debt (see 07.2 step
|
||||
6): the tight 2s waits are now 15s, but re-run when tests are re-enabled
|
||||
before treating that path as proven.
|
||||
- `cost::init_global` is now idempotent/no-panic; keep future tests isolated
|
||||
around process-global state so they do not depend on execution order.
|
||||
- Coverage gate: ≥80% on changed lines applies when frontend/rust coverage
|
||||
lanes are triggered. Docs-only migration-plan markdown does not trigger the
|
||||
coverage lane, but code slices still need the normal changed-line coverage
|
||||
before PR completion.
|
||||
@@ -1,157 +0,0 @@
|
||||
# 99 — Deletion ledger (master list)
|
||||
|
||||
Hard-migration means these files GO. Each row names its precondition step.
|
||||
Rule: call-site search + parity coverage before every delete; tick rows as
|
||||
they land.
|
||||
|
||||
## Immediately deletable (dead/vestigial — no precondition beyond call-site search)
|
||||
|
||||
- [x] `agent/memory_loader.rs` (5-line facade) — 09
|
||||
- [x] `agent/tree_loader.rs` (210, unwired per #3170) — 09
|
||||
- [x] `harness/compaction/mod.rs` shim — 03.1
|
||||
|
||||
## Superseded by existing middlewares (verify + delete)
|
||||
|
||||
- [x] `harness/compaction/cache_align.rs` (200) — 03.1/03.2
|
||||
- [x] `tokenjuice::compact_tool_output` default-Full wrapper — 01.4
|
||||
- [x] `context/microcompact.rs` (269) — 03.1
|
||||
- [x] `context/pipeline.rs` (454) + `context/guard.rs` (236, keep stats structs) — 03.1
|
||||
- [x] `context/tool_result_budget.rs` (172) — 03.1
|
||||
- [x] `harness/payload_summarizer.rs` (490) — 01.4
|
||||
- [x] `tinyagents/middleware.rs::MicrocompactMiddleware` struct + impl (~46) —
|
||||
W2-microcompact (2026-07-03): upstreamed into the vendored crate as
|
||||
`tinyagents::harness::middleware::MicrocompactMiddleware`
|
||||
(`tinyhumansai/tinyagents@feat/microcompact-middleware`, gitlink bumped);
|
||||
OpenHuman now constructs the crate type with `CLEARED_PLACEHOLDER` and
|
||||
events off, so behavior is byte-identical. The in-house struct + impl are
|
||||
deleted; the retained OpenHuman tests assert parity against the crate type.
|
||||
Was the C3-corrected/C5 "extract-then-delete" item (the local microcompact
|
||||
was NOT 1.5.0-superseded; this PR did the extraction).
|
||||
|
||||
## Deletable after SDK-surface adoption
|
||||
|
||||
- [x] `UNKNOWN_TOOL_SENTINEL` + `UnknownToolRewriteMiddleware` — 01.2
|
||||
- [ ] crate-internal tool side-lookup in `tinyagents/middleware.rs` — 01.1
|
||||
(live overlays for args-aware effects/permissions, CLI/RPC scope, and
|
||||
generated runtime context until SDK policy metadata can represent them)
|
||||
- [ ] `harness/tool_filter.rs` mechanics (299) +
|
||||
`subagent_runner/tool_prep.rs` (344) — 01.3
|
||||
(live until middleware owns child/toolkit selection; `tool_prep.rs` also
|
||||
contains non-filter prompt helpers that must move first)
|
||||
- [ ] `ThinkingForwarder` — 02.3
|
||||
(streaming reasoning moved to `MessageDelta.reasoning`; tool-arg **argument**
|
||||
fragments moved to `ToolCallDelta`/`MessageDelta.tool_call` — `emit_tool_args`
|
||||
removed. Still live for the tool-call **start** marker `note_tool_call`
|
||||
(crate `ToolDelta` has no `tool_name`) and the non-streaming reasoning
|
||||
fallback; both must move before deletion)
|
||||
- [ ] `inference/provider/reliable.rs` (now 900, was 1215 + 1443 tests) — 02.2
|
||||
PARTIAL: the shared classifier/backoff exports (`is_non_retryable`,
|
||||
`is_rate_limited`, `is_upstream_unhealthy`, `parse_retry_after_ms`,
|
||||
`structured_http_4xx`, `compute_backoff`, etc.) + their unit tests were
|
||||
extracted to `inference/provider/error_classify.rs`; cross-module importers
|
||||
(model.rs, memory_tree, triage, config_rejection, channels) repointed;
|
||||
classifier + reliable + triage + tinyagents suites green (138 passed).
|
||||
RESIDUAL (the actual delete, deliberately NOT forced): `ReliableProvider`
|
||||
is the universal retry/backoff/**model-fallback**/API-key-rotation/
|
||||
**streaming-failover** wrapper applied to every provider in
|
||||
`provider_factory.rs` and used by non-turn callers (memory-tree, channels)
|
||||
that never enter the crate harness. The crate `FallbackPolicy` carries only
|
||||
the hardcoded tier map, not user `config.reliability.model_fallbacks`, so
|
||||
un-wrapping the turn path would silently drop user fallbacks, and a faithful
|
||||
non-turn replacement reconstitutes most of the struct (net-zero) — the only
|
||||
true removal is routing all non-turn provider calls through the crate
|
||||
harness (large, and unverifiable by the mock suite, which never exercises
|
||||
real transient-network retry). Its own header still says "do not delete
|
||||
yet." Gated on that re-architecture.
|
||||
- [x] `tinyagents/orchestration.rs::run_parallel_fanout` — 08.1
|
||||
- [x] `harness/engine/` (entire dir, 309) — 05.2
|
||||
- [ ] `agent/progress_tracing.rs` + tests (1338, if duplicate) — 05.2
|
||||
(live web-progress exporter until journal-backed projection reaches parity)
|
||||
- [ ] `harness/run_queue/` mechanics (317 total; 174 non-test) — 07.3
|
||||
(live adapter for detached steer/collect plus web followup/parallel;
|
||||
split before deleting)
|
||||
- [ ] crate-internal `harness/spawn_depth_context.rs` (66) — 07.3
|
||||
(live recursion guard for nested delegation)
|
||||
- [x] `harness/worktree_context.rs` (74) — 08.5
|
||||
(deleted: worktree-isolated workers now resolve CWD from the carried
|
||||
`WorkspaceDescriptor` on `ToolExecutionContext` via
|
||||
`effective_action_dir_for_context`; the subagent runner sets the descriptor
|
||||
on the worker `RunContext` instead of the task-local. Parity tests
|
||||
`shell_uses_workspace_descriptor_root_as_cwd` /
|
||||
`git_resolves_cwd_from_workspace_descriptor` assert descriptor→worktree
|
||||
root and no-descriptor→`security.action_dir`)
|
||||
- [x] 32 × `agent_registry/agents/*/graph.rs` stubs (~420) + five default-only
|
||||
non-registry graph modules — 08.4
|
||||
- [x] `tinyagents/checkpoint.rs` (`SqlRunLedgerCheckpointer`, 250) — 04.3
|
||||
(deleted: durable delegation graphs now checkpoint through the crate
|
||||
`SqliteCheckpointer` at a dedicated `{workspace}/graph_checkpoints.db`.
|
||||
Nothing outside the adapter read the old run-ledger `graph_checkpoints`
|
||||
table, so the DDL was removed and pre-swap rows simply expire — orphaned
|
||||
in-flight tasks are reconciled at boot per 07.2. Delegation + 08.3 durable
|
||||
interrupt/resume + session_db suites green: 18 + 43 passed)
|
||||
- [ ] crate-internal `CostBudgetMiddleware` (`tinyagents/middleware.rs`) +
|
||||
crate-internal `agent/harness/turn_subagent_usage.rs` (176) task-local — 06
|
||||
(live until crate budget/run-tree accounting avoids duplicate
|
||||
`UsageRecorded` and covers parent-turn rollups)
|
||||
W2-budget-dedupe (2026-07-03): dedupe guard landed — the event bridge now
|
||||
records a model call's `UsageRecorded` exactly once, keyed on the run-scoped
|
||||
iteration, so the observe-only crate `BudgetMiddleware`'s re-emit can't
|
||||
double-count (`observability::OpenhumanEventBridge::record_usage`, `[budget]`).
|
||||
Crate `BudgetMiddleware` installed OBSERVE-ONLY (empty `BudgetLimits`) at
|
||||
`tinyagents/mod.rs`; local `CostBudgetMiddleware` demoted to a
|
||||
divergence-logging shadow (`[budget_shadow]`, `after_agent`) but STILL
|
||||
authoritative for enforcement. Flip criteria (must ALL hold before deleting
|
||||
this row): (1) ≥ 500 parent+subagent turns with zero `[budget_shadow]`
|
||||
divergence; (2) crate pricing table wired for money budgets; (3) run-tree
|
||||
rollup via a shared `BudgetTracker` replacing the `turn_subagent_usage`
|
||||
task-local. See the flip-criteria comment at the `tinyagents/mod.rs`
|
||||
registration site.
|
||||
- [ ] `agent/dispatcher.rs` (609) + `harness/parse.rs` (833) legacy tool-call
|
||||
parsing — after XML/P-format transcripts read from the store and no
|
||||
live path parses provider text (04.2 + verify)
|
||||
(live compatibility shell for prompt dialect selection, history
|
||||
serialization, XML/P-format fallback parsing, checkpoint cleanup, native
|
||||
text fallback, and TinyAgents text-mode provider responses; trim
|
||||
unused/test-only parse helpers before full delete)
|
||||
|
||||
## Deletable after session-store cutover (04.2 phase 4)
|
||||
|
||||
> **04.2 phase 2 landed (W2-shadow-reads, 2026-07-03):** a store-backed shadow
|
||||
> reader runs beside the legacy transcript reader, normalizes both sides via
|
||||
> `session_import/convert.rs`, and logs `[session_shadow_read]` divergence
|
||||
> (compact, no-PII). Legacy stays authoritative; gated by
|
||||
> `agent.session_shadow_reads` (default OFF) + `OPENHUMAN_SESSION_SHADOW_READS`
|
||||
> kill switch. The rows below stay `[ ]` until the shadow logs are
|
||||
> divergence-clean across the fixture matrix and reads are flipped to the store
|
||||
> (phase 3), which is the precondition for these deletions.
|
||||
|
||||
- [ ] `session/transcript.rs` (1347) + tests (978)
|
||||
- [ ] `session/migration.rs` (373) + tests
|
||||
- [ ] `session/turn/session_io.rs` (391)
|
||||
- [ ] `session_db/{ops,store,schemas,types}.rs` generic parts (~1.6k)
|
||||
- [ ] `agent_orchestration/subagent_sessions/` (~650)
|
||||
- [ ] `src/openhuman/session_import/` (~1.7k) + RPC controller — one
|
||||
release after auto-import ships
|
||||
|
||||
## Shrink (not full delete)
|
||||
|
||||
- Deleted: `session/agent_tool_exec.rs` 471-line test-only parity shim — 01.4
|
||||
- `session/turn/tools.rs` 697 → parent-context/assembly glue — 01.3
|
||||
(dynamic delegation refresh and skill-event catalogue reconciliation remain
|
||||
live until middleware owns contextual selection)
|
||||
- `subagent_runner/ops/*` 2764 (+1827 companion tests) → graph nodes/tests — 07.1
|
||||
- `running_subagents.rs` 1250 → ≤~300 policy/executor glue — 07.2
|
||||
- `tools/spawn_parallel_agents.rs` is a thin tool shell; remaining shrink
|
||||
target is `agent_orchestration/spawn_parallel_graph.rs` graph mechanics
|
||||
(1280) — 08.2
|
||||
- `context/` → stats + product prompt state — 03
|
||||
- `cost/catalog.rs` (622) → catalog snapshot loader once config seeding, cost
|
||||
estimates, and `context_window_for_model` all read one catalog projection —
|
||||
02.4
|
||||
|
||||
## Never delete (product policy)
|
||||
|
||||
Prompts/`agent/prompts/`, agent registry definitions, security/approval
|
||||
semantics, credentials/factory/router names, triage, task board/dispatcher,
|
||||
archivist, memory stores, worktree policy, `host_runtime.rs`, multimodal
|
||||
policy, JSON-RPC shapes, `DomainEvent` until all subscribers move.
|
||||
@@ -1,103 +0,0 @@
|
||||
# C2b — Task board / todos onto `graph::todos` (parity note)
|
||||
|
||||
Status: **first slice landed** (branch `feat/tinyagents-c2-todos`). Adapter-first,
|
||||
shadow-only. Legacy stays authoritative; nothing here changes product behavior.
|
||||
|
||||
This note pairs with the CONTINUATION plan §C2 (step 3) and records what the
|
||||
crate `tinyagents::graph::todos` surface maps onto in OpenHuman, and what it does
|
||||
**not** — the residue that must stay in the product host after a future cutover.
|
||||
|
||||
## What landed in this slice
|
||||
|
||||
- `src/openhuman/todos/graph_shadow.rs` — the adapter:
|
||||
- Total, lossless status mapping OpenHuman ↔ crate (`map_status_to_crate` /
|
||||
`map_status_from_crate`) — the two `TaskCardStatus` enums share the same
|
||||
seven variants (`Todo`, `AwaitingApproval`, `Ready`, `InProgress`,
|
||||
`Blocked`, `Done`, `Rejected`).
|
||||
- `TaskBoardCard` field-by-field conversion (`to_crate_card`) — all metadata
|
||||
(objective, plan, assignedAgent, allowedTools, approvalMode,
|
||||
acceptanceCriteria, evidence, notes, blocker, sessionThreadId,
|
||||
sourceMetadata, order, updatedAt) preserved.
|
||||
- `spawn_mirror` — after every authoritative `todos::ops::save_cards` write of
|
||||
a `Thread` board, mirrors the persisted cards into a crate `FileStore`
|
||||
(`<workspace>/tinyagents_graph_store`) under namespace `graph.todos`. Fire-
|
||||
and-forget, log-only; a crate rejection (e.g. the single-`InProgress`
|
||||
invariant) is warn-logged as a **DIVERGENCE**.
|
||||
- `spawn_shadow_claim` — wired into `todos::ops::claim_card` (which is the
|
||||
single claim entry-point the dispatcher's two claim sites in
|
||||
`task_dispatcher/dispatch.rs` funnel through, plus RPC/reclaim callers). It
|
||||
seeds the crate board with the pre-claim snapshot and replays the crate
|
||||
`claim_card` CAS, warn-logging when the crate ok/err verdict disagrees with
|
||||
the authoritative legacy claim.
|
||||
|
||||
The legacy claim/save path is byte-for-byte preserved: `claim_card` was
|
||||
refactored to compute one ok/err verdict via an extracted `apply_claim` helper
|
||||
(so the shadow sees the same not-found / wrong-status / invariant outcomes), but
|
||||
the persisted result and all existing tests are unchanged.
|
||||
|
||||
## Crate `TodoTool` vs `agent/tools/todo.rs` — what maps
|
||||
|
||||
The crate ships a single multiplexer `TodoTool` (`op`-dispatched) that is a near
|
||||
drop-in for the OpenHuman `todo` tool. Shared surface:
|
||||
|
||||
| Concern | Crate `TodoTool` | OpenHuman `tools/todo.rs` | Match? |
|
||||
| --- | --- | --- | --- |
|
||||
| Dispatch style | single tool, `op` field | single tool, `op` field | yes |
|
||||
| Thread binding | `ToolExecutionContext::thread_id` (never an arg) | `thread_context::current_thread_id()` + `fork_context` parent | yes (both bind to current thread, never an arg) |
|
||||
| `add`/`edit`/`update_status`/`remove`/`replace`/`clear`/`list` | present | present | yes |
|
||||
| Optional card fields | objective/plan/assignedAgent/allowedTools/approvalMode/acceptanceCriteria/evidence/notes/blocker | same | yes |
|
||||
| Return shape | `{ threadId, cards, markdown }` | `{ threadId, cards, markdown }` | yes |
|
||||
| Status aliases | `parse_status` (pending→todo, approved→ready, …) | `ops::parse_status` (identical alias table) | yes |
|
||||
| Single-`InProgress` invariant | `enforce_single_in_progress` (hard error) | `enforce_single_in_progress` (identical) | yes |
|
||||
| `claim_card` CAS | `store::claim_card(expected, target)` | `ops::claim_card(expected, target)` | yes (identical semantics; proven by shadow tests) |
|
||||
|
||||
## What does NOT map (product residue — must stay in the host)
|
||||
|
||||
1. **Approval-gate coupling.** OpenHuman's `todo` tool stamps a default
|
||||
`approvalMode` by reading `config.autonomy.require_task_plan_approval`
|
||||
(`default_task_approval_mode`), and the dispatcher's
|
||||
`requires_plan_approval` + `TaskPlanAwaitingApproval` `DomainEvent` drive the
|
||||
interactive plan-review gate. The crate `TodoTool` has **no** config read and
|
||||
**no** approval-gate wiring — it exposes `decide_plan`/`revise_plan` state
|
||||
transitions only. The gate policy stays product.
|
||||
2. **`DomainEvent` emissions.** `ops::claim_card`/mutations emit
|
||||
`AgentProgress::TaskBoardUpdated` (via `fork_context` `on_progress`) and the
|
||||
dispatcher publishes `TaskPlanAwaitingApproval`. The crate store emits
|
||||
nothing. All event vocabulary stays product (ledger: keep).
|
||||
3. **RPC projection shapes.** `threads.task_board_*` and `openhuman.todos_*`
|
||||
(see `todos/schemas.rs`) are the wire contracts the kanban UI binds to
|
||||
(`app/src/services/api/todosApi.ts`, `USER_TASKS_THREAD_ID = "user-tasks"`).
|
||||
These are **unchanged** by this slice and, per §C2, become read-side
|
||||
projections over the crate store only at cutover — not now.
|
||||
4. **Scratch board.** OpenHuman has a thread-less in-memory `BoardLocation::Scratch`
|
||||
fallback (tool calls outside a chat thread). The crate board is always
|
||||
`(Store, thread_id)`, so scratch mutations have **no** crate mirror target —
|
||||
the shadow skips them (trace-logged).
|
||||
5. **Persistence substrate + timestamps.** Product persists RFC3339
|
||||
`updated_at` to `<workspace>/agent_task_boards/<hex(thread_id)>.json`; the
|
||||
crate uses epoch-millis strings in a `Store` namespace. The mirror does not
|
||||
reconcile timestamps (cosmetic). Card-id minting also differs
|
||||
(`task-<uuid>` product vs `task-<seq>` crate) but ids are passed through, so a
|
||||
persisted board round-trips.
|
||||
6. **Run lifecycle.** `todos/runs.rs` (run records, heartbeats, stale-reclaim)
|
||||
and `task_dispatcher/` executor mechanics (executor resolution, autonomous
|
||||
run, board write-back) are **not** part of the crate todos surface — they
|
||||
stay product and are the §C2 step-3 "runner node" work, tracked separately.
|
||||
|
||||
## Single-writer constraint
|
||||
|
||||
The crate `Store` has no compare-and-set, so ns `graph.todos` assumes a single
|
||||
writer. The core process is that single writer (both the mirror and the
|
||||
shadow-claim run in-core). Documented in the module header; honoured because all
|
||||
mutations funnel through `todos::ops`.
|
||||
|
||||
## Next (not in this slice)
|
||||
|
||||
- Flip the mirror from shadow to authoritative (crate store becomes the source
|
||||
of truth; legacy JSON becomes a projection or is retired).
|
||||
- Reimplement `threads.task_board_*` / `openhuman.todos_*` as projections over
|
||||
the crate store.
|
||||
- Replace the dispatcher claim/poll loop with the crate `claim_card` CAS + a
|
||||
graph runner node, keeping `DomainEvent` emission + channel bindings product.
|
||||
- Delete `task_board.rs` + todo CRUD mechanics + dispatcher executor mechanics
|
||||
(~3.2k + tests) once parity logs are clean.
|
||||
@@ -1,265 +0,0 @@
|
||||
# TinyAgents Migration — Continuation Plan (2026-07-03)
|
||||
|
||||
Status: supersedes the ordering in `README.md` for remaining work. Written
|
||||
after a ground-truth audit of `main` (post-#4249), a re-inventory of the
|
||||
TinyAgents crate (1.4.0 published, 1.5.0 tagged), and a critical
|
||||
re-evaluation of the `99-deletion-ledger.md` "Never delete" list.
|
||||
|
||||
## 1. Where we actually are (ground truth, main @ 2026-07-03)
|
||||
|
||||
The "finish TinyAgents harness migration" PR (#4249 → #4399) has landed.
|
||||
Per-workstream state:
|
||||
|
||||
| Workstream | State |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 00 baseline | Done (1.3.0 + sqlite, rusqlite 0.40) — **needs re-bump to 1.4/1.5** |
|
||||
| 01 tooling | Mostly done. Live: crate-internal tool side-lookup (01.1), `tool_filter.rs` 299 + `tool_prep.rs` 344 (01.3) |
|
||||
| 02 models | Mostly done. Live: `reliable.rs` 900 (gated on re-architecture), `ThinkingForwarder` residual seams |
|
||||
| 03 context/cache | Done. `context/` is 1.3k of product prompt/stats state |
|
||||
| 04 sessions | **Primary unfinished work.** Live path is 100% legacy: `transcript.rs` 1347, `migration.rs` 373, `session_io.rs` 463, `session_db/` 4.5k, `subagent_sessions/` 653, `session_import/` 1.9k. Crate `Store`/`AppendStore` used only by the write-only importer. 04.3 checkpointer swap IS done in code (`tinyagents/checkpoint.rs` deleted) — the 04.3 doc text is stale |
|
||||
| 05 events | 05.2 engine deletion done. `progress_tracing` (817 + 477 langfuse + 719 tests) still the live web-progress exporter; journal projection not at parity |
|
||||
| 06 cost | Not wired: no `BudgetMiddleware`/`BudgetLimits`/`RunTree` in the shared runner. Blocker: `UsageRecorded` de-duplication |
|
||||
| 07 subagents | Diagnostic-skeleton graph only; procedural runner still live. `running_subagents.rs` has **grown to 1931 lines** (target ≤300); `JsonlTaskStore` adopted for detached lifecycle records; `run_queue/` + `spawn_depth_context.rs` still live |
|
||||
| 08 orchestration | 08.1/08.2/08.4 done. Live: 08.3 durable interrupts for approvals, 08.5 `worktree_context.rs` fallback thread |
|
||||
| 09 embeddings | Done |
|
||||
| 10 registry | Pending — `CapabilityRegistry` unused in src/ |
|
||||
| 11 testing | Conformance pass green on 2026-07-02 baseline |
|
||||
|
||||
Crate APIs available but **unused** in src/: `JsonlTaskStore` (now partially
|
||||
adopted), `BudgetMiddleware`, `ContextualToolSelectionMiddleware` (only its
|
||||
shadow), `UnknownToolPolicy`, `CapabilityRegistry`.
|
||||
|
||||
## 2. Crate delta: 1.4.0 / 1.5.0 (the upgrade this plan targets)
|
||||
|
||||
- **1.4.0 (published 2026-07-02)**
|
||||
- `graph::goals` — durable per-thread `ThreadGoal`: completion contract,
|
||||
token budget, Active/Paused/BudgetLimited/Complete, `goal_gate_node`
|
||||
self-driving loop, `run_continuation_tick`, `note_user_turn`, model tools
|
||||
`goal_get/goal_set/goal_complete`, host `goal_pause/resume/clear`.
|
||||
Persists on harness `Store` ns `graph.goals`.
|
||||
- `graph::todos` — `TaskBoard` kanban (Todo→Ready→InProgress→Done, Blocked,
|
||||
AwaitingApproval), single-`InProgress` invariant, `claim_card` CAS,
|
||||
single multiplexer `TodoTool`. Persists ns `graph.todos`.
|
||||
- Graph resilience: `CompiledGraph::with_node_retry(RetryPolicy)`,
|
||||
opt-in backoff sleeping, failure-boundary checkpoints +
|
||||
`CompiledGraph::retry(thread)` resumable failures,
|
||||
`GraphEvent::NodeRetryScheduled`.
|
||||
- **1.5.0 (tagged 2026-07-03, not yet on crates.io)**
|
||||
- `harness::no_progress::NoProgressTracker` — extracted from OpenHuman
|
||||
PR #4389. Pure state machine: `record(step, &ToolAttempt) ->
|
||||
Continue/Nudge/Halt`; identical-failure ladder, varied-failure backstop,
|
||||
fast-trip on hard policy rejects.
|
||||
- Known crate limitation relevant here: `Store` has no compare-and-set, so
|
||||
goals/todos mutations must funnel through one process (fine — the core is
|
||||
the single writer).
|
||||
|
||||
## 3. Re-evaluated component verdicts (revises `99-deletion-ledger.md`)
|
||||
|
||||
The old "Never delete" list over-protects. Revised verdicts, ordered by
|
||||
reclaimable lines (non-test; tests roughly double each figure):
|
||||
|
||||
### Migrate to crate, then delete locally (upstream-extraction candidates)
|
||||
|
||||
Precedent: `NoProgressTracker` was extracted upstream from #4389 and shipped
|
||||
in 1.5.0. Same play for:
|
||||
|
||||
| Component | Lines (~generic) | Notes |
|
||||
| ------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `pformat.rs` + `dispatcher.rs` + `harness/parse.rs` | 1941 (~1900) | Tool-call dialect machinery (P-format encoder, permissive XML/JSON parser with key-drift recovery). Zero DomainEvent coupling. Delete gate: 04.2 read-cutover (no live path parses provider text) |
|
||||
| `multimodal.rs` | 1690 (~1550) | `[IMAGE:]`/`[FILE:]` marker → provider content blocks, mime allowlist, PDF extraction, fetch gating, truncation budget. Only the marker convention is product |
|
||||
| `progress_tracing.rs` + `langfuse.rs` | 1294 (~1200) | Crate already ships Langfuse exporters + journals. Delete gate: 05.3 journal-backed web-progress parity |
|
||||
| `tool_result_artifacts/` | 588 (~500) | Already built on crate `Store`; overflow-to-artifact is a generic harness concern. Product residue: PII scrub hook |
|
||||
| `hooks.rs` + `stop_hooks.rs` trait machinery | 543 (~450) | PostTurnHook/StopHook traits are pure harness; product hook bodies stay |
|
||||
| `host_runtime.rs` adapter core | 456 (~350) | Native/Docker `RuntimeAdapter` overlaps crate workspace isolation |
|
||||
| `ArgRecoveryMiddleware`, `RepeatedToolFailureMiddleware` core | ~300 | Latter becomes a thin driver over crate `NoProgressTracker` (1.5.0) |
|
||||
| `tool_filter.rs` fuzzy ranker | 299 (~250) | Generic ranking; Composio input types stay product |
|
||||
| `triage/` evaluator+routing+decision core | ~800 of 3779 | Generic "LLM triage node" (tiered fallback, cache, verdict parse). `envelope.rs`/`escalation.rs`/`events.rs` stay product (Composio, DomainEvents, agent ids) |
|
||||
|
||||
### Replace with crate features, then delete (no upstreaming needed)
|
||||
|
||||
| Component | Lines | Replaced by |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `thread_goals/` | 2185 | `graph::goals` (1.4.0). OpenHuman keeps RPC schemas + UI projection over the crate store |
|
||||
| `agent/task_board.rs` + `tools/todo.rs` mechanics | 604 + ~430 | `graph::todos` `TaskBoard` + `TodoTool` (1.4.0). RPC shapes (`threads.task_board_*`, `openhuman.todos_*`) become projections |
|
||||
| `task_dispatcher/` executor mechanics | ~450 of 1494 | `claim_card` CAS + graph runner node; DomainEvent emission + channel bindings stay |
|
||||
| 6 duplicate middlewares in `tinyagents/middleware.rs` (2448 total): `OpenHumanToolExposureShadow`, `CacheAlign`, `Microcompact`, `CostBudget`, `PromptCacheSegment` (partial), `ToolPolicyMiddleware` (local) | ~900 | Crate `ContextualToolSelectionMiddleware`, cache guard, compaction, `BudgetMiddleware`, `ToolAllowlistMiddleware`. All are self-acknowledged shadows/parity holds |
|
||||
| `spawn_depth_context.rs` | 92 | Crate `RecursionPolicy`/`RecursionStack` |
|
||||
| legacy session stack (04.2 phase 4) | ~9000 incl. tests | Crate `Store`/`AppendStore`/`StoreChatHistory` |
|
||||
|
||||
### Confirmed product — keep (ledger was right)
|
||||
|
||||
`archivist/` (memory/learning policy — only the hook-scheduling shell ~200
|
||||
lines is generic), `bus.rs`, `schemas.rs`, `error.rs`, `turn_origin.rs`,
|
||||
`memory_context.rs`, `debug/`, `library/`, `progress.rs` event vocabulary,
|
||||
prompts **content** (the section/render machinery ~600 lines is generic but
|
||||
decoupling is low-value), preference/`run_workflow`/`delegate_to_personality`
|
||||
tools, approval/`MemoryProtocol`/`CliRpcOnly`/`ToolOutcomeCapture`
|
||||
middlewares.
|
||||
|
||||
Cross-cutting gate for the coupled middle tier (session turn/builder, triage
|
||||
escalation, task dispatch): **DomainEvent subscribers and JSON-RPC shapes** —
|
||||
these must become projections before their hosts can shrink.
|
||||
|
||||
### Coverage sweep: harness files previously unreferenced by any plan doc (2026-07-03)
|
||||
|
||||
A basename grep of `agent/harness/**` against this plan folder found these
|
||||
non-test files with no verdict anywhere. Assigned now (tests follow parents):
|
||||
|
||||
| File | Lines | Verdict |
|
||||
| --- | --- | --- |
|
||||
| `subagent_runner/extract_tool.rs` | 612 | **[migrate/delete]** — generic progressive-disclosure Q&A over handoff-cached payloads; pairs with `handoff.rs`. Goes with the C5 overflow-to-artifact/handoff extraction |
|
||||
| `subagent_runner/handoff.rs` | 287 | **[migrate/delete]** — generic oversized-result handoff cache (crate tool-output/artifact overlap). C5 |
|
||||
| `subagent_runner/types.rs` | 232 | **[shrink]** — spawn options/outcome/error taxonomy largely maps to crate `SubAgentPolicy`/`OrchestrationTaskKind`. C6 |
|
||||
| `subagent_runner/autonomous.rs` | 29 | **[keep]** — approval-gating override policy for unattended skill runs |
|
||||
| `fork_context.rs` | 226 | **[delete-via-crate]** — task-local parent-context carrier working around the `Tool` trait; replace with `ToolExecutionContext`/`RunContext` fields (same play that deleted `worktree_context.rs`). C6 |
|
||||
| `sandbox_context.rs` | 94 | **[delete-via-crate]** — same task-local pattern (sandbox_mode carrier). C6 |
|
||||
| `task_recency_context.rs` | 106 | **[delete-via-crate]** — same pattern (Composio recency window). C6 |
|
||||
| `turn_attachments_context.rs` | 71 | **[delete-via-crate]** — same pattern (vision-attachment forwarding). C6 |
|
||||
| `session/turn_checkpoint.rs` | 105 | **[delete]** with the C1 session cutover (turn checkpointing rides crate checkpoints) |
|
||||
| `memory_protocol.rs` | 386 | **[keep]** — product memory-protocol state machine (#4116) behind `MemoryProtocolMiddleware` |
|
||||
| `builtin_definitions.rs` | 273 | **[keep]** — product agent-definition data facade |
|
||||
| `definition_loader.rs` | 295 | **[keep]** — product TOML definition loader |
|
||||
| `archivist/{hook_impl,recap,tree_ingest}.rs` | 592 | **[keep]** — archivist internals, verdict inherited from §3 |
|
||||
|
||||
The four task-local context carriers (`fork_context`, `sandbox_context`,
|
||||
`task_recency_context`, `turn_attachments_context`, ~500 lines) are one
|
||||
C6 work item: extend the crate execution context instead of task-locals.
|
||||
|
||||
## 4. Continuation workstreams (execution order)
|
||||
|
||||
Sized for `/goal` execution like the original folders; each step lands code +
|
||||
tests + deletions + a ledger tick.
|
||||
|
||||
### C0 — Crate bump to 1.4 (then 1.5 when published)
|
||||
|
||||
1. Bump both Cargo worlds to `tinyagents = "1.4"`; re-verify sqlite chain.
|
||||
2. When 1.5.0 publishes: bump again; rewrite `RepeatedToolFailureMiddleware`
|
||||
as a driver over `harness::no_progress::NoProgressTracker`; delete the
|
||||
in-house identical-failure ladder.
|
||||
3. Adopt `with_node_retry` + `CompiledGraph::retry` on the delegation and
|
||||
spawn-parallel graphs (replaces bespoke retry glue; complements 08.3).
|
||||
4. Update `00-baseline.md` "1.3.0 delta" → 1.4/1.5 delta; refresh
|
||||
`docs/tinyagents-sdk-gaps.md` (goals/todos and no-progress close two
|
||||
OpenHuman-convergence items).
|
||||
|
||||
### C1 — Sessions cutover (04.2 phases 2–4) — biggest single unlock
|
||||
|
||||
The importer (`session_import/`) already proves shape parity. Execute:
|
||||
|
||||
1. 04.1 live dual-writes (turns append `session.{stem}.messages` + descriptor
|
||||
upsert alongside legacy JSONL).
|
||||
2. Shadow reads + parity fixtures (11-fixture matrix), then flip reads.
|
||||
3. Retire: `transcript.rs` (1347+978), `migration.rs` (373+170),
|
||||
`session_io.rs` (463), `session_db/` generic parts (~1.6k),
|
||||
`subagent_sessions/` (653); `session_import/` one release later.
|
||||
4. Then (unblocked by "no live path parses provider text"): delete
|
||||
`dispatcher.rs` + `parse.rs` + `pformat.rs` (~1.9k + tests), after
|
||||
upstreaming the dialect machinery (C5) or accepting native-only.
|
||||
~11k lines total.
|
||||
5. Fix the stale 04.3 doc (checkpointer swap already landed; `checkpoint.rs`
|
||||
is gone).
|
||||
|
||||
### C2 — Thread goals + thread tasks onto `graph::goals`/`graph::todos`
|
||||
|
||||
1. Adapter: `thread_goals/store.rs`+`runtime.rs`+`continuation.rs` →
|
||||
crate `ThreadGoal` + `goal_gate_node` + `run_continuation_tick`; keep
|
||||
`thread_goals/schemas.rs` RPC shapes as projections; map
|
||||
`goal_get/set/complete` model tools + host pause/resume/clear.
|
||||
2. One-time migration of existing goal rows into ns `graph.goals`.
|
||||
3. `task_board.rs` → crate `TaskBoard`; `tools/todo.rs` → crate `TodoTool`;
|
||||
`task_dispatcher/` claim/poll loop → `claim_card` CAS + runner node.
|
||||
DomainEvent emissions + `threads.task_board_*`/`openhuman.todos_*` RPC
|
||||
become read-side projections of the crate store.
|
||||
4. Single-writer constraint honoured (core is the only mutator; document it).
|
||||
5. Delete: `thread_goals/{store,runtime,continuation}.rs` mechanics,
|
||||
`task_board.rs`, todo CRUD mechanics, dispatcher executor mechanics
|
||||
(~3.2k + tests).
|
||||
|
||||
### C3 — Middleware de-duplication (01.1/01.3/06 finish)
|
||||
|
||||
1. Flip `ContextualToolSelectionMiddleware` from shadow to owner (parity logs
|
||||
are already accumulating); delete `OpenHumanToolExposureShadowMiddleware`,
|
||||
then `tool_filter.rs` mechanics + `tool_prep.rs` selection half.
|
||||
2. De-duplicate `UsageRecorded` (single owner: crate event → bridge records
|
||||
once), then install crate `BudgetMiddleware`/`BudgetLimits`; delete local
|
||||
`CostBudgetMiddleware` + `turn_subagent_usage.rs` task-local (206) in
|
||||
favour of `RunTree` rollup.
|
||||
3. ~~Delete `CacheAlign` + `Microcompact`~~ CORRECTED by C3 execution
|
||||
(2026-07-03): `CacheAlign` deleted (warn-only, crate cache guard already
|
||||
installed — commit on `feat/tinyagents-c3-middleware-dedupe`).
|
||||
`Microcompact` is NOT crate-superseded — tinyagents 1.5.0 has no
|
||||
tool-result body-clearing equivalent and the local one is live on the
|
||||
session turn path — moved to the C5 upstream-extraction batch (extract a
|
||||
crate microcompact first, then delete locally).
|
||||
4. ~~Adopt `UnknownToolPolicy::Rewrite`~~ CORRECTED by C3 execution: the
|
||||
sentinel + `UnknownToolRewriteMiddleware` were already deleted in 01.2;
|
||||
live policy is `UnknownToolPolicy::ReturnToolError`, which preserves the
|
||||
attempted tool name + args (#4419 UX). Rewrite mode would regress (needs a
|
||||
catch-all target tool); rationale comment added at `run_policy_for`. Done —
|
||||
no further action.
|
||||
Net: `tinyagents/middleware.rs` 2448 → ~1500 (Microcompact stays until C5).
|
||||
|
||||
### C4 — Events/progress projection (05.3) then delete progress_tracing
|
||||
|
||||
1. Journal-backed web-progress projection (`HarnessEventJournal` +
|
||||
`HarnessStatusStore` + late-attach `replay_from`).
|
||||
2. Parity vs `SpanCollector` spans; then delete `progress_tracing.rs` +
|
||||
`progress_tracing/` (~2k incl. tests) — Langfuse rides crate exporters.
|
||||
3. DomainEvent/AgentProgress become projections (ledger final clause).
|
||||
|
||||
### C5 — Upstream extraction batch (tinyagents PRs, then local deletes)
|
||||
|
||||
In crate-repo PRs, mirroring the NoProgressTracker precedent, in value order:
|
||||
|
||||
1. `multimodal` attachment resolver (~1550) — marker convention stays here.
|
||||
2. Tool-call dialect layer (`pformat`/parser) (~1900) — enables C1 step 4 to
|
||||
be a pure delete.
|
||||
3. Overflow-to-artifact tool-result store (~500).
|
||||
4. PostTurnHook/StopHook trait machinery (~450); host runtime adapter (~350);
|
||||
fuzzy tool ranker (~250); ArgRecovery (~150); triage evaluator core
|
||||
(~800) as a generic "LLM triage node".
|
||||
Each: crate PR → bump → local adapter shrinks to product residue → delete.
|
||||
|
||||
### C6 — Subagents finish (07)
|
||||
|
||||
1. Absorb `ops/runner.rs` (1212) + `ops/graph.rs` (1039) into named pipeline
|
||||
nodes (07.1); procedural runner retired.
|
||||
2. `running_subagents.rs` 1931 → ≤300: status/tombstone persistence fully on
|
||||
`JsonlTaskStore` + `orchestrate_*` tools (07.2).
|
||||
3. Steering: crate `SteeringRegistry` owns lanes; split then delete
|
||||
`run_queue/` (317); `spawn_depth_context.rs` → `RecursionPolicy` (07.3).
|
||||
4. Replace the four task-local context carriers (`fork_context`,
|
||||
`sandbox_context`, `task_recency_context`, `turn_attachments_context`,
|
||||
~500 lines) with typed fields on the crate execution context (see §3
|
||||
coverage sweep); shrink `subagent_runner/types.rs` onto crate
|
||||
`SubAgentPolicy`/task types.
|
||||
|
||||
### C7 — Remaining gated items
|
||||
|
||||
- 08.3 durable approval interrupts (now easier with 1.4.0 resumable
|
||||
failures); 08.5 drop the `worktree_action_dir` fallback thread, delete
|
||||
`worktree_context.rs` remnant wiring.
|
||||
- 10 CapabilityRegistry projection + fail-closed diagnostics.
|
||||
- 02.2 `reliable.rs`: unchanged verdict — gated on routing non-turn provider
|
||||
calls through the crate harness; do not force.
|
||||
- `ThinkingForwarder`: delete when crate `ModelDelta` grows reasoning +
|
||||
tool-name-on-start (file upstream issue; sdk-gaps §3).
|
||||
|
||||
## 5. Expected reclaim
|
||||
|
||||
| Phase | Local lines deleted (incl. tests, rough) |
|
||||
| --------------------- | ---------------------------------------------------------------------------- |
|
||||
| C1 sessions + dialect | ~11,000 |
|
||||
| C2 goals/todos | ~4,000 |
|
||||
| C3 middleware dedupe | ~1,500 |
|
||||
| C4 progress tracing | ~2,000 |
|
||||
| C5 upstream batch | ~6,000 |
|
||||
| C6 subagents | ~4,500 |
|
||||
| Total | **~29,000** (of ~57k in `agent/` + ~11k adapter + ~9k session/goals modules) |
|
||||
|
||||
## 6. Rules (unchanged)
|
||||
|
||||
Approval/security/sandbox/credential boundaries inviolate; JSON-RPC contracts
|
||||
stable unless a migration note lands; adapter → proven parity → delete;
|
||||
explicit `git add <paths>`; verify branch before commit. Tests deferred per
|
||||
workstream slice with a final conformance pass (11).
|
||||
@@ -1,170 +0,0 @@
|
||||
# Handoff — TinyAgents migration wave 1 (2026-07-03)
|
||||
|
||||
Context file for picking this work up in a fresh session. Read together with
|
||||
[`CONTINUATION-2026-07.md`](CONTINUATION-2026-07.md) (the plan this wave
|
||||
executes) and [`99-deletion-ledger.md`](99-deletion-ledger.md).
|
||||
|
||||
## What this session did
|
||||
|
||||
1. **Audit** — ground-truth audit of post-#4249 main, TinyAgents crate
|
||||
re-inventory (1.4.0 published: `graph::goals`/`graph::todos`/graph
|
||||
resilience; 1.5.0: `NoProgressTracker` extracted from our #4389), and a
|
||||
critical re-review of the deletion ledger's "never delete" list. Product of
|
||||
that: `CONTINUATION-2026-07.md` (workstreams C0–C7, ~29k-line reclaim) with
|
||||
a coverage-sweep appendix for previously unreferenced harness files.
|
||||
2. **Vendored the SDK** — `vendor/tinyagents` git submodule pinned at tag
|
||||
`v1.5.0`; `[patch.crates-io] tinyagents = { path = ... }` in BOTH Cargo
|
||||
worlds (root `Cargo.toml`, `app/src-tauri/Cargo.toml`); Dockerfile now
|
||||
`COPY vendor/ vendor/`; release-production/staging docker jobs run a
|
||||
targeted `git submodule update --init vendor/tinyagents`. All other
|
||||
cargo-running CI jobs already checkout `submodules: recursive`; the mobile
|
||||
crates (`app/src-tauri-mobile`) do NOT depend on the core crate, so their
|
||||
`submodules: false` stays. Agents can now edit SDK source in-tree and PR
|
||||
upstream from the submodule (do this in C5).
|
||||
3. **Executed wave 1** via a 6-agent workflow (run id `wf_df08984c-139`,
|
||||
scripts under the session dir; all agents completed).
|
||||
4. `agent-diff-v0.58.7-to-HEAD.diff` at repo root (gitignored) — full diff of
|
||||
`src/openhuman/agent/` since the last release, for review.
|
||||
|
||||
## Branch map (local; base → children)
|
||||
|
||||
- **`feat/tinyagents-c0-15-baseline`** ← base for everything below; branched
|
||||
from `docs/tinyagents-migration-continuation` (which holds the plan docs and
|
||||
branched from upstream/main).
|
||||
- `c7f287380` bump tinyagents 1.5.0 (+ .diff gitignore)
|
||||
- `e2f010cba` vendor submodule + [patch.crates-io] both worlds
|
||||
- `4c2895801` RepeatedToolFailureMiddleware → crate `NoProgressTracker`
|
||||
driver (in-house identical/varied/hard-reject ladder deleted; Nudge→
|
||||
`SteeringCommand::Redirect`, Halt→`HaltSummarySlot`+Pause+reset; 25
|
||||
middleware tests green)
|
||||
- `fbafad2d3` CI docker submodule fixes
|
||||
- `dd63b3a66` `with_node_retry(RetryPolicy::default().with_max_attempts(1))`
|
||||
on delegation + spawn-parallel graphs (behavior-preserving seam; raise
|
||||
attempts later; backoff sleeping off)
|
||||
- `02be67b06` plan corrections from C3 (below)
|
||||
- **`feat/tinyagents-c1-dual-writes`** — 04.1 done. Live turns dual-write to
|
||||
`{workspace}/tinyagents_store/{kv,journal}` (`session.{stem}.messages` +
|
||||
descriptor upsert, via `session_import/convert.rs` normalization). New
|
||||
config flag `agent.session_dual_write` (serde default **ON**);
|
||||
`OPENHUMAN_SESSION_DUAL_WRITE` is a pure **kill switch** (can force off,
|
||||
never on), read per-turn. Parity test:
|
||||
`session_import::live_tests` (2 pass). Read path untouched.
|
||||
- **`feat/tinyagents-c2-goals`** — `thread_goals/crate_adapter.rs` dual-write
|
||||
mirror into ns `graph.goals` (keys byte-identical to the crate reader,
|
||||
proven by reading back via `tinyagents::graph::goals::store::get`);
|
||||
idempotent `migrate_legacy_goals_into_crate_store` (callable, NOT wired to
|
||||
boot); shadow tool surface flag-gated **OFF**. Legacy store authoritative.
|
||||
- **`feat/tinyagents-c2-todos`** — `todos/graph_shadow.rs` mirrors boards into
|
||||
ns `graph.todos` at `<workspace>/tinyagents_graph_store`; `claim_card` CAS
|
||||
shadow through the single refactored `todos::ops::claim_card` chokepoint
|
||||
(`apply_claim` helper), divergence warn-logged; parity note
|
||||
`C2b-todos-parity.md` (what maps vs product residue). Legacy authoritative.
|
||||
- **`feat/tinyagents-c3-middleware-dedupe`** — PARTIAL by design:
|
||||
`CacheAlignMiddleware` deleted (−147 lines; crate cache guard already
|
||||
installed). See "plan corrections" for the two refusals.
|
||||
- **`feat/tinyagents-c4-journals`** — restart-stable event ids
|
||||
(`EventSink::with_stream_id(run_id)` → `{run_id}-evt-{offset}`), run id
|
||||
minted once via `journal::mint_run_id`; `FileStatusStore` records thread_id
|
||||
(`HarnessRunStatus::with_thread`) so `list_by_thread` answers; late-attach
|
||||
replay test reconstructs from the store alone.
|
||||
|
||||
All slice branches were created FROM `feat/tinyagents-c0-15-baseline`
|
||||
(worktree HEADs were stale; agents rebased themselves correctly).
|
||||
|
||||
## Plan corrections discovered during execution (already committed, 02be67b06)
|
||||
|
||||
- **MicrocompactMiddleware is NOT crate-superseded** — tinyagents 1.5.0 has no
|
||||
tool-result body-clearing; local one is live at `turn/core.rs` (~:885,
|
||||
keep_recent=5). Moved to C5: extract into the crate first, then delete.
|
||||
- **Unknown-tool is already done** — sentinel + rewrite middleware deleted in
|
||||
01.2; live policy `UnknownToolPolicy::ReturnToolError` deliberately kept
|
||||
over `Rewrite` (preserves #4419 attempted-name UX; Rewrite needs a catch-all
|
||||
target tool). Rationale comment at `run_policy_for`.
|
||||
- C3's shadow-parity investigation: exposure-shadow parity logs exist but are
|
||||
not yet asserted divergence-free — flipping
|
||||
`ContextualToolSelectionMiddleware` to owner still needs a parity-log audit.
|
||||
|
||||
## Known debt / gotchas for the next session
|
||||
|
||||
- **Worktrees + submodule**: fresh worktrees leave `vendor/tinyagents` empty →
|
||||
cargo fails on the patch path. Always
|
||||
`git submodule update --init vendor/tinyagents` first.
|
||||
- Test invocation: `RUST_MIN_STACK=16777216 … --test-threads=1`
|
||||
(deep sub-agent futures overflow default stacks); `GGML_NATIVE=OFF` for
|
||||
cargo on Apple Silicon. Full suites were deliberately NOT run — targeted
|
||||
module tests only (user directive). Integration debt is listed per-slice in
|
||||
the workflow reports and belongs to the final conformance pass (plan §11).
|
||||
- C1's `RunContext.stores` session-KV registration has no in-run consumer
|
||||
until 04.2 (`StoreChatHistory` adoption was deferred as read-path work).
|
||||
- C4 follow-ups: replay RPC (`agent.run_events`) unexposed;
|
||||
parent/root run-id lineage still 05.2/05.3.
|
||||
- The five slice branches will conflict lightly in `tinyagents/middleware.rs`
|
||||
/ `mod.rs` when merged together — merge C0 first, then slices one at a
|
||||
time, resolving against C0's tree.
|
||||
|
||||
## Wave 1 (merged)
|
||||
|
||||
All five wave-1 slice branches were MERGED into
|
||||
`feat/tinyagents-c0-15-baseline` and pushed to PR **#4473**, which landed the
|
||||
entire wave-1 scope into `tinyhumansai/openhuman:main`.
|
||||
|
||||
## Wave 2 status (2026-07-04) — DONE, on `feat/tinyagents-wave2`
|
||||
|
||||
All four wave-2 slices are implemented and integrated onto ONE branch
|
||||
`feat/tinyagents-wave2` (off `upstream/main`), one focused commit each, and
|
||||
opened as a single combined PR. Adapter-first throughout: legacy authoritative,
|
||||
crate features shadow + log divergence, deletions gated on proven parity.
|
||||
|
||||
1. `W2-microcompact-upstream` — **done.** The generic
|
||||
`MicrocompactMiddleware` (caller-supplied placeholder, opt-in `Compressed`
|
||||
event, idempotent tool-body clearing) was implemented IN the vendored crate
|
||||
and pushed to `tinyhumansai/tinyagents@feat/microcompact-middleware`
|
||||
(commit `ac73382`) BEFORE the gitlink bump. OpenHuman swapped to the crate
|
||||
type (constructed with `CLEARED_PLACEHOLDER`, events off → byte-identical);
|
||||
local struct+impl deleted, OpenHuman tests retargeted as the parity contract.
|
||||
Crate tests: 5 green. Ledger row ticked.
|
||||
2. `W2-shadow-reads` — **done.** 04.2 phase 2 store-backed shadow reader beside
|
||||
the legacy transcript reader; `[session_shadow_read]` divergence logging;
|
||||
flag `agent.session_shadow_reads` (default OFF) + `OPENHUMAN_SESSION_SHADOW_READS`
|
||||
kill switch. Legacy authoritative. (Flip → reads-from-store is phase 3, the
|
||||
~9k-line deletion unlock.)
|
||||
3. `W2-budget-dedupe` — **done.** Event bridge records each model call's usage
|
||||
exactly once (dedupe guard keyed on the run-scoped iteration cursor); crate
|
||||
`BudgetMiddleware` installed observe-only (empty `BudgetLimits`); local
|
||||
`CostBudgetMiddleware` demoted to a `[budget_shadow]` divergence logger, still
|
||||
authoritative for enforcement. Three flip criteria documented at the
|
||||
`tinyagents/mod.rs` registration site + in the ledger.
|
||||
4. `W2-replay-rpc` — **done.** Read-only `openhuman.agent_run_events` (paged,
|
||||
`next_offset`, capped limit), `agent_run_status`, `agent_runs_active`
|
||||
controllers over the C4 journal/status seams; direct `AgentObservation`/
|
||||
`HarnessRunStatus` serde projection (no PII); registered via
|
||||
`src/core/all.rs` per the controller-migration checklist. (Resolves the
|
||||
"replay RPC unexposed" C4 follow-up.)
|
||||
|
||||
**Test note:** targeted crate tests (microcompact) ran green locally; the full
|
||||
core-crate test binary would not link locally under this box's memory ceiling
|
||||
(single-rustc codegen thrash), so `cargo check --lib --tests` is the local
|
||||
compile gate and CI runs the actual targeted + full suites + coverage.
|
||||
|
||||
## Merge / PR state
|
||||
|
||||
- `feat/tinyagents-wave2` pushed to `origin` (senamakel fork); ONE combined PR
|
||||
vs `upstream` (`--head senamakel:feat/tinyagents-wave2`): **PR #4483**
|
||||
(`tinyhumansai/openhuman#4483`).
|
||||
- Submodule branch `feat/microcompact-middleware` pushed to
|
||||
`tinyhumansai/tinyagents`; gitlink bumped to `ac73382`. Fresh worktrees must
|
||||
`git submodule update --init vendor/tinyagents`.
|
||||
- Git etiquette (user rules): push to `origin`, PR against `upstream` with
|
||||
`--head senamakel:<branch>`; explicit `git add <paths>`; never commit on main.
|
||||
|
||||
## Suggested next steps (wave 3)
|
||||
|
||||
1. Flip `session_shadow_reads` → reads-from-store once the fixture-matrix
|
||||
divergence logs are clean (biggest deletion unlock, ~9k incl.
|
||||
dispatcher/parse/pformat).
|
||||
2. Flip crate `BudgetMiddleware` → enforcing owner once the 3 documented
|
||||
criteria hold; delete local `CostBudgetMiddleware` + `turn_subagent_usage.rs`.
|
||||
3. Open a tinyagents PR for `feat/microcompact-middleware` (currently a pushed
|
||||
branch, not merged) so the gitlink can later track a tagged release.
|
||||
4. Continue C5 upstream extractions (multimodal resolver, dialect layer,
|
||||
overflow-to-artifact, hooks traits) + C2 shadow → authoritative flips.
|
||||
@@ -1,70 +0,0 @@
|
||||
# TinyAgents Full Migration Plan
|
||||
|
||||
Status: active plan (2026-07-02). Branch: `issue/4249-finish-tinyagents-migration`.
|
||||
|
||||
> **2026-07-03 update:** #4249 has landed on main. Remaining work is
|
||||
> re-planned in [`CONTINUATION-2026-07.md`](CONTINUATION-2026-07.md), which
|
||||
> supersedes the workstream ordering below and targets tinyagents 1.4/1.5
|
||||
> (`graph::goals`, `graph::todos`, `NoProgressTracker`, resumable graph
|
||||
> failures).
|
||||
>
|
||||
> **2026-07-04 update:** vendor-crate improvement work (reasoning tokens,
|
||||
> streaming, performance, native Anthropic provider, upstream extractions)
|
||||
> is planned separately in
|
||||
> [`../tinyagents-vendor-improvement-plan/`](../tinyagents-vendor-improvement-plan/)
|
||||
> (workstreams V1–V6, run in parallel with C0–C7; this folder's
|
||||
> `99-deletion-ledger.md` remains the master delete list).
|
||||
|
||||
Goal: **hard-migrate** OpenHuman's agent harness onto the `tinyagents` crate as
|
||||
the library for orchestration, caching, tooling, observability, model
|
||||
providers, context management, embeddings, sub-agents, steering, summarization,
|
||||
events, and session storage — then **delete the legacy OpenHuman files**.
|
||||
OpenHuman keeps only product policy (prompts, registry, security/approval
|
||||
semantics, credentials, UX projections).
|
||||
|
||||
Each workstream folder below is sized for `/goal` execution: the folder
|
||||
`README.md` states scope + done criteria; numbered step files are individual
|
||||
goals. Execute a step file end-to-end (code + tests + deletions + commit).
|
||||
|
||||
## Key facts superseding older docs
|
||||
|
||||
- Current crate is **1.5.0**; the plan still records the earlier "1.3.0 delta"
|
||||
in `00-baseline.md` where those primitives first became available.
|
||||
- `docs/tinyagents-sdk-gaps.md` was refreshed against TinyAgents 1.3.0 and
|
||||
should be re-audited after the 1.5.0 bump; it currently
|
||||
tracks only residual gaps. tinyagents 1.2.0-1.3.0 ships `UnknownToolPolicy`,
|
||||
`ToolPolicy` safety metadata + `ToolPolicyMiddleware`, reasoning deltas
|
||||
(`MessageDelta.reasoning`), durable `JsonlTaskStore` + orchestration tools,
|
||||
`graph::parallel::map_reduce`, `ModelCatalog` w/ pricing,
|
||||
`WorkspaceIsolation`, `BudgetMiddleware`, event journals/status stores,
|
||||
`CapabilityRegistry`, embeddings traits. See `00-baseline.md`.
|
||||
- Inventory of live/legacy files: `../tinyagents-harness-migration-audit.md`
|
||||
plus the per-folder deletion lists here (`99-deletion-ledger.md` is the
|
||||
master list).
|
||||
|
||||
## Workstreams (suggested order)
|
||||
|
||||
| # | Folder | Theme |
|
||||
|---|--------|-------|
|
||||
| 0 | `00-baseline.md` | crate bump, rusqlite 0.40, feature flags |
|
||||
| 1 | `01-tooling/` | ToolPolicy round-trip, unknown-tool, dynamic exposure, output budgets |
|
||||
| 2 | `02-models/` | ModelRegistry, profiles/catalog, fallback/retry, reasoning stream |
|
||||
| 3 | `03-context-cache/` | delete legacy context reducers, ResponseCache + prompt-cache guard |
|
||||
| 4 | `04-sessions/` | transcript → Store/AppendStore cutover, import phases 2–4 |
|
||||
| 5 | `05-events/` | journals/status canonical, bridge consolidation, DomainEvent projection |
|
||||
| 6 | `06-cost/` | UsageAccounting/Budget middleware, lineage rollup |
|
||||
| 7 | `07-subagents/` | SubAgent pipeline, detached TaskStore, steering/recursion |
|
||||
| 8 | `08-orchestration/` | spawn_parallel graph tool, map_reduce, interrupts, graph-stub cleanup |
|
||||
| 9 | `09-embeddings.md` | EmbeddingModel/VectorStore/Retriever adapters |
|
||||
| 10 | `10-registry.md` | CapabilityRegistry projection + diagnostics |
|
||||
| 11 | `11-testing.md` | parity matrix, testkit, conformance (last) |
|
||||
| 99 | `99-deletion-ledger.md` | master delete list with preconditions |
|
||||
|
||||
## Rules (unchanged from spec)
|
||||
|
||||
- Never bypass approval/security/sandbox/workspace/credential boundaries.
|
||||
- JSON-RPC contracts stay stable unless a migration note lands with the change.
|
||||
- Adapter first, flip ownership on proven parity, **then delete** — deletion is
|
||||
mandatory, not optional; every step file names its deletions.
|
||||
- Stage files explicitly (`git add <paths>`), never `git add -A` (see memory).
|
||||
- Verify `git branch --show-current` before each commit.
|
||||
@@ -1,453 +0,0 @@
|
||||
# TinyAgents Harness Migration Audit
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Scope: current `openhuman-5` checkout on branch `issue/4249-finish-tinyagents-migration`.
|
||||
|
||||
TinyAgents refresh: reviewed `tinyhumansai/tinyagents` `main` at
|
||||
`348a0e7dc71a1f9039f3d523a2a384661a7a9acd` after the current audit was first
|
||||
written. That repo is now materially ahead of the assumptions in the older
|
||||
OpenHuman migration docs: it has harness cache/store/session primitives,
|
||||
sub-agent reuse, graph subgraph/sub-agent nodes, lineage-aware events/status,
|
||||
and JSONL-backed append stores.
|
||||
|
||||
This is a documentation-only audit of how much of the OpenHuman agent harness is
|
||||
actually migrated to TinyAgents, and which remaining OpenHuman files are good
|
||||
candidates to port, collapse, or keep as product-specific adapters.
|
||||
|
||||
## Bottom Line
|
||||
|
||||
The core turn loop is migrated. The live chat turn, channel/CLI turn, and
|
||||
sub-agent turn all route through `src/openhuman/tinyagents::run_turn_via_tinyagents_shared`.
|
||||
That means the model/tool iteration loop, TinyAgents middleware stack, event
|
||||
bridge, stop hooks, context compression, unknown-tool recovery, and tool policy
|
||||
boundary are on the TinyAgents harness path.
|
||||
|
||||
The repository still has a large OpenHuman harness shell around that path:
|
||||
|
||||
| Area | Rust files | Lines | Current role |
|
||||
| ------------------------------------ | ---------: | -----: | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/openhuman/agent/harness/` | 83 | 30,521 | Agent session assembly, transcript compatibility, prompt/tool filtering, context product logic, sub-agent build pipeline, leftover generic loop seams |
|
||||
| `src/openhuman/tinyagents/` | 13 | 6,481 | TinyAgents adapters, middleware, event bridge, graph helpers, checkpoint adapter |
|
||||
| `src/openhuman/agent_orchestration/` | 58 | 21,704 | Product orchestration tools, durable workflow/team/session state, graph-backed fanout/delegation wrappers |
|
||||
|
||||
The migration should not be read as "OpenHuman can delete `agent/harness/` now."
|
||||
The correct read is: the execution core is on TinyAgents, but much of the
|
||||
surrounding runtime was preserved for product compatibility. After reviewing the
|
||||
current TinyAgents code, more of that surrounding runtime is now portable than
|
||||
this audit originally assumed: session transcript tracking, prompt/response
|
||||
cache handling, and sub-agent pipeline orchestration can be expressed through
|
||||
TinyAgents store/cache/subgraph/sub-agent-node primitives, with OpenHuman keeping
|
||||
only product policy and compatibility adapters.
|
||||
|
||||
## What Is Migrated
|
||||
|
||||
### Turn execution chokepoints
|
||||
|
||||
- `src/openhuman/agent/harness/session/turn/core.rs` routes the main chat turn
|
||||
through `super::graph::run_chat_turn_graph`, which is a thin wrapper over
|
||||
`run_turn_via_tinyagents_shared`.
|
||||
- `src/openhuman/agent/harness/graph.rs` routes channel/CLI turns through
|
||||
`run_turn_via_tinyagents_shared`.
|
||||
- `src/openhuman/agent/harness/subagent_runner/ops/graph.rs` routes sub-agent
|
||||
turns through `run_turn_via_tinyagents_shared`.
|
||||
- `src/openhuman/tinyagents/mod.rs` owns the shared harness assembly:
|
||||
OpenHuman provider adapter, tool adapters, event bridge, middleware, steering,
|
||||
early-exit hooks, stop hooks, model/tool call caps, and outcome capture.
|
||||
|
||||
Practical status: the old in-tree model/tool loop is no longer the live
|
||||
execution engine for those three entry points.
|
||||
|
||||
### Middleware and policy
|
||||
|
||||
Already migrated into TinyAgents middleware or the TinyAgents adapter seam:
|
||||
|
||||
- Approval and security gating at the tool boundary.
|
||||
- CLI/RPC-only tool denial.
|
||||
- Channel permission ceiling and tool policy checks.
|
||||
- Unknown-tool recovery via an internal sentinel.
|
||||
- Non-object tool-argument recovery.
|
||||
- Cost budget pre-checks before model calls.
|
||||
- Repeated tool failure halting.
|
||||
- Context compression and message trimming via TinyAgents middleware.
|
||||
- OpenHuman tool-output budgets and payload summarizer hooks as TinyAgents
|
||||
tool middleware.
|
||||
- Stop-hook checks via `src/openhuman/tinyagents/stop_hooks.rs`.
|
||||
|
||||
This is real migration, but not complete ownership transfer: OpenHuman still
|
||||
keeps rich policy metadata outside TinyAgents because the SDK tool schema does
|
||||
not yet carry all OpenHuman policy fields.
|
||||
|
||||
### Graph-backed orchestration
|
||||
|
||||
The codebase has several real TinyAgents graph uses:
|
||||
|
||||
- `src/openhuman/tinyagents/delegation.rs`: durable plan -> execute -> review ->
|
||||
finalize graph.
|
||||
- `src/openhuman/agent_orchestration/workflow_runs/graph.rs`: workflow phase DAG
|
||||
scheduler graph.
|
||||
- `src/openhuman/agent_orchestration/agent_teams/graph.rs`: member execution
|
||||
conditional-routing graph.
|
||||
- `src/openhuman/tinyagents/orchestration.rs`: reusable `run_parallel_fanout`
|
||||
helper, used by `spawn_parallel_agents` and workflow phase fanout.
|
||||
- `src/openhuman/tinyagents/topology.rs`: topology export for fixed graph
|
||||
structures.
|
||||
- `src/openhuman/agent_orchestration/running_subagents.rs`: detached sub-agent
|
||||
lifecycle is mirrored into TinyAgents `InMemoryTaskStore`.
|
||||
|
||||
Practical status: graph adoption is meaningful but uneven. The larger product
|
||||
tools still own validation, persistence, cancellation semantics, compatibility
|
||||
events, and result formatting.
|
||||
|
||||
## What Is Still Mostly OpenHuman-Owned
|
||||
|
||||
### Session, transcript, and cache shell
|
||||
|
||||
Largest remaining harness surface: `src/openhuman/agent/harness/session/`
|
||||
at roughly 13.2k lines.
|
||||
|
||||
Current OpenHuman role:
|
||||
|
||||
- Session transcript migration and compatibility.
|
||||
- Product-level `AgentBuilder` configuration.
|
||||
- Prompt section assembly and prompt-cache stability decisions.
|
||||
- Memory/context injection policy.
|
||||
- Post-turn hooks, transcript persistence, and OpenHuman-specific history shape.
|
||||
- Tool dispatcher compatibility for persisted native/XML/P-format transcript
|
||||
suffixes.
|
||||
|
||||
Updated TinyAgents target:
|
||||
|
||||
- Use TinyAgents `harness::store::{Store, AppendStore, FileStore,
|
||||
JsonlAppendStore}` as the durable substrate for message history, event
|
||||
journals, tool/model call records, artifacts, and local migration outputs.
|
||||
- Use TinyAgents `harness::cache::{ResponseCache, PromptCacheLayout,
|
||||
CacheLayoutEvent, CachePolicy}` for response-cache and provider KV-cache
|
||||
layout protection instead of keeping prompt-cache reasoning as OpenHuman-only
|
||||
session logic.
|
||||
- Use TinyAgents run/event/status lineage (`root_run_id`, `parent_run_id`,
|
||||
offsets, status stores) as the canonical internal transcript/run inspection
|
||||
surface, then project into legacy OpenHuman views where needed.
|
||||
- Write a one-time migration script for old OpenHuman `session_raw/*.jsonl` and
|
||||
legacy Markdown session files into TinyAgents store/journal records. The
|
||||
script should preserve original session ids, transcript stems, timestamps,
|
||||
provider/model metadata, tool-call ids, and parent/child links, and should be
|
||||
idempotent with a marker/version row so it can be safely re-run.
|
||||
|
||||
Keep in OpenHuman:
|
||||
|
||||
- The legacy reader/writer compatibility layer until migrated sessions are
|
||||
proven equivalent.
|
||||
- Product-specific prompt assembly, memory source identity, approval/security
|
||||
context, and JSON-RPC response shapes.
|
||||
|
||||
### Sub-agent build pipeline
|
||||
|
||||
Remaining surface: `src/openhuman/agent/harness/subagent_runner/`
|
||||
at roughly 6.1k lines.
|
||||
|
||||
Keep in OpenHuman:
|
||||
|
||||
- Agent definition lookup and allowlist enforcement.
|
||||
- Prompt assembly for archetypes.
|
||||
- Parent context, memory context, action root, sandbox, and toolkit filtering.
|
||||
- Worker-thread mirroring and transcript compatibility.
|
||||
- Integrations-agent preflight and handoff cache behavior.
|
||||
|
||||
Updated TinyAgents target:
|
||||
|
||||
- Map the OpenHuman sub-agent build pipeline into TinyAgents `SubAgent`,
|
||||
`SubAgentSession`, and `SubAgentTool` once registry/policy adapters are ready.
|
||||
- Express multi-step sub-agent work as graph structure using
|
||||
`graph::subagent_node` for harness-agent leaves and `graph::subgraph` for
|
||||
reusable pipelines. The current OpenHuman "prepare prompt -> filter tools ->
|
||||
run child -> checkpoint/handback -> mirror transcript" flow can become a
|
||||
subgraph rather than a sidecar runner.
|
||||
- Use TinyAgents parent/root run lineage and `UsageTotals` for child usage/cost
|
||||
rollup instead of carrying separate OpenHuman aggregation structs.
|
||||
- Keep OpenHuman-specific preflight nodes for agent definition lookup,
|
||||
allowlists, toolkit gates, sandbox/action-root narrowing, and worker-thread
|
||||
mirroring.
|
||||
|
||||
### Detached sub-agent registry
|
||||
|
||||
Remaining surface: `src/openhuman/agent_orchestration/running_subagents.rs`
|
||||
at roughly 1.0k lines.
|
||||
|
||||
Current state: it uses TinyAgents `InMemoryTaskStore` as a typed lifecycle
|
||||
ledger, but still owns watch channels, abort handles, wait/steer/cancel control,
|
||||
tombstones, ownership checks, and session lookup.
|
||||
|
||||
Updated TinyAgents target:
|
||||
|
||||
- Durable `TaskStore` support.
|
||||
- Typed wait/steer/cancel APIs.
|
||||
- Parent/root run tree queries.
|
||||
- Cancellation requests and hard abort lifecycle events.
|
||||
|
||||
TinyAgents now has more of the lifecycle substrate than this audit originally
|
||||
assumed, including harness/graph status records, lineage-aware events,
|
||||
`SubAgentSession` reuse, and append stores. The migration should therefore be an
|
||||
adapter exercise first: map existing durable OpenHuman sub-agent session rows and
|
||||
worker-thread records into TinyAgents session/status/journal records, then keep
|
||||
OpenHuman controllers as compatibility projections.
|
||||
|
||||
Keep in OpenHuman until parity is proven:
|
||||
|
||||
- Desktop restart/resume compatibility.
|
||||
- Existing JSON-RPC/tool response shapes.
|
||||
- Durable OpenHuman session and worker-thread ledgers.
|
||||
|
||||
### Parallel agents and workflow fanout
|
||||
|
||||
Remaining surfaces:
|
||||
|
||||
- `src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`
|
||||
- `src/openhuman/agent_orchestration/workflow_runs/engine.rs`
|
||||
- `src/openhuman/tinyagents/orchestration.rs`
|
||||
|
||||
Current state: fanout execution uses a TinyAgents graph helper, but
|
||||
`spawn_parallel_agents` is not yet a first-class graph tool. It still owns
|
||||
validation, worktree setup, overlap/stale-read checks, compatibility events, and
|
||||
JSON output formatting.
|
||||
|
||||
Good TinyAgents candidates:
|
||||
|
||||
- Re-express `spawn_parallel_agents` as `validate -> dispatch -> worker ->
|
||||
collect -> finalize` using graph `Send` or an SDK map/reduce helper.
|
||||
- Move deterministic ordering, cancellation boundaries, graph status, and child
|
||||
lineage into TinyAgents graph state.
|
||||
- Keep result formatting and OpenHuman worktree policy in a thin wrapper.
|
||||
|
||||
### Per-agent graph selectors
|
||||
|
||||
There are 32 `src/openhuman/agent_registry/agents/*/graph.rs` files and all
|
||||
currently return `AgentGraph::Default`.
|
||||
|
||||
This is mostly scaffolding, not migrated behavior. It proves the extension seam
|
||||
exists, but there are no bespoke per-agent TinyAgents graphs yet.
|
||||
|
||||
Good candidates for first real custom graphs:
|
||||
|
||||
- `orchestrator`: planning/delegation/parallelism policy can become explicit
|
||||
graph routing instead of prompt-only convention.
|
||||
- `researcher`: search -> read -> synthesize -> cite can be bounded and
|
||||
checkpointed.
|
||||
- `tool_maker`: detect missing capability -> generate -> validate -> expose can
|
||||
become a graph with explicit review gates.
|
||||
|
||||
Candidate cleanup: replace boilerplate default `graph.rs` files with registry
|
||||
defaults once at least one real custom graph proves the API shape.
|
||||
|
||||
## Porting Candidates By Priority
|
||||
|
||||
### P0: Update stale active docs and comments
|
||||
|
||||
Evidence: active architecture docs still described the old `engine::run_turn_engine`
|
||||
loop in sections below the TinyAgents status callout.
|
||||
|
||||
Action:
|
||||
|
||||
- Rewrite active sections to describe TinyAgents as the live loop.
|
||||
- Move the removed in-house loop details into a short historical appendix.
|
||||
- Sweep code comments that still say `run_turn_engine`, `run_tool_call_loop`, or
|
||||
`run_inner_loop` when they now mean the TinyAgents harness path.
|
||||
|
||||
Why first: stale docs make it hard to tell whether remaining files are live
|
||||
runtime, compatibility shell, or historical residue.
|
||||
|
||||
Status (2026-07-01): done. `gitbooks/developing/architecture/agent-harness.md`
|
||||
active sections now describe the TinyAgents path (loop, dialects-as-transcript-
|
||||
compat, middleware context management, steering-channel cancellation, corrected
|
||||
file map, 1.2 pin); pre-migration loop details are confined to the marked
|
||||
historical sections. The retired-loop comment sweep landed earlier (f5a6b5196);
|
||||
remaining `run_turn_engine`/`run_inner_loop` mentions in code are explicit
|
||||
"legacy parity" references, not current-behavior claims.
|
||||
|
||||
### P1: Migrate old OpenHuman sessions into TinyAgents stores
|
||||
|
||||
Design: `docs/tinyagents-session-migration-design.md` (2026-07-01) — source
|
||||
format inventory, target store layout, lineage-key mapping, idempotency
|
||||
ledger, fixture matrix, and phasing.
|
||||
|
||||
Status (2026-07-01): Phase 1 (write-only importer) implemented in
|
||||
`src/openhuman/session_import/` as `openhuman.session_import_run`, with the
|
||||
full fixture matrix as tests. Phases 2–4 (read-side shadow, cutover,
|
||||
retirement of legacy readers) remain.
|
||||
|
||||
Current OpenHuman files:
|
||||
|
||||
- `src/openhuman/agent/harness/session/transcript.rs`
|
||||
- `src/openhuman/agent/harness/session/migration.rs`
|
||||
- `session_raw/*.jsonl` and legacy Markdown session directories under user
|
||||
workspaces.
|
||||
|
||||
Target shape:
|
||||
|
||||
- Add a one-time migration command/script that reads old OpenHuman session JSONL
|
||||
and Markdown transcripts, normalizes them into TinyAgents message/event/store
|
||||
records, and writes them through TinyAgents-compatible `Store`/`AppendStore`
|
||||
semantics.
|
||||
- Preserve compatibility metadata so old UI surfaces and run-ledger lookups can
|
||||
still answer by OpenHuman session key while new internals read by TinyAgents
|
||||
`thread_id`, `run_id`, `root_run_id`, and stream offset.
|
||||
- Make the migration idempotent and observable: dry-run mode, per-file summary,
|
||||
warning list, migrated-count counters, and a marker/version record.
|
||||
|
||||
Risk: transcript shape is user data. This needs fixture coverage over current
|
||||
flat `session_raw/*.jsonl`, older date-folder JSONL, Markdown sessions,
|
||||
sub-agent transcript stems, native tool-call envelopes, XML/P-format tool
|
||||
history, and malformed partial files.
|
||||
|
||||
### P2: Make event/status journals canonical
|
||||
|
||||
Current OpenHuman files:
|
||||
|
||||
- `src/openhuman/tinyagents/observability.rs`
|
||||
- `src/openhuman/agent/harness/engine/progress.rs`
|
||||
- `src/openhuman/session_db/run_ledger/*`
|
||||
- `src/core/event_bus/*`
|
||||
|
||||
Target shape:
|
||||
|
||||
- TinyAgents journals/status stores become the internal source for model/tool
|
||||
events, usage, graph steps, child run lineage, and resumable inspection.
|
||||
- `AgentProgress` and `DomainEvent` become compatibility projections.
|
||||
|
||||
Risk: this crosses UI streaming, cost footer, run ledgers, and desktop
|
||||
reconnect behavior. It needs focused parity tests before deletion.
|
||||
|
||||
### P3: Port detached task lifecycle beyond the OpenHuman registry
|
||||
|
||||
Current OpenHuman files:
|
||||
|
||||
- `src/openhuman/agent_orchestration/running_subagents.rs`
|
||||
- `src/openhuman/agent_orchestration/tools/{wait_subagent,steer_subagent,close_subagent,continue_subagent}.rs`
|
||||
|
||||
Target shape:
|
||||
|
||||
- TinyAgents owns typed task lifecycle, wait/steer/cancel semantics, parent/root
|
||||
run lineage, and terminal history.
|
||||
- OpenHuman owns durable SQL/JSON projection and product response formatting.
|
||||
|
||||
This is no longer just waiting on SDK primitives. Current TinyAgents exposes
|
||||
session reuse, lineage-aware events/status, graph/harness observability, and
|
||||
JSONL append storage. The concrete blocker is now designing the OpenHuman
|
||||
compatibility adapter and proving restart/resume parity against existing
|
||||
controllers.
|
||||
|
||||
### P4: Re-express `spawn_parallel_agents` as a graph tool
|
||||
|
||||
Current OpenHuman files:
|
||||
|
||||
- `src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`
|
||||
- `src/openhuman/agent_orchestration/worktree.rs`
|
||||
- `src/openhuman/tinyagents/orchestration.rs`
|
||||
|
||||
Target shape:
|
||||
|
||||
- A graph with nodes for validation, dispatch, worker, collect, finalize.
|
||||
- Deterministic reducer state for per-task result order.
|
||||
- Child run lineage and cancellation through graph status.
|
||||
- Thin OpenHuman wrapper for the existing JSON result and worktree policy.
|
||||
|
||||
Why: this is a high-value migration because it converts a large product-visible
|
||||
orchestration loop without touching the basic chat turn.
|
||||
|
||||
### P5: Replace generic checkpoint/progress seams
|
||||
|
||||
Current OpenHuman files:
|
||||
|
||||
- `src/openhuman/agent/harness/engine/checkpoint.rs`
|
||||
- `src/openhuman/agent/harness/engine/progress.rs`
|
||||
- `src/openhuman/agent/harness/subagent_runner/ops/checkpoint.rs`
|
||||
|
||||
Target shape:
|
||||
|
||||
- Model-call cap, early-exit pause, resumable checkpoint summary, and progress
|
||||
projection represented as TinyAgents status/events/middleware.
|
||||
|
||||
Keep only the OpenHuman-specific compatibility formatting for existing
|
||||
transcripts and tool outputs.
|
||||
|
||||
### P6: Retire boilerplate per-agent graph selectors
|
||||
|
||||
Current OpenHuman files:
|
||||
|
||||
- `src/openhuman/agent/harness/agent_graph.rs`
|
||||
- `src/openhuman/agent_registry/agents/*/graph.rs`
|
||||
|
||||
Target shape:
|
||||
|
||||
- Default graph supplied centrally.
|
||||
- Only agents with custom graph behavior keep a `graph.rs`.
|
||||
- Registry diagnostics show which graph each agent actually uses.
|
||||
|
||||
This should happen after at least one bespoke graph lands, so the cleanup does
|
||||
not remove a seam before it has proved useful.
|
||||
|
||||
## Code That Should Probably Stay In OpenHuman
|
||||
|
||||
Do not port these blindly into TinyAgents:
|
||||
|
||||
- Agent registry definitions and built-in prompt semantics.
|
||||
- Tool allowlists/denylists that encode product policy.
|
||||
- Security policy, sandbox roots, approval records, credential access, and
|
||||
workspace/action-dir boundaries.
|
||||
- Session transcript migration and persisted compatibility formats.
|
||||
- OpenHuman model provider routing, billing classification, and credential
|
||||
ownership.
|
||||
- UI-facing JSON-RPC response shapes and `DomainEvent` compatibility until every
|
||||
subscriber is moved.
|
||||
- Worktree isolation policy and dirty-worktree safeguards.
|
||||
- Memory stores, retrieval policy, and context source identity.
|
||||
|
||||
TinyAgents should own generic runtime machinery. OpenHuman should own product
|
||||
semantics and compatibility boundaries.
|
||||
|
||||
## Main SDK Gaps Blocking More Deletion
|
||||
|
||||
The most important upstream gaps are tracked in `docs/tinyagents-sdk-gaps.md`,
|
||||
but that file must be refreshed against TinyAgents `main` at
|
||||
`348a0e7dc71a1f9039f3d523a2a384661a7a9acd`. Several older "missing" items now
|
||||
exist as concrete TinyAgents modules:
|
||||
|
||||
- `harness::cache` has response caching and prompt/KV-cache layout protection.
|
||||
- `harness::store` has key-value stores and `JsonlAppendStore`.
|
||||
- `harness::subagent` has `SubAgent`, `SubAgentSession`, and `SubAgentTool`.
|
||||
- `graph::subgraph` and `graph::subagent_node` can express nested graph and
|
||||
sub-agent pipelines.
|
||||
- harness/graph observability and status types carry `root_run_id` /
|
||||
`parent_run_id` lineage.
|
||||
|
||||
Remaining blockers are narrower:
|
||||
|
||||
- Rich tool policy metadata in TinyAgents schemas.
|
||||
- Recoverable unknown-tool policy without the OpenHuman sentinel.
|
||||
- Full OpenHuman policy mapping for approval/security/sandbox/workspace roots.
|
||||
- A migration adapter from OpenHuman session JSONL/Markdown/run-ledger rows into
|
||||
TinyAgents store/journal/status records.
|
||||
- SQLite/native-link compatibility if OpenHuman wants to use TinyAgents'
|
||||
embedded SQLite backend directly rather than its own SQLite adapter.
|
||||
- Redaction/cursor/backfill rules for production UI replay.
|
||||
- OpenHuman controller compatibility during the migration window.
|
||||
|
||||
## Suggested Next Documentation Pass
|
||||
|
||||
1. Rewrite the active `gitbooks/developing/architecture/agent-harness.md`
|
||||
sections below the status block so they describe the TinyAgents path instead
|
||||
of the removed in-house loop.
|
||||
2. Add a one-time transcript/session migration design for old OpenHuman
|
||||
`session_raw/*.jsonl` and Markdown sessions into TinyAgents store/journal
|
||||
records. Done: `docs/tinyagents-session-migration-design.md`.
|
||||
3. Add a small "Historical pre-TinyAgents loop" appendix for details that are
|
||||
still useful context.
|
||||
4. Add per-folder READMEs or module docs for:
|
||||
- `src/openhuman/agent/harness/session/`: product session shell over
|
||||
TinyAgents.
|
||||
- `src/openhuman/agent/harness/subagent_runner/`: OpenHuman sub-agent build
|
||||
pipeline over TinyAgents.
|
||||
- `src/openhuman/agent_orchestration/`: product orchestration wrappers over
|
||||
TinyAgents graphs/task lifecycle.
|
||||
5. Keep `docs/tinyagents-migration-spec.md` as the backlog, and use this audit
|
||||
as the current inventory snapshot.
|
||||
@@ -1,836 +0,0 @@
|
||||
# TinyAgents Migration Spec
|
||||
|
||||
Status: draft migration backlog
|
||||
|
||||
TinyAgents source reviewed: `tinyhumansai/tinyagents` `origin/main` at
|
||||
`8f226f1`, crate version `1.1.0`. Refreshed against `tinyhumansai/tinyagents`
|
||||
`main` at `348a0e7dc71a1f9039f3d523a2a384661a7a9acd` after the SDK/docs update.
|
||||
Current OpenHuman dependency in this checkout is
|
||||
`tinyagents = { version = "1.5.0", features = ["sqlite"] }`.
|
||||
|
||||
OpenHuman already depends on TinyAgents and already routes the live agent turn
|
||||
through `src/openhuman/tinyagents/`. This spec is not a proposal to add
|
||||
TinyAgents. It is a todo list for moving the rest of OpenHuman's generic agent
|
||||
runtime behavior onto TinyAgents primitives while keeping OpenHuman-owned
|
||||
product semantics in OpenHuman.
|
||||
|
||||
Current inventory snapshot: [`tinyagents-harness-migration-audit.md`](tinyagents-harness-migration-audit.md).
|
||||
|
||||
## Goal
|
||||
|
||||
Use TinyAgents as the generic runtime for:
|
||||
|
||||
- model/provider abstraction and model selection
|
||||
- tools and tool schemas
|
||||
- middleware around model/tool calls
|
||||
- streaming, events, traces, and replayable run status
|
||||
- session transcript storage and migration targets
|
||||
- prompt/response cache layout protection
|
||||
- token usage and cost rollups
|
||||
- state graphs, fanout, reducers, checkpoints, and interrupts
|
||||
- sub-agent recursion, steering, cancellation, and reusable sessions
|
||||
- deterministic testkit coverage for the runtime seams
|
||||
|
||||
OpenHuman should continue to own:
|
||||
|
||||
- desktop product UX and Tauri/RPC boundaries
|
||||
- user/workspace config, credentials, keychain, and approval records
|
||||
- OpenHuman memory stores, thread transcripts, run ledgers, and controllers
|
||||
- security policy, sandboxing, tool permission tiers, and workspace roots
|
||||
- product-specific built-in agents, prompts, MCP setup, Composio, channels, and
|
||||
native tools
|
||||
- compatibility with existing JSON-RPC method names and persisted state
|
||||
|
||||
## Sources Reviewed
|
||||
|
||||
TinyAgents SDK:
|
||||
|
||||
- `src/lib.rs`
|
||||
- `src/harness/*`
|
||||
- `src/graph/*`
|
||||
- `src/registry/*`
|
||||
- `src/language/*`
|
||||
- `src/repl/*`
|
||||
- `docs/modules/harness/*.md`
|
||||
- `docs/modules/graph/*.md`
|
||||
- `docs/modules/registry/*.md`
|
||||
- `docs/modules/expressive-language/README.md`
|
||||
- `docs/modules/repl-language/README.md`
|
||||
- examples: `agent_loop_tools`, `orchestrator_subagents`, `durable_graph`,
|
||||
`openai_graph_agent`, `openai_self_blueprint`
|
||||
|
||||
OpenHuman Rust core:
|
||||
|
||||
- `src/openhuman/tinyagents/*`
|
||||
- `src/openhuman/agent/**`
|
||||
- `src/openhuman/agent_orchestration/**`
|
||||
- `src/openhuman/agent_registry/**`
|
||||
- `src/openhuman/tools/**`
|
||||
- `src/openhuman/inference/**`
|
||||
- `src/openhuman/cost/**`
|
||||
- `src/openhuman/context/**`
|
||||
- `src/openhuman/approval/**`
|
||||
- `src/openhuman/security/**`
|
||||
- `src/openhuman/mcp_registry/**`
|
||||
- `src/core/event_bus/**`
|
||||
- `src/core/all.rs`
|
||||
- `gitbooks/developing/architecture/agent-harness.md`
|
||||
|
||||
## Current Adoption Inventory
|
||||
|
||||
Already done or partially done:
|
||||
|
||||
- `Cargo.toml` pins `tinyagents = { version = "1.5.0", features = ["sqlite"] }`.
|
||||
- `src/openhuman/tinyagents/mod.rs` registers OpenHuman `Provider` and `Tool`
|
||||
adapters on `tinyagents::harness::runtime::AgentHarness`.
|
||||
- `ProviderModel` maps OpenHuman `ChatRequest`/`ChatResponse` into
|
||||
`tinyagents::harness::model::{ModelRequest, ModelResponse, ModelStream}`.
|
||||
- `ToolAdapter` and `SharedToolAdapter` map OpenHuman tools into
|
||||
`tinyagents::harness::tool::Tool`.
|
||||
- `OpenhumanEventBridge` maps TinyAgents `AgentEvent` into `AgentProgress` and
|
||||
the global cost tracker.
|
||||
- `StopHookMiddleware`, `ContextCompressionMiddleware`, and
|
||||
`MessageTrimMiddleware` are already used on the TinyAgents path.
|
||||
- `SqlRunLedgerCheckpointer` implements TinyAgents `Checkpointer` on top of the
|
||||
OpenHuman session DB while the migration re-points checkpoint rows to the
|
||||
crate checkpointer.
|
||||
- `spawn_parallel_graph` uses `GraphBuilder` and
|
||||
`graph::parallel::map_reduce` for reusable concurrent fanout.
|
||||
- `model_council`, `workflow_runs`, `agent_teams`, and
|
||||
`tinyagents/delegation.rs` already use TinyAgents graphs.
|
||||
- Built-in agents already have `graph.rs` selectors, but most return
|
||||
`AgentGraph::Default`.
|
||||
|
||||
Important current gaps:
|
||||
|
||||
- OpenHuman still has separate registries for agents, tools, MCP tools, model
|
||||
providers, controllers, cost, and event bus projections.
|
||||
- Tool safety metadata exists in OpenHuman traits but is not fully expressed as
|
||||
TinyAgents tool safety/runtime metadata.
|
||||
- Cost/usage is still converted through a bridge, not an end-to-end TinyAgents
|
||||
usage/cost journal.
|
||||
- Event streams are mirrored into `AgentProgress`, but TinyAgents event journals
|
||||
and status stores are not the canonical durable inspection surface.
|
||||
- Provider model profiles, model resolution, and fallback remain primarily in
|
||||
OpenHuman inference/router logic.
|
||||
- Sub-agent lifecycle, durable state, worker threads, and wait/abort controls
|
||||
still live in OpenHuman orchestration stores.
|
||||
|
||||
Some todos below are local adapter work. Others still require upstream
|
||||
TinyAgents SDK extensions. In particular, the SDK has a strong tool/runtime
|
||||
boundary today (`ToolSchema`, `ToolExecutionContext`, middleware hooks), but
|
||||
OpenHuman's full tool safety metadata is richer than the current SDK schema.
|
||||
After the TinyAgents `main` refresh, durable session/cache/journal primitives are
|
||||
no longer the broad SDK gap they were in the original baseline; the remaining
|
||||
work is to design OpenHuman's compatibility adapter, migrate old transcripts and
|
||||
run-ledger rows, and prove restart/resume parity.
|
||||
|
||||
## Migration Rules
|
||||
|
||||
- Keep every product-facing JSON-RPC contract stable unless a migration plan is
|
||||
written next to the code change.
|
||||
- Do not bypass OpenHuman approval, security policy, sandbox, workspace root, or
|
||||
credential boundaries by adopting a generic TinyAgents tool API.
|
||||
- Prefer adapters first, then flip ownership once tests prove parity.
|
||||
- Preserve existing transcript and run-ledger compatibility. TinyAgents may
|
||||
become the internal runtime without changing persisted public records in the
|
||||
same PR.
|
||||
- Old OpenHuman session JSONL/Markdown data should move through a one-time,
|
||||
idempotent migration script into TinyAgents-compatible store/journal records
|
||||
before the old readers are deleted.
|
||||
- Every migration task needs unit coverage plus at least one JSON-RPC or
|
||||
harness-level e2e when behavior crosses controller, tool, provider, or graph
|
||||
boundaries.
|
||||
|
||||
## Phase 0 - Baseline And Drift Control
|
||||
|
||||
- [x] Add a version/feature compatibility note to the OpenHuman architecture doc.
|
||||
- OpenHuman files: `gitbooks/developing/architecture/agent-harness.md`,
|
||||
`Cargo.toml`.
|
||||
- TinyAgents components: crate features `default`, `openai`, `sqlite`, `repl`.
|
||||
- Acceptance: document why default features are used, why TinyAgents `sqlite`
|
||||
is enabled through the aligned `rusqlite` stack, and which OpenHuman adapters
|
||||
still replace SDK-owned providers.
|
||||
- **Done:** added "TinyAgents crate: features & compatibility" section to
|
||||
agent-harness.md (default-only, `openai`/`sqlite`/`repl` rationale, adapter
|
||||
map) + fixed stale `council_graph.rs`/`member_graph.rs` links.
|
||||
|
||||
## Phase 1 - Tools
|
||||
|
||||
- [~] Make OpenHuman tool metadata round-trip into TinyAgents tool metadata.
|
||||
- OpenHuman files: `src/openhuman/tools/traits.rs`,
|
||||
`src/openhuman/tinyagents/tools.rs`, `src/openhuman/tinyagents/convert.rs`.
|
||||
- TinyAgents components: `harness::tool::{ToolSchema, ToolFormat,
|
||||
ToolExecutionContext, ToolResult}`.
|
||||
- Migrate: permission level, external effect, generated runtime context,
|
||||
timeout policy, concurrency safety, result-size cap, display label/detail,
|
||||
markdown support.
|
||||
- Acceptance: a TinyAgents tool call has enough metadata for middleware to
|
||||
enforce approval, security, timeout, concurrency, truncation, and display
|
||||
behavior without re-querying OpenHuman trait methods ad hoc.
|
||||
- **1.3 update:** crate `ToolPolicy`, `Tool::policy()`,
|
||||
`ToolRegistry::policies()`, and `ToolPolicyMiddleware` now provide the
|
||||
SDK-owned safety/runtime/access projection. `ToolSchema` still carries only
|
||||
name/description/parameters/format — it has **no** model-visible
|
||||
metadata/extension map — so OpenHuman should map enforcement fields into
|
||||
`ToolPolicy` while keeping display/schema annotations as app-side metadata.
|
||||
Existing side-lookup middleware (`ApprovalSecurityMiddleware`,
|
||||
`ToolOutputMiddleware`) can shrink as those policy snapshots become the
|
||||
source of truth.
|
||||
|
||||
- [x] Move unknown-tool recovery into a reusable middleware or tool policy layer.
|
||||
- Current path: `run_policy_for` sets
|
||||
`UnknownToolPolicy::ReturnToolError`; no sentinel tool is registered.
|
||||
- TinyAgents components: `ToolRegistry`, `ToolMiddleware`,
|
||||
`AgentEvent::ToolStarted/ToolCompleted`, repairable tool results.
|
||||
- Acceptance: hallucinated tool names remain recoverable, sub-agent wording is
|
||||
preserved, and TinyAgents event stream records the original requested tool
|
||||
name without exposing the sentinel as a model-visible tool.
|
||||
- **1.3 update:** crate `RunPolicy.unknown_tool` now has
|
||||
`UnknownToolPolicy::{Fail, ReturnToolError, Rewrite}` and emits
|
||||
`AgentEvent::UnknownToolCall` with the original requested name/arguments.
|
||||
OpenHuman now uses that policy directly; `UNKNOWN_TOOL_SENTINEL` and
|
||||
`UnknownToolRewriteMiddleware` are gone from source.
|
||||
|
||||
- [x] Route approval and security through TinyAgents middleware.
|
||||
- Current OpenHuman files: `src/openhuman/approval/*`,
|
||||
`src/openhuman/security/*`, `src/openhuman/tinyagents/tools.rs`.
|
||||
- TinyAgents components: `ToolMiddleware`, `ToolExecutionContext`,
|
||||
tool safety metadata.
|
||||
- Acceptance: approval checks happen in `before_tool`/`wrap_tool`, emit typed
|
||||
events, preserve audit rows, and return model-consumable denial results.
|
||||
- **Done:** `ApprovalSecurityMiddleware` (`tinyagents/middleware.rs`, a
|
||||
`wrap_tool` middleware) replaces the inline approval block in
|
||||
`execute_openhuman_tool`. Denials short-circuit with a model-consumable
|
||||
result; approved external-effect calls now record a terminal audit row
|
||||
(`record_execution`) the old path dropped. Typed approval events still ride
|
||||
`DomainEvent` (the crate `AgentEvent` enum has no approval variant — SDK gap).
|
||||
Tool-*internal* security (path/command `live_policy`) stays per-tool by
|
||||
design. Follow-ups: channel permission-ceiling threading; per-tool metadata
|
||||
side-lookup (Task C).
|
||||
|
||||
- [ ] Use TinyAgents bounded-concurrent tool execution where safe.
|
||||
- OpenHuman files: `src/openhuman/tools/traits.rs`,
|
||||
`src/openhuman/tinyagents/tools.rs`.
|
||||
- TinyAgents components: graph `Send`, graph fanout, or harness tool
|
||||
execution policy.
|
||||
- Acceptance: read-only/concurrency-safe tool batches can run in parallel
|
||||
with deterministic result ordering and identical transcript semantics.
|
||||
|
||||
## Phase 2 - Models And Providers
|
||||
|
||||
- [ ] Register OpenHuman inference providers as TinyAgents model registry entries.
|
||||
- OpenHuman files: `src/openhuman/inference/provider/*`,
|
||||
`src/openhuman/inference/model_ids.rs`, `src/openhuman/tinyagents/model.rs`.
|
||||
- TinyAgents components: `harness::model::{ChatModel, ModelRegistry,
|
||||
ModelProfile, CapabilitySet, ModelRequest, ModelResponse}`.
|
||||
- Acceptance: every workload route (`agentic`, `reasoning`, `coding`,
|
||||
`memory`, `subconscious`, etc.) can resolve to a TinyAgents model entry
|
||||
while retaining OpenHuman provider strings and config compatibility.
|
||||
|
||||
- [~] Translate OpenHuman provider capability data into TinyAgents model profiles.
|
||||
- OpenHuman files: `src/openhuman/inference/provider/traits.rs`,
|
||||
`src/openhuman/inference/provider/factory.rs`,
|
||||
`docs/inference-provider-catalog.md`.
|
||||
- TinyAgents components: `ModelProfile`, `CapabilitySet`, registry model
|
||||
catalog.
|
||||
- Acceptance: context window, tool calling, streaming, vision, structured
|
||||
output, reasoning, local/cloud source, and provider-family metadata are
|
||||
available before dispatch.
|
||||
- **Partial:** every `ProviderModel` registered by the shared runner now
|
||||
carries a crate `ModelProfile` built at construction from the provider's
|
||||
canonical capability accessors — tool calling (+parallel), vision
|
||||
(`modalities.image_in`), streaming, local/remote source — plus the
|
||||
runner-threaded token limits (`with_context_window` → `max_input_tokens`,
|
||||
output cap → `max_output_tokens`). `ChatModel::profile()` returns it, so
|
||||
the crate's pre-dispatch validation and structured-output strategy see real
|
||||
capabilities. Remaining: structured-output/JSON-schema/reasoning flags
|
||||
(no OpenHuman capability source yet), release/status metadata, and a
|
||||
registry-level model *catalog* (ties into the workload-route registry item
|
||||
above).
|
||||
|
||||
- [ ] Move model fallback and retry policy to TinyAgents policy/middleware.
|
||||
- OpenHuman files: `src/openhuman/inference/provider/reliable.rs`,
|
||||
`src/openhuman/inference/provider/router.rs`,
|
||||
`src/openhuman/tinyagents/mod.rs`.
|
||||
- TinyAgents components: `RunPolicy`, `RetryPolicy`, `FallbackPolicy`,
|
||||
`ModelFallbackMiddleware`, `AgentEvent::RetryScheduled`,
|
||||
`AgentEvent::FallbackSelected`.
|
||||
- Acceptance: OpenHuman provider retry does not double-retry under
|
||||
TinyAgents, fallback events are typed, and tests cover transient 429/5xx,
|
||||
config rejection, and billing exhaustion.
|
||||
|
||||
- [ ] Preserve provider-specific metadata in the TinyAgents message model.
|
||||
- OpenHuman files: `src/openhuman/inference/provider/traits.rs`,
|
||||
`src/openhuman/tinyagents/convert.rs`,
|
||||
`src/openhuman/tinyagents/model.rs`.
|
||||
- TinyAgents components: `ContentBlock`, `AssistantMessage`, provider
|
||||
metadata, tool-call ids.
|
||||
- Acceptance: Gemini thought signatures, reasoning content, native tool-call
|
||||
ids, cached tokens, and raw provider metadata survive multi-turn history.
|
||||
|
||||
## Phase 3 - Middleware
|
||||
|
||||
- [ ] Convert OpenHuman turn cross-cuts into named TinyAgents middleware.
|
||||
- Current OpenHuman surfaces: stop hooks, approval gate, security policy,
|
||||
output caps, context compression, memory injection, tool allowlists,
|
||||
cost/usage, prompt cache stability, event bridge.
|
||||
- TinyAgents components: `Middleware`, `ModelMiddleware`, `ToolMiddleware`,
|
||||
`MiddlewareStack`, `RunContext`.
|
||||
- Acceptance: each cross-cut has a stable middleware name, tests for ordering,
|
||||
emitted events, and explicit interaction with streaming/retry/fallback.
|
||||
|
||||
- [ ] Add OpenHuman policy middleware for dynamic tool exposure.
|
||||
- OpenHuman files: `src/openhuman/agent_registry/*`,
|
||||
`src/openhuman/agent/harness/subagent_runner/**`,
|
||||
`src/openhuman/tools/user_filter.rs`.
|
||||
- TinyAgents components: `before_model`, `before_tool`, `ToolRegistry`.
|
||||
- Acceptance: agent `tool_allowlist`, `tool_denylist`, sub-agent tool scope,
|
||||
MCP tool visibility, and channel permission ceilings are enforced through
|
||||
middleware rather than scattered pre-filtering.
|
||||
|
||||
- [ ] Add prompt/cache-layout middleware tests.
|
||||
- OpenHuman files: `src/openhuman/context/*`,
|
||||
`src/openhuman/agent/harness/session/turn/core.rs`,
|
||||
`src/openhuman/tinyagents/summarize.rs`.
|
||||
- TinyAgents components: `harness::cache::{CachePolicy, CacheLayoutEvent,
|
||||
PromptCacheLayout, ResponseCache}`, `ContextCompressionMiddleware`,
|
||||
`MessageTrimMiddleware`.
|
||||
- Acceptance: system prompt prefix remains stable across later turns; volatile
|
||||
memory, timestamps, tool results, and steering messages land in the tail.
|
||||
|
||||
- [ ] Move OpenHuman response-cache and provider KV-cache protection onto
|
||||
TinyAgents cache primitives.
|
||||
- OpenHuman files: `src/openhuman/agent/harness/session/turn/core.rs`,
|
||||
`src/openhuman/context/*`, `src/openhuman/tinyagents/middleware.rs`.
|
||||
- TinyAgents components: `harness::cache::{ResponseCache,
|
||||
PromptCacheLayout, CachePolicy}`, `AgentEvent::CacheHit`,
|
||||
`AgentEvent::CacheMiss`.
|
||||
- Acceptance: repeated deterministic model requests can be served by the
|
||||
TinyAgents response cache where safe, prompt-prefix stability is asserted by
|
||||
`PromptCacheLayout`, and OpenHuman cache-align warnings become TinyAgents
|
||||
cache-layout events.
|
||||
|
||||
## Phase 4 - Events, Status, And Observability
|
||||
|
||||
- [ ] Make TinyAgents event journals the canonical internal run event stream.
|
||||
- OpenHuman files: `src/openhuman/tinyagents/observability.rs`,
|
||||
`src/core/event_bus/*`, `src/openhuman/notifications/*`,
|
||||
`src/openhuman/session_db/run_ledger/*`.
|
||||
- TinyAgents components: `HarnessEventJournal`, `HarnessStatusStore`,
|
||||
`GraphEventJournal`, `GraphStatusStore`, `harness::store::AppendStore`,
|
||||
`harness::store::JsonlAppendStore`, `AgentEvent`, `GraphEvent`.
|
||||
- Acceptance: UIs can reconstruct a running or completed agent turn from
|
||||
persisted TinyAgents events without relying only on transient
|
||||
`AgentProgress`.
|
||||
|
||||
- [x] Write a one-time OpenHuman session transcript migration into TinyAgents
|
||||
store/journal records. Done: `src/openhuman/session_import/`
|
||||
(`openhuman.session_import_run`), design + as-built notes in
|
||||
`docs/tinyagents-session-migration-design.md`. Write-only Phase 1; read-side
|
||||
shadow/cutover remain.
|
||||
- OpenHuman files: `src/openhuman/agent/harness/session/transcript.rs`,
|
||||
`src/openhuman/agent/harness/session/migration.rs`, user workspace
|
||||
`session_raw/*.jsonl`, legacy Markdown session directories.
|
||||
- TinyAgents components: `harness::store::{Store, AppendStore, FileStore,
|
||||
JsonlAppendStore}`, `harness::message::Message`, harness event/status
|
||||
records with `thread_id`, `run_id`, `root_run_id`, and stream offsets.
|
||||
- Acceptance: old OpenHuman sessions are imported idempotently, preserving
|
||||
timestamps, transcript stems, parent/child session links, provider/model
|
||||
metadata, tool-call ids, native/XML/P-format history, and malformed-file
|
||||
warnings. Existing OpenHuman readers remain as compatibility projections
|
||||
until parity fixtures prove the migration.
|
||||
|
||||
- [ ] Bridge TinyAgents events into `DomainEvent` as a compatibility projection.
|
||||
- OpenHuman files: `src/core/event_bus/events.rs`,
|
||||
`src/openhuman/agent/bus.rs`, `src/openhuman/tinyagents/observability.rs`.
|
||||
- Acceptance: existing subscribers continue to receive `DomainEvent`, but new
|
||||
code reads TinyAgents events/status first.
|
||||
|
||||
- [ ] Persist graph run status and checkpoint metadata in OpenHuman run ledger.
|
||||
- OpenHuman files: `src/openhuman/tinyagents/checkpoint.rs`,
|
||||
`src/openhuman/session_db/run_ledger/store.rs`,
|
||||
`src/openhuman/agent_orchestration/**`.
|
||||
- TinyAgents components: `Checkpoint`, `CheckpointMetadata`,
|
||||
`GraphRunStatus`, `GraphObservation`.
|
||||
- Acceptance: command center, workflow runs, delegation, and team runs can
|
||||
list checkpoints, current node/task status, and replay offsets from one DB
|
||||
source.
|
||||
|
||||
- [x] Export graph topology for debugging and UI inspection.
|
||||
- OpenHuman files: built-in `graph.rs` files, `agent_orchestration/*/graph.rs`,
|
||||
`model_council/graph.rs`.
|
||||
- TinyAgents components: `GraphTopology`, `to_json`, `to_mermaid`,
|
||||
validation report.
|
||||
- Acceptance: every custom OpenHuman graph has a debug endpoint or test
|
||||
snapshot that exports topology and validates missing nodes/routes.
|
||||
- **Done:** `tinyagents/topology.rs` — `GraphTopologyReport` (mermaid + JSON +
|
||||
validation errors/warnings), `describe()`, and `all_graph_topologies()`.
|
||||
Pattern: each graph exposes a `build_*_graph` (structure) reused by both the
|
||||
runner and a `*_topology()` that builds it with no-op stub closures and
|
||||
returns `CompiledGraph::topology()`. Exported graphs: `agent_teams:member`,
|
||||
`delegation` (extracted `build_delegation_graph`),
|
||||
`workflow_runs:scheduler` (`build_scheduler_graph` with injected
|
||||
`select`/`run` engine effects), `subagent:pipeline`, and
|
||||
`spawn_parallel_agents`. Generic map-reduce fan-outs such as council runs
|
||||
are still item-count-driven dispatch→N→collect patterns, not fixed named
|
||||
topologies, so they are intentionally not exported. Debug endpoint:
|
||||
`agent.graph_topologies` JSON-RPC controller (`agent/schemas.rs`) returning
|
||||
`{name, ok, errors, warnings, mermaid, topology}` per graph.
|
||||
|
||||
## Phase 5 - Usage, Cost, And Budgets
|
||||
|
||||
- [~] Replace bridge-only usage accounting with TinyAgents usage records.
|
||||
- OpenHuman files: `src/openhuman/cost/*`,
|
||||
`src/openhuman/tinyagents/observability.rs`,
|
||||
`src/openhuman/inference/provider/traits.rs`.
|
||||
- TinyAgents components: `harness::usage::{Usage, UsageTotals}`,
|
||||
`harness::cost::CostTotals`, `AgentEvent::UsageRecorded`.
|
||||
- Acceptance: input, output, cached input, reasoning, image/audio, embedding,
|
||||
tool/model call counts, and estimated/provider-reported source are recorded
|
||||
in normalized records.
|
||||
- **Partial (real bug fixed):** the bridge hardcoded `charged_amount_usd: 0.0`
|
||||
and `ProviderModel` dropped cached tokens, so EVERY tinyagents turn recorded
|
||||
**$0 cost**. Now `model.rs` carries `cached_input_tokens` via crate
|
||||
`Usage.cache_read_tokens`, and the bridge estimates per-call cost from
|
||||
catalogued per-MTok rates (`cost::catalog::estimate_cost_usd`). Remaining:
|
||||
reasoning/image/audio/embedding token fields, model/tool call counts, and an
|
||||
explicit estimated-vs-provider-charged `cost_source` tag on `TokenUsage`
|
||||
(provider-charged preservation needs an out-of-band carry — crate `Usage` has
|
||||
no USD field).
|
||||
|
||||
- [~] Move budget checks to pre-call TinyAgents middleware.
|
||||
- OpenHuman files: `src/openhuman/cost/tracker.rs`,
|
||||
`src/openhuman/tinyagents/mod.rs`.
|
||||
- TinyAgents components: `RunPolicy`, cost middleware, `before_model`,
|
||||
`before_tool`.
|
||||
- Acceptance: per-run, per-thread, daily, and monthly budgets can warn or
|
||||
fail before spend where enough data exists, then reconcile after provider
|
||||
usage is known.
|
||||
- **Partial:** `CostBudgetMiddleware` (`before_model`) fails the run before a
|
||||
model call when the global daily/monthly budget is already exceeded
|
||||
(`CostTracker::check_budget`), and logs on the warning threshold. Self-gating
|
||||
on `config.enabled`; previously daily/monthly enforcement was dormant on the
|
||||
tinyagents path. Remaining: per-run/per-thread budgets (need new
|
||||
`CostConfig` fields + thread-id threading into the runner) and projecting the
|
||||
*next* call's cost pre-spend (needs an input-token estimate).
|
||||
|
||||
- [~] Add cost rollup across sub-agents and graphs.
|
||||
- OpenHuman files: `src/openhuman/agent_orchestration/**`,
|
||||
`src/openhuman/cost/global.rs`.
|
||||
- TinyAgents components: run ids, parent/root run lineage,
|
||||
`SubAgentStarted`, `SubAgentCompleted`, graph child runs.
|
||||
- Acceptance: parent run totals include child agent/model/tool usage without
|
||||
double counting dashboard totals.
|
||||
- **Partial (audit + real gap fixed).** Audit of the current mechanics:
|
||||
(1) parent-turn rollup — the `turn_subagent_usage` task-local collector
|
||||
wraps the turn future; `run_typed_mode` (the single sub-agent chokepoint)
|
||||
records every inline child, and graph fan-outs (`spawn_parallel_graph`,
|
||||
delegation, council) execute via `join_all` **on the same task**, so their
|
||||
children inherit the collector too; (2) dashboard totals — the global
|
||||
tracker is fed per model call by each run's own event bridge, and the
|
||||
parent's fold into `LastTurnUsage`/transcript never re-records to the
|
||||
tracker, so there is no double counting. **Gap fixed:** the tracker feed
|
||||
lived *only* in the bridge, so an unobserved (`on_progress = None`,
|
||||
fire-and-forget) turn's spend never reached the dashboard — the runner's
|
||||
cost fallback now records the aggregate via `record_unobserved_turn_usage`
|
||||
(mutually exclusive with the bridge → exactly-once). Remaining: crate
|
||||
run-id / parent-root lineage on cost records (needs `TokenUsage` schema
|
||||
fields), and rollup for *detached* background children beyond
|
||||
global-tracker capture (documented behavior today).
|
||||
|
||||
## Phase 6 - Graph Runtime And Orchestration
|
||||
|
||||
- [ ] Convert remaining ad hoc control loops into explicit TinyAgents graphs.
|
||||
- Candidate OpenHuman files: `src/openhuman/agent_orchestration/*`,
|
||||
`src/openhuman/subconscious/*`, `src/openhuman/cron/*`,
|
||||
`src/openhuman/learning/*`, `src/openhuman/tools/ops.rs`.
|
||||
- TinyAgents components: `GraphBuilder`, `Command`, conditional routing,
|
||||
reducers, `Send`, barriers, recursion policy.
|
||||
- Acceptance: every long-running multi-step orchestration has named nodes,
|
||||
route tests, recursion bounds, cancellation checks, and topology export.
|
||||
|
||||
- [ ] Replace simple fanout helpers with graph `Send` where payload-specific
|
||||
fanout matters.
|
||||
- Current helper: `src/openhuman/tinyagents/orchestration.rs`.
|
||||
- TinyAgents components: `Command::send`, `GraphInput`, `NodeContext::send_arg`.
|
||||
- Acceptance: map-reduce style flows can schedule multiple invocations of the
|
||||
same node with distinct payloads instead of materializing one node per item.
|
||||
|
||||
- [ ] Make `spawn_parallel_agents` a first-class TinyAgents graph tool.
|
||||
- Current OpenHuman files:
|
||||
`src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`,
|
||||
`src/openhuman/tinyagents/orchestration.rs`,
|
||||
`src/openhuman/agent_orchestration/worktree.rs`,
|
||||
`src/openhuman/agent_orchestration/workflow_runs/engine.rs`.
|
||||
- Current behavior: validates at least two tasks, checks parent context,
|
||||
enforces `max_parallel_tools`, resolves `AgentDefinition`s, enforces the
|
||||
parent `subagents.allowlist`, optionally creates per-worker git worktrees,
|
||||
fans workers out through `spawn_parallel_graph` + `map_reduce`, collects results in input
|
||||
order, emits `DomainEvent` + `AgentProgress`, detects stale parent file
|
||||
reads and cross-worker changed-file overlaps, and returns a structured
|
||||
`parallel_agents` JSON payload.
|
||||
- TinyAgents components: `GraphBuilder`, `Command`, `Send`,
|
||||
`NodeContext::send_arg`, `ChannelSet`/reducers, `GraphEventSink`,
|
||||
`SubAgentNode` or OpenHuman `run_subagent` adapter, `RecursionPolicy`,
|
||||
`RunPolicy`, `CancellationToken`.
|
||||
- Required migration shape:
|
||||
- `validate` node: parse tasks, enforce min/max count, parent context,
|
||||
allowlist, toolkit requirements, and worktree preflight.
|
||||
- `dispatch` node: use `Send` to schedule one worker invocation per task
|
||||
instead of generating one static worker node per task.
|
||||
- `worker` node: run the OpenHuman sub-agent build pipeline with inherited
|
||||
model/tool/security policy, optional `worktree_action_dir`, child task id,
|
||||
and bounded turn/output budgets.
|
||||
- `collect` reducer: aggregate successes, failures, elapsed time,
|
||||
iterations, worktree status, changed files, and stale read markers in
|
||||
deterministic task order.
|
||||
- `finalize` node: emit compatibility `DomainEvent`/`AgentProgress`
|
||||
projections, overlap warnings, and the existing JSON result shape.
|
||||
- Acceptance: parallel agent runs are checkpointable, cancellable at graph
|
||||
boundaries, bounded by parent policy, observable through TinyAgents graph
|
||||
events/status, compatible with existing `spawn_parallel_agents` tool
|
||||
output, and able to run edit-capable workers in isolated worktrees without
|
||||
silently falling back to shared workspace.
|
||||
|
||||
- [ ] Define parallel-agent ownership and scheduling policy explicitly.
|
||||
- OpenHuman files: `src/openhuman/agent_registry/agents/orchestrator/prompt.md`,
|
||||
`src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`,
|
||||
`src/openhuman/tools/traits.rs`.
|
||||
- TinyAgents components: graph route metadata, `RunPolicy`, task metadata,
|
||||
tool safety metadata.
|
||||
- Required policy:
|
||||
- The parent must provide disjoint ownership boundaries for write-capable
|
||||
tasks, or the graph rejects/falls back to serial delegation.
|
||||
- Read-only workers may share the parent workspace; write-capable workers
|
||||
should request `isolation = "worktree"`.
|
||||
- Parent and children inherit one root run id for cost/event rollups, but
|
||||
each child gets its own task id and optional worker-thread id.
|
||||
- A child cannot widen tools, model choice, sandbox mode, trusted roots, or
|
||||
budget beyond the parent-granted policy.
|
||||
- Cancellation, steering, and wait/collect must be delivered at graph or
|
||||
harness safe boundaries only.
|
||||
- Acceptance: the orchestrator can ask for parallelism without prompt-only
|
||||
conventions; policy violations become structured graph/tool errors.
|
||||
|
||||
- [ ] Make per-agent `graph.rs` selectors real customization points.
|
||||
- OpenHuman files: `src/openhuman/agent_registry/agents/*/graph.rs`,
|
||||
`src/openhuman/agent/harness/agent_graph.rs`.
|
||||
- TinyAgents components: `CompiledGraph`, sub-agent nodes, graph testkit.
|
||||
- Acceptance: at least three agents get bespoke graphs where useful:
|
||||
orchestrator, researcher, and tool_maker are good first candidates.
|
||||
|
||||
- [~] Keep durable orchestration stores OpenHuman-owned until the OpenHuman
|
||||
compatibility adapter is written.
|
||||
- Current OpenHuman files: `running_subagents.rs`, `workflow_runs`,
|
||||
`agent_teams`, `command_center`, `subagent_sessions`.
|
||||
- Updated TinyAgents status: current `main` has harness stores, JSONL append
|
||||
journals, lineage-aware harness/graph status, `SubAgentSession`, subgraph
|
||||
nodes, and sub-agent graph nodes. This means the blocker is no longer only
|
||||
"SDK lacks storage"; it is now the OpenHuman adapter/migration design and
|
||||
restart/resume parity.
|
||||
- Acceptance: migrate durable SQL/JSON state through a compatibility adapter,
|
||||
not by dropping records into in-memory task storage. OpenHuman controllers
|
||||
continue to read compatible projections while TinyAgents records become the
|
||||
canonical internal state.
|
||||
|
||||
- [ ] Add graph interrupt/resume for human review points.
|
||||
- OpenHuman files: `src/openhuman/approval/*`,
|
||||
`src/openhuman/agent_orchestration/workflow_runs/*`,
|
||||
`src/openhuman/tinyagents/delegation.rs`.
|
||||
- TinyAgents components: `Interrupt`, `ResumeTarget`, `Command::resume`,
|
||||
checkpoints.
|
||||
- Acceptance: approval/review pauses are durable graph interrupts where the
|
||||
run can resume from the exact checkpoint after user action.
|
||||
|
||||
## Phase 7 - Sub-Agents, Steering, And Recursion
|
||||
|
||||
- [ ] Represent `spawn_subagent`, `steer_subagent`, `wait_subagent`, and
|
||||
follow-ups as TinyAgents steering commands plus OpenHuman projections.
|
||||
- OpenHuman files: `src/openhuman/agent_orchestration/tools.rs`,
|
||||
`src/openhuman/agent_orchestration/running_subagents.rs`,
|
||||
`src/openhuman/agent/harness/subagent_runner/**`.
|
||||
- TinyAgents components: `SteeringCommand`, `SteeringHandle`,
|
||||
`SubAgentSession`, `SubAgentTool`, recursion depth events.
|
||||
- Acceptance: mid-flight messages are delivered only at safe loop boundaries,
|
||||
accepted/rejected steering emits events, and tool/model allowlists can only
|
||||
narrow from parent policy.
|
||||
|
||||
- [ ] Re-express the OpenHuman sub-agent pipeline as a TinyAgents subgraph.
|
||||
- OpenHuman files: `src/openhuman/agent/harness/subagent_runner/**`,
|
||||
`src/openhuman/agent_orchestration/tools/spawn_subagent.rs`,
|
||||
`src/openhuman/agent_orchestration/subagent_sessions/**`.
|
||||
- TinyAgents components: `harness::subagent::{SubAgent, SubAgentSession,
|
||||
SubAgentTool}`, `graph::subagent_node::{SubAgentNode, SubAgentPolicy}`,
|
||||
`graph::subgraph::{shared_subgraph_node, adapter_subgraph_node}`,
|
||||
`CapabilityRegistry`, graph status/observability.
|
||||
- Acceptance: definition resolution, tool filtering, prompt assembly,
|
||||
toolkit preflight, sandbox/action-root narrowing, handoff cache,
|
||||
checkpoint/awaiting-user handback, and worker-thread mirroring are explicit
|
||||
graph nodes or adapters. The final child run has TinyAgents lineage, status,
|
||||
usage/cost rollup, and transcript storage; OpenHuman keeps only product
|
||||
policy nodes and compatibility response formatting.
|
||||
|
||||
- [ ] Reconcile OpenHuman spawn depth with TinyAgents recursion policy.
|
||||
- OpenHuman files: `src/openhuman/agent/harness/spawn_depth_context.rs`,
|
||||
`src/openhuman/agent/harness/subagent_runner/**`.
|
||||
- TinyAgents components: `RecursionPolicy`, `RecursionStack`,
|
||||
`SubAgentDepth`.
|
||||
- Acceptance: there is one authoritative recursion cap and one error shape,
|
||||
with compatibility conversion for existing UI/JSON-RPC responses.
|
||||
|
||||
- [~] Keep OpenHuman's sub-agent build pipeline as product policy, but move the
|
||||
pipeline mechanics into TinyAgents graph/sub-agent primitives.
|
||||
- Product-owned pieces: agent definition resolution, prompt assembly, memory
|
||||
context, worker-thread mirroring, handoff cache, tool filtering, provider
|
||||
routing, sandbox scope.
|
||||
- TinyAgents components to adopt beneath it: `SubAgentSession`,
|
||||
`SubAgentTool`, `SubAgentNode`, `SubAgentPolicy`, `ToolExecutionContext`,
|
||||
event lineage, graph status, cancellation, usage/cost rollup.
|
||||
- Acceptance: use TinyAgents for execution, transcript/session tracking,
|
||||
graph structure, and lineage without flattening OpenHuman's agent registry
|
||||
semantics into generic SDK defaults.
|
||||
|
||||
## Phase 8 - Registry And Capability Catalog
|
||||
|
||||
- [ ] Build a `CapabilityRegistry` projection from OpenHuman registries.
|
||||
- OpenHuman files: `agent_registry`, `tools`, `mcp_registry`, `inference`,
|
||||
`cost`, `approval`, `security`, controller registry in `src/core/all.rs`.
|
||||
- TinyAgents components: `CapabilityRegistry`, `ComponentId`,
|
||||
`ComponentKind`, model catalog, graph/tool/agent/store/middleware entries.
|
||||
- Acceptance: OpenHuman models, tools, agents, graphs, stores, and middleware
|
||||
can be looked up through one policy-aware capability projection.
|
||||
|
||||
- [ ] Add registry diagnostics for duplicate names and unsafe aliases.
|
||||
- OpenHuman files: `src/openhuman/tools/generated.rs`,
|
||||
`mcp_registry`, `composio`, `agent_registry`.
|
||||
- TinyAgents components: component names, `ToolRegistry`, diagnostics.
|
||||
- Acceptance: duplicate tool names, provider-specific aliases, MCP names, and
|
||||
generated tool names fail closed before model dispatch.
|
||||
|
||||
- [ ] Use TinyAgents model catalog shape for local provider catalog snapshots.
|
||||
- OpenHuman files: `docs/inference-provider-catalog.md`,
|
||||
`src/openhuman/inference/presets.rs`,
|
||||
`src/openhuman/inference/provider/factory.rs`.
|
||||
- TinyAgents components: registry model catalog, local snapshots, price and
|
||||
context metadata.
|
||||
- Acceptance: model picker, router, budget estimator, and capability filter
|
||||
read one normalized catalog projection.
|
||||
|
||||
## Phase 9 - Memory, Retrieval, Embeddings, Context, And Cache
|
||||
|
||||
- [ ] Adapt OpenHuman memory/retrieval to TinyAgents retriever interfaces.
|
||||
- OpenHuman files: `memory`, `memory_search`, `memory_tree`,
|
||||
`agent_memory/memory_loader.rs`, `context/*`.
|
||||
- TinyAgents components: `EmbeddingModel`, `Retriever`, `VectorStore`,
|
||||
`ScoredDoc`, context events.
|
||||
- Acceptance: the agent harness can load retrieval context through a
|
||||
TinyAgents retriever facade while OpenHuman stores remain authoritative.
|
||||
|
||||
- [ ] Move context compaction provenance into TinyAgents events.
|
||||
- OpenHuman files: `context/README.md`, `tinyagents/summarize.rs`,
|
||||
`tinyagents/payload_summarizer.rs`.
|
||||
- TinyAgents components: `SummaryRecord`, `Compressed` events,
|
||||
`PromptCacheLayout`, cache layout events.
|
||||
- Acceptance: every summary records source ids, before/after token estimates,
|
||||
policy version, and whether stable prompt prefix was preserved.
|
||||
|
||||
- [ ] Normalize embedding usage/cost records.
|
||||
- OpenHuman files: `embeddings`, `memory_sync`, `cost`.
|
||||
- TinyAgents components: embedding usage fields, model catalog pricing.
|
||||
- Acceptance: embedding calls contribute usage and cost with provider/model,
|
||||
dimensions, vector count, and source.
|
||||
|
||||
## Phase 10 - Dead Code And TinyAgents Re-Expression Audit
|
||||
|
||||
Do not delete these blindly. Treat this as an audit list for code that is dead,
|
||||
vestigial, or generic runtime behavior now expressible through TinyAgents
|
||||
harness/graph primitives. Delete only after call-site search, compatibility
|
||||
assessment, and migration coverage are complete.
|
||||
|
||||
- [ ] Audit `src/openhuman/agent/harness/engine/*`.
|
||||
- Current role: surviving seams from the retired in-house turn loops:
|
||||
`CheckpointStrategy`, `ProgressReporter`, and `TurnProgress`.
|
||||
- TinyAgents expression: `RunPolicy`, `AgentEvent`, `GraphEvent`,
|
||||
`EventSink`, `HarnessStatusStore`, cap/stop middleware.
|
||||
- Candidate outcome: move max-iteration and progress projection into
|
||||
TinyAgents middleware/events, then delete `engine/*` if no product-specific
|
||||
compatibility seam remains.
|
||||
|
||||
- [ ] Audit `src/openhuman/agent/harness/agent_graph.rs` and built-in
|
||||
`agent_registry/agents/*/graph.rs` default selectors.
|
||||
- Current role: per-agent graph hook, but most built-ins return
|
||||
`AgentGraph::Default`.
|
||||
- TinyAgents expression: a registry of `CompiledGraph`/graph factories keyed
|
||||
by agent id, with default graph supplied by the runtime and custom graphs
|
||||
registered only where they differ.
|
||||
- Candidate outcome: replace dozens of boilerplate default `graph.rs` files
|
||||
with registry defaults, keeping files only for agents with custom graphs.
|
||||
|
||||
- [x] Audit stale architecture references to removed in-house graph/loop code.
|
||||
- Current files: `gitbooks/developing/architecture/agent-harness.md`,
|
||||
`src/openhuman/context/README.md`.
|
||||
- Candidate stale names: `src/openhuman/agent_graph/`, `GraphBlueprint`,
|
||||
`run_turn_engine`, `run_tool_call_loop`, `harness/tool_loop.rs`, old
|
||||
context summarizer files.
|
||||
- TinyAgents expression: link to live `src/openhuman/tinyagents/*`,
|
||||
`GraphBuilder`, `AgentHarness`, `ContextCompressionMiddleware`, and
|
||||
graph-export/status surfaces.
|
||||
- Candidate outcome: move historical details into a short "pre-migration
|
||||
history" appendix or remove them from active architecture docs.
|
||||
- **Partial:** flagged the `agent-harness.md` `agent_graph`/`GraphBlueprint`
|
||||
section as HISTORICAL-removed (strong inline callout pointing at the live
|
||||
tinyagents surfaces); fixed `context/README.md` "Used by" line that still
|
||||
referenced the deleted `reduce_before_call`/`ProviderSummarizer`/
|
||||
`SegmentRecapSummarizer`/`unified_compaction_enabled`. **Sweep completed:**
|
||||
every code doc-comment that described `run_turn_engine`/`run_tool_call_loop`/
|
||||
`tool_loop.rs` as *current* behavior (≈20 sites across 15 files: tools,
|
||||
security, tokenjuice, triage, orchestration steering, task-local contexts,
|
||||
cron, host_runtime, event-bus example, test-file headers) now points at the
|
||||
live tinyagents surfaces; intentionally-historical "legacy X was removed /
|
||||
parity with" notes were kept. Domain READMEs (tools, tokenjuice, approval)
|
||||
fixed too — the tokenjuice one now records that `compact_tool_output` lost
|
||||
its only production caller with the retired loop (re-wiring it as an
|
||||
`after_tool` middleware; the stale default-Full wrapper is now deleted.
|
||||
|
||||
- [ ] Audit `src/openhuman/context/{pipeline,guard,microcompact}.rs`.
|
||||
- Current role: context stats/session-memory bookkeeping plus older
|
||||
compaction concepts; live history reduction moved to
|
||||
`ContextCompressionMiddleware` and `MessageTrimMiddleware`.
|
||||
- TinyAgents expression: context-window middleware, `Compressed` events,
|
||||
`PromptCacheLayout`, cache layout events, `SummaryRecord`,
|
||||
usage/context pressure status.
|
||||
- Candidate outcome: keep only stats/session-memory state that remains
|
||||
OpenHuman-specific; move compression policy/provenance into TinyAgents
|
||||
middleware and delete unused reduction paths.
|
||||
|
||||
- [ ] Audit `src/openhuman/tinyagents/payload_summarizer.rs`.
|
||||
- Current role: oversized tool-result compression via a `summarizer`
|
||||
sub-agent with a local circuit breaker.
|
||||
- TinyAgents expression: `ToolMiddleware::after_tool`,
|
||||
`ContextCompressionMiddleware`, `SummaryRecord`, tool artifact/result
|
||||
compaction events.
|
||||
- Candidate outcome: convert to middleware over TinyAgents tool results so
|
||||
the summarizer is no longer a separate OpenHuman-only hook.
|
||||
|
||||
- [ ] Audit `src/openhuman/agent_orchestration/running_subagents.rs`.
|
||||
- Current role: bespoke live-task registry layered on TinyAgents
|
||||
`InMemoryTaskStore`, plus watch channels, abort handles, tombstones,
|
||||
task/session lookup, wait, steer, and cancel operations.
|
||||
- TinyAgents expression: `TaskStore`, `SteeringCommand`, `SteeringHandle`,
|
||||
`CancellationToken`, run tree/status store, durable OpenHuman ledger
|
||||
projections.
|
||||
- Candidate outcome: keep OpenHuman durable session/worker-thread records,
|
||||
but collapse transient lifecycle mechanics into TinyAgents task/status
|
||||
primitives once they can represent wait/steer/hard-abort needs.
|
||||
|
||||
- [ ] Audit `src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs`
|
||||
after the graph-tool migration.
|
||||
- Current role: validation, preflight, fanout, worktree setup, event
|
||||
projection, overlap detection, result formatting.
|
||||
- TinyAgents expression: graph nodes (`validate`, `dispatch`, `worker`,
|
||||
`collect`, `finalize`) with `Send` fanout and reducers.
|
||||
- Candidate outcome: keep a thin tool wrapper that invokes the graph and
|
||||
formats the existing JSON payload; move orchestration mechanics into the
|
||||
graph module.
|
||||
|
||||
- [ ] Audit hand-rolled `join_all` fanouts that are workflow orchestration, not
|
||||
simple IO batching.
|
||||
- Candidate files from current search: `src/openhuman/mcp_registry/registry.rs`,
|
||||
`src/openhuman/learning/reflection.rs`,
|
||||
`src/openhuman/inference/local/service/ollama_admin/diagnostics.rs`.
|
||||
- TinyAgents expression: only migrate fanouts that need agent/run lineage,
|
||||
checkpointing, policy, cancellation, or graph observability. Leave simple
|
||||
independent IO probes as ordinary Rust concurrency.
|
||||
- Candidate outcome: document why each fanout stays as `join_all` or move it
|
||||
to `graph::parallel::map_reduce` / graph `Send`.
|
||||
|
||||
- [x] Audit tool registry comments and docs that still describe retired
|
||||
direct-loop behavior.
|
||||
- Current files: `src/openhuman/tools/traits.rs`,
|
||||
`src/openhuman/tools/README.md`,
|
||||
`src/openhuman/agent/harness/session/turn/tools.rs`.
|
||||
- TinyAgents expression: `ToolRegistry`, `ToolMiddleware`, graph tool nodes,
|
||||
tool safety metadata, `ToolExecutionContext`.
|
||||
- Candidate outcome: update comments to describe the TinyAgents execution
|
||||
path and delete references to the retired serial `harness::tool_loop`
|
||||
dispatcher once no code path uses it.
|
||||
- **Done:** `tools/traits.rs` concurrency note and `tools/README.md` "Used
|
||||
by" now describe the tinyagents execution path (`SharedToolAdapter` /
|
||||
`ToolPolicyMiddleware`); `session/turn/tools.rs` had no stale references.
|
||||
|
||||
## Phase 11 - Testing And Conformance
|
||||
|
||||
- [ ] Create a focused parity test matrix for the current TinyAgents route.
|
||||
- OpenHuman files: `src/openhuman/tinyagents/tests.rs`,
|
||||
`tests/agent_harness_e2e.rs`, `tests/agent_tool_loop_raw_coverage_e2e.rs`.
|
||||
- TinyAgents components: `AgentHarness`, `AgentEvent`, `ToolRegistry`,
|
||||
`ModelRegistry`, `MessageTrimMiddleware`, `ContextCompressionMiddleware`.
|
||||
- Acceptance: chat turn, channel turn, sub-agent turn, unknown tool recovery,
|
||||
approval denial, streaming text, streaming tool args, reasoning deltas,
|
||||
early-exit pause, model-call cap, and cost footer all have tests on the
|
||||
TinyAgents path.
|
||||
|
||||
- [x] Add a TinyAgents adapter inventory test.
|
||||
- OpenHuman files: `src/openhuman/tinyagents/mod.rs`,
|
||||
`src/openhuman/tinyagents/tests.rs`.
|
||||
- Acceptance: one test asserts that the shared runner registers model, tools,
|
||||
middleware, event bridge, context compression, stop hooks, and unknown-tool
|
||||
policy in the intended order.
|
||||
- **Done:** harness assembly extracted from `run_turn_via_tinyagents_shared`
|
||||
into a testable `assemble_turn_harness` returning `AssembledTurnHarness`
|
||||
(harness + cursor/error-slot/halt-summary/outcome-sink/steering/early-exit
|
||||
seams). `adapter_inventory_registers_model_tools_and_middleware` asserts
|
||||
model registry, callable tools + unknown-tool policy, 9 lifecycle + 2
|
||||
around-tool middlewares, steering handle, early-exit hook;
|
||||
`adapter_inventory_gates_context_middleware_on_window` proves the
|
||||
compression/trim gating. SDK gap: `MiddlewareStack` exposes lengths but not
|
||||
names, so exact ordering is documented at the registration sites and
|
||||
guarded by counts.
|
||||
|
||||
- [ ] Port behavior clusters to TinyAgents testkit.
|
||||
- OpenHuman files: `src/openhuman/agent/harness/*_tests.rs`,
|
||||
`tests/agent_*`.
|
||||
- TinyAgents components: harness `testkit`, graph `assert_graph`,
|
||||
`GraphEventRecorder`, mock models/tools.
|
||||
- Acceptance: legacy assertions over loop wording are replaced by assertions
|
||||
over TinyAgents events, checkpoints, graph metadata, and final transcript.
|
||||
|
||||
- [ ] Add cross-module e2e tests for graph/sub-agent/model/tool composition.
|
||||
- Candidate tests: workflow run with child sub-agents, delegation with review
|
||||
loop, council fanout, MCP tool call, Composio approval denial, memory
|
||||
retrieval plus summarization.
|
||||
- Acceptance: tests cover default features, `openai`-feature compatible code
|
||||
paths where available, and all-features where dependency constraints allow.
|
||||
|
||||
- [ ] Add fuzz-style graph composition tests.
|
||||
- TinyAgents components: graph testkit, reducers, command routing, subgraph
|
||||
nodes, sub-agent fake nodes.
|
||||
- Acceptance: generated small graphs cover direct edges, conditional routes,
|
||||
`Send` fanout, joins, interrupts, recursion caps, and checkpoint resume.
|
||||
|
||||
## Suggested Execution Order
|
||||
|
||||
1. Confirm version/feature compatibility and SDK-extension gaps.
|
||||
2. Move tool safety/approval/security into TinyAgents middleware.
|
||||
3. Normalize usage/cost records and parent/child rollups.
|
||||
4. Persist TinyAgents event/status journals to the OpenHuman run ledger.
|
||||
5. Build the OpenHuman `CapabilityRegistry` projection.
|
||||
6. Convert one high-value built-in agent to a bespoke TinyAgents graph.
|
||||
7. Adapt memory/retrieval/context surfaces where the TinyAgents interfaces are
|
||||
a good fit.
|
||||
8. Audit and remove or re-express dead/vestigial runtime code through
|
||||
TinyAgents harness/graph primitives.
|
||||
9. Finish with parity, adapter-inventory, conformance, e2e, and fuzz-style tests.
|
||||
|
||||
## Non-Goals For The First Migration Wave
|
||||
|
||||
- Do not replace OpenHuman's durable run ledgers with TinyAgents SQLite storage
|
||||
until the one-time
|
||||
transcript/session migration, TinyAgents store/status adapter, and
|
||||
restart/resume parity tests are complete.
|
||||
- Do not expose TinyAgents' OpenAI provider directly to product code while
|
||||
OpenHuman provider config, credentials, OAuth, and billing classification are
|
||||
still the product source of truth.
|
||||
- Do not remove `DomainEvent` until all existing subscribers have a TinyAgents
|
||||
event/status replacement.
|
||||
@@ -1,360 +0,0 @@
|
||||
# TinyAgents SDK Gaps
|
||||
|
||||
This document lists TinyAgents SDK features that are missing or only partially
|
||||
available from the perspective of migrating OpenHuman's Rust agent core onto
|
||||
TinyAgents.
|
||||
|
||||
Scope:
|
||||
|
||||
- Original source baseline: local TinyAgents checkout at `6f898fb`.
|
||||
- Refresh note: TinyAgents 1.3.0 was re-verified from the published crate
|
||||
source. Several older "missing" items are now shipped in 1.2.0-1.3.0:
|
||||
tool policy metadata, recoverable unknown-tool calls, reasoning/tool-call
|
||||
stream deltas, orchestration task stores, budget reservation/reconciliation,
|
||||
ordered parallel map/reduce, sub-agent steering/task controls, workspace
|
||||
isolation hooks, and middleware control outcomes. The backlog below keeps only
|
||||
the residual OpenHuman migration pressure and SDK surface gaps.
|
||||
- OpenHuman evidence: `src/openhuman/tinyagents/*`,
|
||||
`src/openhuman/agent/*`, `src/openhuman/cost/*`, and
|
||||
`src/openhuman/tokenjuice/*`.
|
||||
- This is not the OpenHuman migration plan. That plan lives in
|
||||
`docs/tinyagents-migration-spec.md`.
|
||||
- Items here are upstream TinyAgents implementation candidates.
|
||||
- Tests should follow each migrated surface; only broad end-to-end parity suites
|
||||
should wait for the final cutover.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
TinyAgents already has strong primitives for harness runs, graph execution,
|
||||
middleware, event streams, model profiles, usage/cost accounting, checkpointers,
|
||||
policy metadata, recoverable tool-call behavior, workspace isolation, and
|
||||
sub-agent orchestration. The biggest remaining gaps are now narrower:
|
||||
OpenHuman adapter migration, transcript/session migration, richer replay and
|
||||
redaction rules, model/provider catalog integration, registry diagnostics, and a
|
||||
few still-missing SDK fields.
|
||||
|
||||
OpenHuman can migrate more of `src/openhuman/agent/` by adopting the newer
|
||||
TinyAgents surfaces and filling the remaining gaps:
|
||||
|
||||
- A free-form metadata map on `ToolSchema` for model-visible schema
|
||||
annotations; SDK-owned enforcement metadata now lives in `ToolPolicy`.
|
||||
- A reasoning field on the middleware-facing `harness::model::ModelDelta`;
|
||||
`AgentEvent::ModelDelta` already carries nested `MessageDelta.reasoning`.
|
||||
- A one-time migration path from old OpenHuman `session_raw/*.jsonl` and
|
||||
Markdown transcripts into TinyAgents store/journal/status records.
|
||||
- Production replay rules over TinyAgents stores/status: redaction, cursors,
|
||||
backfill, cancellation, and OpenHuman controller compatibility.
|
||||
- Storage compatibility options for SQLite users that already own a connection,
|
||||
schema, or native sqlite patch policy.
|
||||
- A `root_run_id` field on harness `RunConfig`; lineage exists in graph and
|
||||
observability records, but not on the harness config itself.
|
||||
- A money/USD field on `Usage`; token usage and cost totals remain separate.
|
||||
- Provider/model catalog metadata that can drive preflight, fallback, and
|
||||
reconciliation.
|
||||
- Conformance suites for providers, tools, middleware, graph stores, and
|
||||
checkpointers.
|
||||
|
||||
## Backlog
|
||||
|
||||
### 1. Rich Tool Policy Metadata
|
||||
|
||||
Status: shipped in 1.3.0; residual schema metadata gap remains.
|
||||
|
||||
TinyAgents now has SDK-owned `ToolPolicy`, `ToolSideEffects`, `ToolRuntime`,
|
||||
`ToolAccess`, `WorkspaceAccess`, `SandboxMode`, `Tool::policy`, registry policy
|
||||
snapshots, and `ToolPolicyMiddleware`. Strict policy can fail closed on
|
||||
unclassified tools, enforce sandbox and result-byte requirements, and keep plain
|
||||
`ToolSchema` as the model-visible projection.
|
||||
|
||||
Residual:
|
||||
|
||||
- `ToolSchema` still has no free-form metadata map for model-visible schema
|
||||
annotations or app-specific hints. Use `ToolPolicy` for enforcement metadata
|
||||
and keep this as the remaining SDK shape gap.
|
||||
- OpenHuman still needs to map domain tool registry metadata into
|
||||
`Tool::policy` snapshots before deleting adapter-local policy plumbing.
|
||||
|
||||
### 2. Recoverable Unknown Tool Calls
|
||||
|
||||
Status: shipped in 1.3.0.
|
||||
|
||||
TinyAgents now has `UnknownToolPolicy::{Fail, ReturnToolError, Rewrite}` and
|
||||
emits `AgentEvent::UnknownToolCall` with the original requested name and
|
||||
arguments. This distinguishes "tool not found" from "tool executed and failed"
|
||||
and lets a run keep going so the model can correct itself.
|
||||
|
||||
OpenHuman follow-up:
|
||||
|
||||
- Replace the adapter-local `__openhuman_unknown_tool__` sentinel with
|
||||
`UnknownToolPolicy` once the surrounding compatibility surface is migrated.
|
||||
|
||||
### 3. Reasoning And Tool-Argument Streaming
|
||||
|
||||
Status: shipped in 1.3.0; residual middleware event-shape gap remains.
|
||||
|
||||
TinyAgents `MessageDelta` now carries provider-neutral `text`, `reasoning`, and
|
||||
`tool_call` channels, and `AgentEvent::ModelDelta` carries the delta with an
|
||||
explicit `run_id` and `call_id`.
|
||||
|
||||
Residual:
|
||||
|
||||
- The middleware-facing `harness::model::ModelDelta` still has no `reasoning`
|
||||
field, and the agent loop drops reasoning when converting stream items into
|
||||
that middleware shape. Event consumers can read reasoning from
|
||||
`AgentEvent::ModelDelta.delta.reasoning`.
|
||||
- OpenHuman still needs to route provider reasoning/tool-argument deltas through
|
||||
the TinyAgents stream before deleting UI-specific forwarding shims.
|
||||
|
||||
### 4. Durable Orchestration Task Store
|
||||
|
||||
Status: shipped as SDK primitives in 1.3.0; OpenHuman migration remains.
|
||||
|
||||
TinyAgents now has `TaskStore`, `InMemoryTaskStore`, `JsonlTaskStore`,
|
||||
`OrchestrationTaskRecord`, lifecycle transitions, cancel/kill outcomes,
|
||||
filters, graph/harness status with lineage, `graph::subagent_node`, and
|
||||
`graph::subgraph`.
|
||||
|
||||
OpenHuman follow-up:
|
||||
|
||||
- Map durable sub-agent session rows and worker-thread records into TinyAgents
|
||||
task/status/journal records while preserving controller compatibility.
|
||||
- Retire bespoke task status/tombstone persistence in `running_subagents.rs`
|
||||
only after restart/replay behavior is projected through the SDK records.
|
||||
|
||||
### 5. SQLite Storage Compatibility
|
||||
|
||||
Status: partially present.
|
||||
|
||||
TinyAgents 1.3 has `SqliteCheckpointer`, `from_connection`, and `schema_sql`,
|
||||
and OpenHuman now enables the `sqlite` feature by aligning both Cargo worlds on
|
||||
`rusqlite 0.40` / `libsqlite3-sys 0.38`. The remaining gap is not feature
|
||||
enablement or basic schema access; it is ownership. OpenHuman still patches the
|
||||
sqlite crates locally for the current toolchain, owns existing session/checkpoint
|
||||
tables through `SqlRunLedgerCheckpointer`, and needs a clean way to adopt or
|
||||
bridge SDK checkpoint storage without surrendering dependency or schema control.
|
||||
|
||||
Implement one or more OpenHuman compatibility paths:
|
||||
|
||||
- Provide a version-flexible storage layer, possibly via `sqlx` or a separate
|
||||
crate feature matrix.
|
||||
- Expose a small `CheckpointStore` persistence trait below `Checkpointer`.
|
||||
- Add an adapter/cutover path that can project OpenHuman run-ledger checkpoints
|
||||
into SDK checkpoint storage without breaking existing resume semantics.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Applications that already own SQLite can use TinyAgents durable checkpoints
|
||||
without native-link conflicts.
|
||||
- OpenHuman can replace `SqlRunLedgerCheckpointer` with an SDK-supported adapter
|
||||
or a thin schema integration.
|
||||
- Storage features remain opt-in and keep the default crate dependency-light.
|
||||
|
||||
### 6. Production Event And Status Journals
|
||||
|
||||
Status: partially present.
|
||||
|
||||
TinyAgents has `HarnessEventJournal`, `StoreEventJournal`, `HarnessStatusStore`,
|
||||
and `HarnessRunStatus`. OpenHuman still bridges TinyAgents events into its own
|
||||
progress system, cost tracker, run ledger, and UI status stream.
|
||||
|
||||
Implement:
|
||||
|
||||
- Durable event journals with cursors, replay windows, filters, compaction, and
|
||||
redaction hooks.
|
||||
- Status stores with parent/root lineage, thread-scoped listing, phase details,
|
||||
active tool/model call ids, usage totals, cost totals, and terminal summaries.
|
||||
- Event filters for UI surfaces: text stream only, tool timeline, cost updates,
|
||||
graph lifecycle, errors, task lifecycle.
|
||||
- Redaction policies for prompts, tool args, tool results, PII, secrets, and
|
||||
provider payloads.
|
||||
- Stable event ids and offset semantics across process restarts.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- A UI can attach late and reconstruct a run without subscribing at start time.
|
||||
- A supervisor can query every active descendant of a root run.
|
||||
- OpenHuman event bridges become mostly format adapters, not state owners.
|
||||
|
||||
### 7. Cost, Usage, And Budget Enforcement
|
||||
|
||||
Status: shipped in 1.3.0; residual money field gap remains.
|
||||
|
||||
TinyAgents now has `Usage`, `UsageTotals`, `CostTotals`,
|
||||
`BudgetLimits.max_cached_input_tokens`, budget middleware, and
|
||||
`AgentEvent::{BudgetReserved, BudgetReconciled, BudgetWarning, BudgetExceeded}`.
|
||||
The SDK can preflight, reserve, enforce, and reconcile token budgets.
|
||||
|
||||
Residual:
|
||||
|
||||
- `Usage` still has no USD/money field. Token usage and money remain separate
|
||||
(`Usage`/`UsageTotals` vs. `CostTotals`), so OpenHuman cost UI still needs a
|
||||
projection that joins token usage with pricing/cost records.
|
||||
|
||||
### 8. Model Catalog And Provider Resolution
|
||||
|
||||
Status: partially present.
|
||||
|
||||
TinyAgents has `ModelProfile`, including provider, model, modalities, tool
|
||||
calling, streaming, structured output, reasoning, and token windows. OpenHuman
|
||||
still has provider catalog logic and local model capability inference that drive
|
||||
fallback, token budgeting, and routing.
|
||||
|
||||
Implement:
|
||||
|
||||
- SDK-owned model catalog snapshots with provider, model id, display name,
|
||||
lifecycle status, context windows, modalities, streaming support, reasoning,
|
||||
structured-output support, and pricing keys.
|
||||
- Capability-driven model resolution: required capabilities, fallback chains,
|
||||
local/cloud preferences, and provider health.
|
||||
- Runtime profile discovery hooks for local models.
|
||||
- Pricing table integration that maps `ModelProfile` to `CostTotals`.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Model selection can be expressed in TinyAgents policy instead of
|
||||
OpenHuman-only routing code.
|
||||
- Fallback can reject models that lack required tool, vision, structured-output,
|
||||
context-window, or reasoning capabilities.
|
||||
- Token budgeting can use the resolved model's real context window.
|
||||
|
||||
### 9. Dynamic Tool Exposure And Allowlist Policy
|
||||
|
||||
Status: partially present.
|
||||
|
||||
TinyAgents can run with a provided tool registry, but OpenHuman needs per-agent,
|
||||
per-tier, per-sub-agent, and per-task allowlists. Tool visibility depends on
|
||||
security tier, workspace roots, parent/child delegation policy, model
|
||||
capabilities, and whether the run is background or interactive.
|
||||
|
||||
Implement:
|
||||
|
||||
- A tool selection middleware that receives run context, agent identity, task
|
||||
kind, parent policy, and model profile.
|
||||
- Allowlist/denylist composition with explicit inheritance rules.
|
||||
- Explainable exposure decisions for audit/debugging.
|
||||
- Fail-closed behavior when policy metadata is missing.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Sub-agents inherit only the tools they are allowed to call.
|
||||
- Tool exposure decisions are visible in run events or observations.
|
||||
- OpenHuman can remove adapter-local allowlist enforcement from most call paths.
|
||||
|
||||
### 10. Graph Fanout And Parallel Agent Ergonomics
|
||||
|
||||
Status: map/reduce helper shipped in 1.2.1-1.3.0; OpenHuman builder migration
|
||||
remains.
|
||||
|
||||
TinyAgents now has ordered `map_reduce`, `FailurePolicy`, `ParallelOptions`,
|
||||
max concurrency, per-item timeout, total timeout, and cooperative cancellation.
|
||||
OpenHuman council runs and `spawn_parallel_agents` can use this helper directly.
|
||||
|
||||
Residual:
|
||||
|
||||
- The higher-level parallel-agent builder remains OpenHuman-specific policy
|
||||
glue: task validation, `Send` dispatch shape, result envelopes, usage/cost
|
||||
merging, and ownership/worktree policy adapters.
|
||||
|
||||
### 11. Sub-Agent Steering, Waiting, And Reuse
|
||||
|
||||
Status: SDK primitives shipped in 1.3.0; OpenHuman lifecycle projection remains.
|
||||
|
||||
TinyAgents now has sub-agent sessions/tools, steering, task stores, cancel/kill
|
||||
control outcomes, graph/harness lineage, and reusable child-run primitives.
|
||||
|
||||
OpenHuman follow-up:
|
||||
|
||||
- Project existing detached run tracking, wait handles, user-facing
|
||||
cancellation, early-exit handling, and parent-child progress aggregation onto
|
||||
TinyAgents task/status records before reducing `running_subagents.rs`.
|
||||
|
||||
### 12. Workspace Isolation And Sandbox Hooks
|
||||
|
||||
Status: shipped in 1.3.0; OpenHuman policy integration remains.
|
||||
|
||||
TinyAgents now has `WorkspaceDescriptor`, `WorkspaceIsolation`,
|
||||
`SharedRootWorkspace`, sandbox descriptors, `ToolExecutionContext.workspace`,
|
||||
and `WorkspaceDescriptor::enforce(path, events)` which emits
|
||||
`AgentEvent::WorkspaceViolation` and fails closed when a path leaves the allowed
|
||||
roots.
|
||||
|
||||
OpenHuman follow-up:
|
||||
|
||||
- Implement OpenHuman's action-root, trusted-root, internal-workspace, worktree,
|
||||
sandbox, and command-tier policy as a `WorkspaceIsolation` provider and tool
|
||||
middleware projection.
|
||||
|
||||
### 13. Middleware Control Outcomes
|
||||
|
||||
Status: shipped in 1.3.0.
|
||||
|
||||
TinyAgents now has `MiddlewareControl::{StopWithFinal, Interrupt}`,
|
||||
`RunContext::request_control`, precedence handling via
|
||||
`MiddlewareControl::precedence()`, stable `kind()` labels, and
|
||||
`AgentEvent::ControlApplied` so control decisions are visible in journals.
|
||||
|
||||
OpenHuman follow-up:
|
||||
|
||||
- Route early-exit tools and budget stop hooks through `MiddlewareControl`
|
||||
before deleting adapter-local steering side channels.
|
||||
|
||||
### 15. Registry Diagnostics And Introspection
|
||||
|
||||
Status: partially present.
|
||||
|
||||
TinyAgents has registry primitives. OpenHuman still needs richer diagnostics for
|
||||
duplicate components, alias resolution, component health, model/provider/tool
|
||||
capabilities, and event listener wiring.
|
||||
|
||||
Implement:
|
||||
|
||||
- Registry snapshot export with models, tools, middleware, graph nodes,
|
||||
checkpointers, task stores, event listeners, and aliases.
|
||||
- Duplicate and shadowing diagnostics.
|
||||
- Health/status probes for registered providers and stores.
|
||||
- Machine-readable component dependency graph.
|
||||
- Optional DOT/JSON graph export for runtime components, not only graph nodes.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- A CLI or UI can show exactly what TinyAgents components are active.
|
||||
- Registry failures are actionable without inspecting app-specific logs.
|
||||
- OpenHuman dead-code audits can map old modules to SDK-owned registry entries.
|
||||
|
||||
### 17. Storage And Graph Conformance
|
||||
|
||||
Status: partially present.
|
||||
|
||||
TinyAgents 1.3 includes storage conformance coverage for built-in checkpointers
|
||||
and task stores, including SQLite under the feature. Durable OpenHuman adapters
|
||||
and fuller graph behavior are still hard to migrate safely without shared
|
||||
contract coverage.
|
||||
|
||||
Implement:
|
||||
|
||||
- Checkpointer conformance for OpenHuman adapters and caller-supplied stores.
|
||||
- TaskStore conformance for lifecycle transitions, filters, cancellation,
|
||||
timeout, kill, restart/replay, and concurrent writes.
|
||||
- Graph conformance for `Send`, reducers, interrupts, resume, max concurrency,
|
||||
dynamic routing, fanout failure policy, and deterministic result collection.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Storage adapters can be swapped without changing graph behavior.
|
||||
- Durable interrupt/resume semantics are proven across backends.
|
||||
- Parallel-agent helpers have regression tests for order, failure, timeout, and
|
||||
cancellation.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Define API contracts for tool policy, unknown-tool handling, streaming delta
|
||||
channels, durable task storage, storage adapters, and control outcomes.
|
||||
2. Implement the lowest-level data types and traits behind non-breaking
|
||||
defaults.
|
||||
3. Add in-memory implementations first.
|
||||
4. Add durable stores and compatibility adapters second.
|
||||
5. Add middleware helpers and high-level graph helpers.
|
||||
6. Migrate OpenHuman adapters to the new SDK surfaces.
|
||||
7. Remove OpenHuman-specific compatibility shims once the SDK behavior is
|
||||
equivalent.
|
||||
8. Implement conformance and regression tests last.
|
||||
@@ -1,246 +0,0 @@
|
||||
# TinyAgents Session Migration Design
|
||||
|
||||
Date: 2026-07-01
|
||||
|
||||
Status: Phase 1 implemented (2026-07-01) in `src/openhuman/session_import/`
|
||||
as the `openhuman.session_import_run` controller
|
||||
(`openhuman-core session-import run`). Phases 2–4 (read-side shadow, cutover,
|
||||
retirement) are not started. Implementation deviations from the original
|
||||
sketch are marked "as built" below.
|
||||
|
||||
Goal: a one-time, idempotent migration of persisted OpenHuman session data —
|
||||
transcript JSONL, legacy Markdown transcripts, and run-ledger/sub-agent rows —
|
||||
into TinyAgents store/journal records, so new internals can read by TinyAgents
|
||||
`thread_id` / `run_id` / stream offset while legacy surfaces keep answering by
|
||||
OpenHuman session key.
|
||||
|
||||
## Source inventory (what exists on disk today)
|
||||
|
||||
All facts verified against the current checkout (TinyAgents 1.5.0 pinned with the
|
||||
`sqlite` feature enabled).
|
||||
|
||||
### 1. Transcript JSONL (source of truth)
|
||||
|
||||
- Path: `{workspace}/session_raw/{stem}.jsonl`
|
||||
(writer/reader: `src/openhuman/agent/harness/session/transcript.rs`).
|
||||
- Legacy layout still readable: `session_raw/{DDMMYYYY}/{stem}.jsonl`
|
||||
(pre-0.53.4 date folders). A layout migration already exists
|
||||
(`session/migration.rs`, marker `state/migrations/session_layout_v1.done`).
|
||||
- Stem encodes identity and lineage:
|
||||
- root session: `{unix_ts}_{agent_id}`;
|
||||
- sub-agent: `{parent_chain}__{unix_ts}_{agent_id}` — the `__` chain is the
|
||||
only parent/child link on disk; there is no pointer file.
|
||||
- Line 1 is a `_meta` header (`MetaPayload`): `agent`, optional `agent_id` /
|
||||
`agent_type` / `provider` / `model` / `thread_id` / `task_id`, `dispatcher`,
|
||||
`created`, `updated`, `turn_count`, `input_tokens`, `output_tokens`,
|
||||
`cached_input_tokens`, `charged_amount_usd`.
|
||||
- Remaining lines are `MessageLine`s: required `role` + `content`; optional
|
||||
`id`, `extra_metadata`, `provider`, `model`, `usage` (`input`, `output`,
|
||||
`cached_input`, `context_window`, `cost_usd`), `reasoning_content`,
|
||||
`tool_calls` (`id`, `name`, `arguments` as raw JSON string, optional
|
||||
`extra_content`), `iteration`, `ts` (RFC-3339). Only the last assistant
|
||||
message of each turn carries the per-turn fields.
|
||||
- Tool-call encoding varies by the `_meta.dispatcher` value:
|
||||
- native: structured `tool_calls` array on the assistant line;
|
||||
- XML / P-format: markup (`<tool_call>…</tool_call>`, `name[a|b]`) embedded
|
||||
verbatim in `content` — never re-parsed on resume today.
|
||||
|
||||
### 2. Markdown transcripts
|
||||
|
||||
- Human-readable companion: `{workspace}/sessions/{YYYY_MM_DD}/{stem}.md`
|
||||
(legacy `sessions/{DDMMYYYY}/`). Never read back except by the one-release
|
||||
legacy reader `read_transcript_legacy_md()` (HTML-comment `<!--MSG …-->`
|
||||
format). Treat `.md` as a **fallback source only** when a stem has no JSONL.
|
||||
|
||||
### 3. Run ledger (SQLite `{workspace}/session_db/sessions.db`)
|
||||
|
||||
- `agent_runs`: id, kind (subagent | worker_thread | background_agent |
|
||||
team_member | workflow_child), parent_run_id, parent_thread_id, agent_id,
|
||||
status, worker_thread_id, task ids, checkpoint refs, metadata, timestamps.
|
||||
- `run_events` (`run_id` + `sequence` → `event_type`, `payload_json`) and
|
||||
`run_telemetry` (per-run token/cost roll-up).
|
||||
- `graph_checkpoints` (written by `SqlRunLedgerCheckpointer`,
|
||||
`src/openhuman/tinyagents/checkpoint.rs`): seq, thread_id, checkpoint_id,
|
||||
run_id, record_json (a full tinyagents `Checkpoint<State>`), created_at.
|
||||
- **No table stores the transcript stem/path.** Ledger row ↔ transcript file
|
||||
correlation is by convention via `thread_id` (+ `task_id`).
|
||||
|
||||
### 4. Durable sub-agent sessions (JSON blob)
|
||||
|
||||
- `{workspace}/.openhuman/subagent_sessions.json`: a single pretty-printed
|
||||
`Vec<DurableSubagentSession>` — `subagentSessionId`, `parentSession`
|
||||
(parent session_key), `parentThreadId`, `workerThreadId`, `agentId`,
|
||||
toolkit/model/sandbox/action-root selector fields, `status`, `reusable`,
|
||||
inline `latestHistory` message mirror, timestamps.
|
||||
|
||||
## Target shape (TinyAgents 1.3+ primitives)
|
||||
|
||||
Use the crate's `harness::store` as the substrate — no new storage layer:
|
||||
|
||||
- `AppendStore` / `JsonlAppendStore`: one line per `StoreRecord { offset,
|
||||
value, created_at_ms }`, offset = line index. Streams live under a store
|
||||
root as `<stream>.jsonl`.
|
||||
- `Store` / `FileStore`: key-value records as `<namespace>/<key>.json`.
|
||||
|
||||
Layout as built under `{workspace}/tinyagents_store/` (`kv/` holds the
|
||||
`FileStore`, `journal/` the `JsonlAppendStore`). Two constraints reshaped the
|
||||
original sketch:
|
||||
|
||||
- TinyAgents store/stream names are **slash-free** (ASCII alphanumerics plus
|
||||
`-_.` — the crate's path-traversal guard), so the `thread/{id}/messages`
|
||||
shape is impossible; names are dot-separated.
|
||||
- Journals are **per session, not per thread**: multiple transcript files can
|
||||
share one `_meta.thread_id`, and appending them into a shared stream would
|
||||
interleave sessions. The descriptor carries `thread_id`, so thread-level
|
||||
views remain a projection.
|
||||
|
||||
| Record | Primitive | Stream / key |
|
||||
| ----------------------------- | ------------- | ------------------------------------------------ |
|
||||
| Message journal (per session) | `AppendStore` | stream `session.{session_key}.messages` |
|
||||
| Session descriptor | `Store` | ns `sessions`, key `{session_key}` |
|
||||
| Item idempotency ledger | `Store` | ns `migration_items`, key `sha256(source path)` |
|
||||
| Global run marker | `Store` | ns `migrations`, key `session_import_v1` |
|
||||
|
||||
Run event journals and run descriptors were dropped from v1 (see the resolved
|
||||
open questions at the end): run events stay queryable in SQLite and belong to
|
||||
the P2 journal-canonicalization work.
|
||||
|
||||
Descriptor records carry the compatibility mapping both directions:
|
||||
|
||||
```json
|
||||
// ns sessions, key {session_key} (session_key = transcript stem)
|
||||
{
|
||||
"session_key": "1719800000_orchestrator",
|
||||
"parent_session_key": null, // from the __ stem chain
|
||||
"thread_id": "…", // _meta.thread_id or imported-{stem}
|
||||
"thread_id_synthesized": false,
|
||||
"task_id": "…", // from _meta.task_id (nullable)
|
||||
"run_ids": ["…"], // joined from agent_runs via thread_id
|
||||
"stream": "session.1719800000_orchestrator.messages",
|
||||
"dispatcher": "native",
|
||||
"agent_name": "…", "agent_id": "…", "agent_type": "…",
|
||||
"provider": "…", "model": "…",
|
||||
"created": "…", "updated": "…", "turn_count": 1,
|
||||
"usage": { "input": 0, "output": 0, "cached_input": 0, "cost_usd": 0.0 },
|
||||
"source": { "jsonl": "session_raw/….jsonl", "md": null },
|
||||
"import": { "version": 1, "imported_at": "…", "warnings": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
Message journal values (as built) are full-fidelity records of what
|
||||
`read_transcript()` returns — `{id?, role, content, extra_metadata?}`, where
|
||||
`extra_metadata` carries the reconstructed `openhuman_turn_usage` block
|
||||
(`iteration`, `reasoning_content`, per-turn `usage`, `tool_calls` including
|
||||
`extra_content`). `ChatMessage` itself marks `id`/`extra_metadata`
|
||||
`skip_serializing`, so the journal defines its own record type
|
||||
(`JournalMessage`). Projection into the tinyagents `harness::message::Message`
|
||||
model is left to the read side.
|
||||
|
||||
### Lineage keys
|
||||
|
||||
- `thread_id`: taken from `_meta.thread_id` when present; when absent (old
|
||||
files), synthesize `imported:{session_key}` so every migrated session has a
|
||||
stable thread stream. Record the synthesis in the descriptor.
|
||||
- Parent/child: derive from the `__` stem chain and cross-check against
|
||||
`agent_runs.parent_thread_id` / `subagent_sessions.json`; disagreements are
|
||||
warnings, stem chain wins (it is the write-time truth).
|
||||
- `root_run_id`: tinyagents carries it in graph types but OpenHuman's
|
||||
`graph_checkpoints` schema drops it. The importer sets `root_run_id` on run
|
||||
descriptors by walking `agent_runs.parent_run_id` to the root. (Separately,
|
||||
adding a `root_run_id` column to `graph_checkpoints` is a small schema
|
||||
follow-up — tracked in the audit, not part of this migration.)
|
||||
|
||||
## Tool-call normalization
|
||||
|
||||
- Native-dispatcher transcripts: `tool_calls` arrays map 1:1 onto tinyagents
|
||||
`ToolCall` records (`arguments` parsed from the raw JSON string; parse
|
||||
failure → keep as string + warning).
|
||||
- XML / P-format transcripts: **do not re-parse in v1.** The markup stays
|
||||
verbatim in message content, exactly as the live resume path treats it
|
||||
today; the descriptor records `"dispatcher": "xml" | "pformat"` so a later
|
||||
pass (or read-side shim) can re-extract structured calls using the existing
|
||||
`ToolDispatcher` parsers if ever needed. Re-parsing at import time is high
|
||||
risk (P-format needs the positional-arg registry of the tool set as it
|
||||
existed then) for no current consumer.
|
||||
|
||||
## Idempotency and observability
|
||||
|
||||
Follow the proven `session_layout_v1` pattern, plus per-item ledger entries:
|
||||
|
||||
- Global marker: `Store` record `migrations/session_import_v1` with run
|
||||
timestamp, counters, and tool version. Present → skip scan entirely
|
||||
(bypassed by `--only`, `--force`, and dry runs).
|
||||
- Per-source ledger: each imported stem writes
|
||||
`migration_items/{sha256(workspace-relative source path)}` with source size
|
||||
+ mtime. Re-runs (e.g. after a crash) skip completed items; a changed
|
||||
size/mtime re-imports and overwrites that item's records (only that
|
||||
session's stream file is reset and rewritten).
|
||||
- Never mutate or delete sources. `session_raw/`, `sessions/`, `sessions.db`,
|
||||
and `subagent_sessions.json` remain untouched; legacy readers keep working
|
||||
until parity is proven.
|
||||
- Surface (as built): the `openhuman.session_import_run` controller —
|
||||
`openhuman-core session-import run` / RPC — with `dry_run`, `only`
|
||||
(stem glob), `force`, `verbose`, and `workspace` (dir override) params.
|
||||
- dry-run prints the per-file plan (stem → thread stream, message count,
|
||||
dialect, warnings) and writes nothing;
|
||||
- real run emits a summary: files scanned / imported / skipped / failed,
|
||||
messages written, warnings list; grep-friendly `[session-import]` log
|
||||
prefix on every line.
|
||||
- Failure policy: per-file errors are warnings (matching
|
||||
`migrate_session_layout_if_needed`); the command never aborts the batch and
|
||||
never blocks core startup — it is an explicit command, not a boot hook, in
|
||||
v1. Wiring it into startup comes only after parity tests.
|
||||
|
||||
## Fixture matrix (required before implementation is "done")
|
||||
|
||||
Implemented in `src/openhuman/session_import/ops_tests.rs` (18 tests covering
|
||||
every row below, plus dry-run purity and sources-untouched assertions).
|
||||
|
||||
Golden-file tests over real captured shapes:
|
||||
|
||||
1. Current flat `session_raw/{ts}_{agent}.jsonl`, native dispatcher.
|
||||
2. Legacy date-folder `session_raw/{DDMMYYYY}/…` (pre-layout-migration).
|
||||
3. Legacy Markdown-only session (no JSONL twin) via the `<!--MSG-->` reader.
|
||||
4. Sub-agent stems, including a two-level `a__b__c` chain.
|
||||
5. XML-dialect transcript (markup-in-content preserved byte-for-byte).
|
||||
6. P-format transcript (ditto).
|
||||
7. Assistant line with `tool_calls` incl. `extra_content` (Gemini
|
||||
thought-signature passthrough).
|
||||
8. Malformed files: missing `_meta` first line, truncated last line, empty
|
||||
file, unparseable message line (skip + warning, matching the current
|
||||
reader's tolerance).
|
||||
9. `_meta` without `thread_id` (synthesized thread id path).
|
||||
10. Ledger cross-check: `agent_runs` row whose `parent_thread_id` disagrees
|
||||
with the stem chain (warning path).
|
||||
11. Idempotency: import → re-run (all skipped) → touch one source → only that
|
||||
item re-imports.
|
||||
|
||||
Parity assertion: for every fixture, reading the migrated thread stream back
|
||||
and projecting it into `ChatMessage` history must equal what
|
||||
`read_transcript()` returns for the source file (same messages, same
|
||||
`openhuman_turn_usage` reconstruction).
|
||||
|
||||
## Phasing
|
||||
|
||||
1. **Importer + CLI + fixtures** — done (`src/openhuman/session_import/`):
|
||||
write-only into `tinyagents_store/`, sources untouched, nothing reads the
|
||||
new records yet.
|
||||
2. **Read-side shadow**: run-inspection surfaces read both and diff-log
|
||||
mismatches (behind a debug flag).
|
||||
3. **Cutover**: new internals read TinyAgents records; legacy readers stay as
|
||||
compatibility projections keyed by `session_key`.
|
||||
4. **Retirement**: delete legacy readers once telemetry shows no shadow
|
||||
mismatches (separate decision, out of scope here).
|
||||
|
||||
## Open questions (resolved in v1)
|
||||
|
||||
- `run_events` / `run_telemetry` journaling: **not in v1** — transcripts +
|
||||
descriptors only. Run events stay queryable in SQLite; P2 (journal
|
||||
canonicalization) owns that surface.
|
||||
- Store root: **`{workspace}/tinyagents_store/`** (`kv/` + `journal/`).
|
||||
Living under the workspace means the fail-closed
|
||||
`is_workspace_internal_path` guard keeps agent tools from writing here —
|
||||
desirable.
|
||||
- `subagent_sessions.json` `latestHistory` mirrors: **dropped** — they are a
|
||||
cache of the same messages the child's own transcript carries; the child
|
||||
stem imports as its own session.
|
||||
@@ -1,118 +0,0 @@
|
||||
# 00 — Harness Audit: what remains in OpenHuman, what can migrate
|
||||
|
||||
Ground truth as of 2026-07-04, main + waves 1–2 (#4473, #4483).
|
||||
|
||||
Scope totals (incl. tests):
|
||||
|
||||
| Tree | LOC | Notes |
|
||||
| ---- | --- | ----- |
|
||||
| `src/openhuman/agent/` | ~56,951 | this audit's core scope |
|
||||
| `src/openhuman/agent_orchestration/` | ~25,749 | lifecycle glue over SDK graphs |
|
||||
| `src/openhuman/tinyagents/` | ~12,185 | the live adapter seam (all turns route here) |
|
||||
| `src/openhuman/inference/` | ~52,955 | provider layer; does NOT wrap tinyagents |
|
||||
|
||||
Classification legend: **(a)** thin adapter over tinyagents · **(b)**
|
||||
migratable/duplicative generic harness logic · **(c)** product policy, stays ·
|
||||
**(d)** unclear.
|
||||
|
||||
## 1. Where execution already lives on tinyagents
|
||||
|
||||
Chat turns (`harness/session/turn/core.rs` → `turn/graph.rs`), channel/CLI
|
||||
turns (`harness/graph.rs`, entry `agent/bus.rs` `agent.run_turn`), and
|
||||
sub-agent turns (`harness/subagent_runner/ops/graph.rs`) all route through
|
||||
`run_turn_via_tinyagents_shared` in the `src/openhuman/tinyagents/` adapter.
|
||||
Middleware, stop hooks, unknown-tool policy, and context compression are on
|
||||
SDK middleware. The legacy `run_tool_call_loop` is deleted.
|
||||
|
||||
Direct `tinyagents::` references inside `agent/` are sparse by design — the
|
||||
adapter module owns the seam. Highest densities: `subagent_runner/ops/runner.rs`
|
||||
and `ops/graph.rs` (15 refs each), `turn/core.rs` (13).
|
||||
|
||||
## 2. Remaining subsystems, ranked by migratable size
|
||||
|
||||
1. **Session/transcript shell — `harness/session/`, ~13.2k LOC.** Transcript
|
||||
read/write + dialect-suffix compat (`transcript.rs` 1347+978 tests),
|
||||
builder assembly (`builder/factory.rs` 1312 — product, stays), turn IO
|
||||
(`session_io.rs`), migration (373), progress projection
|
||||
(`tool_progress.rs` 252). Duplicates crate `Store`/`AppendStore`/
|
||||
`StoreChatHistory`. Old-plan C1 covers the cutover; dual-writes are ON,
|
||||
shadow reads landed (flag OFF). **Biggest single unlock (~9–11k incl.
|
||||
session_db/subagent_sessions/session_import).**
|
||||
2. **Sub-agent runner — `harness/subagent_runner/`, ~6.1k LOC.** Prepare →
|
||||
filter tools → run child → checkpoint → extract → mirror. Duplicates
|
||||
`SubAgent`/`SubAgentSession`/`SubAgentTool` + `graph::subagent_node`.
|
||||
`extract_tool.rs` (612) + `handoff.rs` (287) are generic (old-plan C5/C6);
|
||||
`tool_prep.rs` allowlist half is product.
|
||||
3. **Orchestration lifecycle glue — `agent_orchestration/`, ~3.7k of it.**
|
||||
`running_subagents.rs` **1931** (target ≤300; watch channels, abort/wait/
|
||||
steer, tombstones — belongs on a durable `TaskStore`, see 06),
|
||||
`spawn_parallel_graph.rs` 1764 (dup of `map_reduce`).
|
||||
4. **Progress/streaming projection — ~2.9k LOC.** `progress.rs` (303,
|
||||
`AgentProgress` incl. `ThinkingDelta`/`SubagentThinkingDelta`/
|
||||
`ToolCallArgsDelta`), `session/tool_progress.rs` (252, bridges
|
||||
`ProviderDelta` → `AgentProgress`), `progress_tracing.rs` + `langfuse.rs`
|
||||
+ tests (~2k). Duplicates crate `AgentEvent`/`HarnessEventJournal`/
|
||||
Langfuse exporters. Deletion gated on old-plan C4 journal parity **and**
|
||||
on crate streaming fixes (docs 02/03 here) so nothing regresses in the UI.
|
||||
5. **Tool-call dialects — ~1.9k LOC.** `dispatcher.rs` (609) `ToolDispatcher`
|
||||
trait (native/XML/P-Format), `harness/parse.rs` (833) permissive XML/JSON
|
||||
parser with arg-key drift recovery, `pformat.rs` (499) compact positional
|
||||
`name[a|b]` calls (~80% token cut, OpenHuman-invented). Live path is
|
||||
native; these survive for prompt-guided dialects + transcript compat.
|
||||
Extraction candidate (06) so the C1 read-cutover becomes a pure delete.
|
||||
6. **Multimodal assembly — `multimodal.rs` 1690 (+1020 tests).**
|
||||
`[IMAGE:]`/`[FILE:]` markers → provider content blocks, mime allowlist,
|
||||
PDF extraction, fetch gating, truncation budgets. ~1550 generic (06).
|
||||
7. **Cost — `cost.rs` 343.** Per-turn USD + tier pricing + budget stop-hook
|
||||
feed. Blocked on crate `Usage` having no money field and
|
||||
`reasoning_tokens` never being populated (see 01/02). Old-plan C3 flip
|
||||
criteria stand.
|
||||
8. **Tool policy/filter — `tool_policy.rs` 524 + `harness/tool_filter.rs`
|
||||
299.** Overlaps crate `ToolPolicy`; residual gap is free-form
|
||||
model-visible `ToolSchema` metadata (sdk-gaps §1).
|
||||
|
||||
## 3. Confirmed product — keep in OpenHuman
|
||||
|
||||
- `triage/` (3.8k) — trigger classification; *invokes* the migrated loop.
|
||||
~800-line generic evaluator core is an extraction candidate (06), envelope/
|
||||
escalation/events stay.
|
||||
- `task_dispatcher/` (1.6k) — deterministic card dispatch (CAS claim →
|
||||
autonomous turn). Mechanics shrink onto `graph::todos` (old-plan C2).
|
||||
- `archivist/` (1.4k + 1k tests) — episodic indexing/lessons; memory policy.
|
||||
- `prompts/` (~4k) — section assembly/rendering; product voice.
|
||||
- `agent/tools/` (~4k) — product tools (`run_workflow`, preferences,
|
||||
`delegate_to_personality`, todo CRUD → C2 projections).
|
||||
- `builder/factory.rs`, definitions (`definition.rs` 846,
|
||||
`definition_loader.rs`, `builtin_definitions.rs`), `memory_context.rs`,
|
||||
`memory_protocol.rs`, credentials/sandbox context, `debug/`, `library/`,
|
||||
`bus.rs`, `schemas.rs`.
|
||||
- `inference/` (53k) — stays for credential ownership, billing
|
||||
classification, OAuth, local/Ollama. Largest *parallel* duplication of a
|
||||
tinyagents capability in the tree, but per the standing verdict
|
||||
(`reliable.rs` 900) not forced; revisit only after a native Anthropic
|
||||
provider (05) proves out.
|
||||
|
||||
## 4. Streaming & reasoning today (OpenHuman side)
|
||||
|
||||
Provider deltas (`inference::provider::ProviderDelta::{TextDelta,
|
||||
ThinkingDelta, ToolCallArgsDelta}`) are bridged in
|
||||
`session/tool_progress.rs:226` into `AgentProgress`, a per-request
|
||||
`mpsc::Sender` channel distinct from the DomainEvent bus. Reasoning reaches
|
||||
the UI **only** through this OpenHuman-side path (plus the `ThinkingForwarder`
|
||||
shim), because the crate drops reasoning (01 §3). Fixing 02/03 in the vendor
|
||||
crate lets `AgentProgress` become a thin projection of crate `AgentEvent`s
|
||||
and unblocks deleting `ThinkingForwarder`, `tool_progress.rs`, and eventually
|
||||
the `progress_tracing` stack.
|
||||
|
||||
## 5. Delta vs the old plan's inventory
|
||||
|
||||
The C0–C7 verdicts hold. New/raised items from this sweep:
|
||||
|
||||
- `running_subagents.rs` keeps growing (1931; was flagged at the same size on
|
||||
2026-07-03 — enforce the ≤300 target when C6 lands or it accretes further).
|
||||
- The dialect layer's delete gate ("no live path parses provider text") is
|
||||
reachable only after C1 phase 3 **and** either upstreaming P-Format/XML
|
||||
parsing (06) or accepting native-only transcripts.
|
||||
- `inference/provider/reliable.rs` verdict unchanged, but 05 (native
|
||||
Anthropic in-crate) is the first concrete step that could eventually make
|
||||
routing non-turn provider calls through the crate harness attractive.
|
||||
@@ -1,144 +0,0 @@
|
||||
# 01 — TinyAgents Crate Audit (vendor/tinyagents @ 1.5.0 + ac73382)
|
||||
|
||||
~41.5k non-test LOC (~60k with tests); 54 integration tests, 15 examples.
|
||||
Deps: tokio (sync/time/macros), reqwest (rustls/json/stream), serde, sha2;
|
||||
optional `rusqlite` (`sqlite`), `rhai` (`repl`). No provider-specific SDKs.
|
||||
|
||||
The crate's own `goal.md` is a pre-written gap list from OpenHuman's
|
||||
migration perspective and corroborates most findings below.
|
||||
|
||||
## 1. Module map
|
||||
|
||||
- `harness/` — the agent runtime. Largest: `middleware/` (~5.3k, trait +
|
||||
~18 built-ins incl. retry/timeout/fallback/budget/tool-policy/contextual
|
||||
selection/approval/redaction/prompt-cache guard), `providers/` (~3k:
|
||||
`MockModel` + one real `OpenAiModel`), `agent_loop/` (~2.3k), `model/`
|
||||
(~1.9k: `ChatModel`, `ModelRequest/Response`, `ModelStream`,
|
||||
`StreamAccumulator`, `ModelRegistry`), `observability/` (~1.8k journals/
|
||||
status/Langfuse), `subagent/` (~1.2k), `events/` (~1.2k), plus store/tool/
|
||||
summarization/embeddings/steering/cache/usage/cost/no_progress/testkit.
|
||||
- `graph/` (~19k) — durable typed graphs: compiled runtime, checkpoints
|
||||
(memory/file/sqlite), orchestration `TaskStore` + tools, `todos`, `goals`,
|
||||
`map_reduce`, subagent/subgraph nodes, recursion, export, testkit.
|
||||
- `registry/`, `language/` (.rag), `repl/` (.ragsh, feature-gated).
|
||||
- Ergonomics gap: graph surface is fully re-exported at crate root, but core
|
||||
harness types (`AgentHarness`, `ChatModel`, `ModelRequest`, `OpenAiModel`,
|
||||
middleware, message types) require deep `harness::…` paths (`lib.rs`).
|
||||
|
||||
## 2. The harness loop (what's good)
|
||||
|
||||
`AgentHarness::run_loop` (`harness/agent_loop/mod.rs:293-673`): cancellation
|
||||
checkpoint → steering drain → deadline/call caps → build request →
|
||||
`before_model` → wrap-onion model call with retry + fallback chain
|
||||
(`:766-859`) + per-call `tokio::time::timeout` budget (`:902-917`) + response
|
||||
cache (`:685-756`) → `after_model` → usage/events → `MiddlewareControl`
|
||||
(StopWithFinal/Interrupt) → tools → repeat. Unknown-tool policy
|
||||
`Fail | ReturnToolError | Rewrite` (`:563-618`). Sub-agents nest as tools
|
||||
with a depth cap. Rich extension points; the loop *shape* itself is fixed.
|
||||
|
||||
## 3. Defects & gaps (file:line), grouped
|
||||
|
||||
### Reasoning / thought tokens — scaffolding exists, last mile unwired
|
||||
|
||||
- `ContentBlock` = `Text | Json | Image | ProviderExtension`
|
||||
(`harness/message/types.rs:21-30`) — **no Thinking/Redacted variant**; no
|
||||
persisted home for Anthropic thinking blocks or their signatures.
|
||||
- OpenAI SSE path hardcodes `reasoning: String::new()` per delta
|
||||
(`providers/openai/mod.rs:772`); never parses `reasoning_content` or
|
||||
o-series reasoning.
|
||||
- `StreamAccumulator::finish` **drops accumulated reasoning** — final message
|
||||
built from text + tool chunks only (`model/mod.rs:683-710`).
|
||||
- Loop's middleware-facing `ModelDelta` built with content + tool_call only,
|
||||
dropping reasoning (`agent_loop/mod.rs:980-984`) — the exact sdk-gaps §3
|
||||
item blocking `ThinkingForwarder` deletion.
|
||||
- `Usage.reasoning_tokens` (`usage/types.rs:29`) and
|
||||
`CostTotals.reasoning_cost` (`cost/mod.rs:69`) exist but are **always 0**:
|
||||
`convert_usage` maps only prompt/completion/total/cached
|
||||
(`openai/mod.rs:708-719`); `completion_tokens_details.reasoning_tokens`
|
||||
isn't even in the wire struct (`openai/types.rs:235-256`).
|
||||
- No signature/redacted_thinking handling anywhere → correct multi-turn
|
||||
extended-thinking + tool-use vs native Anthropic is currently impossible.
|
||||
|
||||
### Streaming — real SSE, but observation-only and single-level
|
||||
|
||||
- OpenAI streaming is genuine incremental SSE (`openai/mod.rs:1020-1073`,
|
||||
line buffering `:874-896`, chunk folding `:753-804`); tool-call arg
|
||||
fragments stream as `ToolCallDelta` (`:796-799`).
|
||||
- **`invoke_streaming` returns a completed `AgentRun`, not a `Stream`**
|
||||
(`agent_loop/mod.rs:195-241`); deltas reachable only via `EventSink`/
|
||||
middleware (`:929-994`).
|
||||
- **Sub-agents never stream**: `SubAgentTool::call` →
|
||||
`SubAgent::invoke` non-streaming path (`subagent/mod.rs:535`, `:179-189`).
|
||||
No parent/root attribution on deltas.
|
||||
- No explicit tool-call started/completed stream events.
|
||||
- `MockModel::stream` replays a completed `invoke` (`model/types.rs:536-549`)
|
||||
— mock-backed tests aren't truly incremental.
|
||||
|
||||
### Performance — correctness-first cloning in the hot loop
|
||||
|
||||
- Full history cloned per turn: `ModelRequest::new(messages.clone())`
|
||||
(`agent_loop/mod.rs:356`); full request cloned again **per retry/fallback
|
||||
attempt** (`:804`, `:937`).
|
||||
- Tool schemas rebuilt every iteration: `.with_tools(self.tools.schemas())`
|
||||
(`:356`).
|
||||
- Provider `translate_request` re-clones every message + tool schema and
|
||||
re-serializes tool args per call (`openai/mod.rs:404-449, 570-584`).
|
||||
- Response-cache key = serde round-trip + canonicalize + SHA-256 over the
|
||||
**entire request** per cached call (`cache/mod.rs:101-106`).
|
||||
- `StreamAccumulator::push_tool_chunk` linear scan per fragment
|
||||
(`model/mod.rs:643-650`).
|
||||
- Tools execute **strictly serially** (`agent_loop/mod.rs:526-659`);
|
||||
`parallel_tool_calls` is a capability flag only.
|
||||
|
||||
### Providers — one adapter, many presets
|
||||
|
||||
- Only `OpenAiModel` is real; `anthropic()`/`deepseek`/`groq`/`xai`/
|
||||
`openrouter`/`together`/`mistral`/`ollama` are base-URL presets on the
|
||||
Chat Completions wire (`openai/mod.rs:309-383`).
|
||||
- **No Anthropic Messages API**: zero `cache_control`/`ephemeral` hits in
|
||||
providers; no thinking config; `Usage.cache_creation_tokens` always 0
|
||||
(only `cache_read_tokens` from `prompt_tokens_details.cached_tokens`,
|
||||
`openai/mod.rs:713-716`).
|
||||
- Prompt-cache shaping is metadata-only: `cache_segments` /
|
||||
`prompt_fingerprint` / `cacheable_prefix_ids()`
|
||||
(`model/types.rs:385-405`) + `PromptCacheGuardMiddleware`
|
||||
(`middleware/mod.rs:19-60`) never reach any wire field — the spec's
|
||||
"extreme prompt caching" (`docs/spec/README.md:258-278`) is observability,
|
||||
not request shaping.
|
||||
- Spec explicitly plans feature-flagged native `openai`/`anthropic`/`ollama`
|
||||
(`docs/spec/README.md:279-284`; slot documented at
|
||||
`providers/types.rs:322-337`).
|
||||
|
||||
### Durability / storage
|
||||
|
||||
- `TaskStore` is in-memory/JSONL only; no lifecycle history/replay
|
||||
(goal.md §4/§11) — what `running_subagents.rs` (1931 LOC) needs to shrink.
|
||||
- SQLite ownership: bundled `rusqlite 0.40` vs app-owned sqlite → needs a
|
||||
trait-first/connection-adapter path (goal.md §5).
|
||||
- `Store` has no compare-and-set (single-writer constraint documented in the
|
||||
old plan).
|
||||
|
||||
## 4. Test coverage holes
|
||||
|
||||
No tests for: native Anthropic/prompt caching/thinking (no impl), reasoning
|
||||
end-to-end (only hand-fed accumulator test,
|
||||
`tests/e2e_reasoning_and_selection.rs:57-98`), parallel tool execution,
|
||||
caller-consumable streaming, sub-agent stream propagation. Live-provider
|
||||
tests exist behind env keys; conformance suites exist for graph/checkpoint.
|
||||
|
||||
## 5. Ranked improvement opportunities (effort: S≈1-2d, M≈3-5d, L≈1-2w, XL multi-week)
|
||||
|
||||
1. **Reasoning end-to-end** — L — doc 02. Unblocks `ThinkingForwarder`
|
||||
deletion + real reasoning cost accounting.
|
||||
2. **Hot-loop de-cloning + cache-key + schema caching** — M — doc 04.
|
||||
Biggest runtime win for OpenHuman's long transcripts.
|
||||
3. **Streaming out of the harness + up from sub-agents** — L — doc 03.
|
||||
Unblocks `tool_progress.rs`/progress projection deletions (C4).
|
||||
4. **Native Anthropic Messages provider + cache_control shaping** — L→XL —
|
||||
doc 05. Highest-leverage provider gap.
|
||||
5. **Parallel tool execution** — M — doc 04 §4.
|
||||
6. **Usage completeness (OpenAI reasoning tokens) + catalog pricing** — S→M
|
||||
— doc 02 §5; feeds old-plan C3 budget flip.
|
||||
7. **Durable TaskStore (SQLite/JSONL lifecycle + replay)** — L — doc 06 §4.
|
||||
8. **SQLite dependency flexibility** — M — goal.md §5.
|
||||
9. **Crate-root re-export ergonomics** — S.
|
||||
@@ -1,88 +0,0 @@
|
||||
# 02 — Thought Tokens End-to-End (vendor crate work)
|
||||
|
||||
Goal: reasoning/thinking content becomes a first-class, streamed, persisted,
|
||||
replayed, and priced citizen of the crate — so OpenHuman deletes
|
||||
`ThinkingForwarder` and stops re-projecting thinking via its own
|
||||
`ProviderDelta::ThinkingDelta` bridge (`session/tool_progress.rs:226`).
|
||||
|
||||
All steps are vendor-crate changes (`vendor/tinyagents`), committed on a
|
||||
submodule feature branch, gitlink-bumped into OpenHuman, PR'd upstream.
|
||||
Precedent: `NoProgressTracker` (#7), MicrocompactMiddleware (`ac73382`).
|
||||
|
||||
## Step 1 — Message representation
|
||||
|
||||
- Add `ContentBlock::Thinking { text: String, signature: Option<String> }`
|
||||
and `ContentBlock::RedactedThinking { data: String }` to
|
||||
`harness/message/types.rs:21-30`.
|
||||
- Serde: additively tagged; existing transcripts (no thinking blocks) parse
|
||||
unchanged. Round-trip tests in `message/test.rs`.
|
||||
- `AssistantMessage` rendering helpers must skip thinking blocks for
|
||||
plain-text extraction (`Message::text()`-style accessors) but preserve them
|
||||
for provider replay.
|
||||
|
||||
## Step 2 — Accumulator persistence
|
||||
|
||||
- `StreamAccumulator::finish` (`harness/model/mod.rs:683-710`): emit the
|
||||
accumulated `self.reasoning` as a leading `ContentBlock::Thinking` on the
|
||||
final message instead of dropping it. Carry signature fragments (Anthropic
|
||||
`signature_delta`) through a new accumulator field.
|
||||
- Keep the existing `reasoning()` side-channel accessor for backward compat.
|
||||
|
||||
## Step 3 — Delta plumbing
|
||||
|
||||
- Populate `MessageDelta.reasoning` from providers:
|
||||
- OpenAI path: parse `delta.reasoning_content` (DeepSeek/compat) and
|
||||
o-series reasoning summaries instead of hardcoding `String::new()`
|
||||
(`providers/openai/mod.rs:772`); add the wire fields to
|
||||
`openai/types.rs`.
|
||||
- Anthropic path (05): `thinking_delta` / `signature_delta` events.
|
||||
- Thread reasoning into the middleware-facing `ModelDelta`
|
||||
(`agent_loop/mod.rs:980-984`) — closes sdk-gaps §3. Add
|
||||
`ToolDelta.tool_name` on the first fragment (tool-name-on-start), the
|
||||
other half of that gap.
|
||||
- Emit reasoning in `AgentEvent::ModelDelta` with parent/root run
|
||||
attribution (pairs with doc 03's sub-agent propagation so
|
||||
`SubagentThinkingDelta` can become a projection).
|
||||
|
||||
## Step 4 — Replay correctness (Anthropic contract)
|
||||
|
||||
Anthropic requires thinking blocks (with signatures) to be replayed verbatim
|
||||
in the assistant turn preceding tool results. With Step 1 the blocks live in
|
||||
the transcript; the provider translation (05) must:
|
||||
|
||||
- Serialize `Thinking`/`RedactedThinking` blocks back onto the wire for
|
||||
assistant messages in multi-turn tool-use conversations.
|
||||
- Never send thinking blocks to providers that reject them (OpenAI compat
|
||||
path strips them) — provider capability flag `supports_thinking` on
|
||||
`ModelProfile`.
|
||||
- Property test: build a 3-turn tool-use conversation with thinking, assert
|
||||
byte-stable signature replay.
|
||||
|
||||
## Step 5 — Usage & cost
|
||||
|
||||
- Add `completion_tokens_details.reasoning_tokens` to the OpenAI wire struct
|
||||
(`openai/types.rs:235-256`) and map it in `convert_usage`
|
||||
(`openai/mod.rs:708-719`).
|
||||
- Anthropic (05): thinking output tokens are billed as output; keep
|
||||
`reasoning_tokens` as the *reported* subset where the API exposes it.
|
||||
- `CostTotals.reasoning_cost` (`cost/mod.rs:69`) then prices real numbers —
|
||||
feeds OpenHuman's C3 budget-flip criteria (pricing table wired).
|
||||
|
||||
## Step 6 — OpenHuman follow-through (after gitlink bump)
|
||||
|
||||
1. Delete `ThinkingForwarder` (old-plan C7 item; sdk-gaps §3 closes).
|
||||
2. `AgentProgress::ThinkingDelta`/`SubagentThinkingDelta` become projections
|
||||
of crate `AgentEvent::ModelDelta.reasoning` — shrinks
|
||||
`session/tool_progress.rs` toward deletion (with doc 03).
|
||||
3. Persisted-transcript compat: OpenHuman's `multimodal.rs` reasoning-block
|
||||
handling shrinks once the crate owns thinking blocks in messages.
|
||||
|
||||
## Tests (crate)
|
||||
|
||||
- Provider-level: SSE fixture streams with reasoning deltas → accumulator →
|
||||
message contains `Thinking` block; usage carries reasoning tokens.
|
||||
- Loop-level: middleware `on_model_delta` sees reasoning; `AgentRun.messages`
|
||||
persists it; replay serialization per provider capability.
|
||||
- Extend `tests/e2e_reasoning_and_selection.rs` beyond hand-fed deltas.
|
||||
|
||||
Effort: **L** (1–2 weeks) crate-side; OpenHuman follow-through **S**.
|
||||
@@ -1,80 +0,0 @@
|
||||
# 03 — Streaming Improvements (vendor crate work)
|
||||
|
||||
Goal: the harness exposes a real caller-consumable stream, sub-agent deltas
|
||||
propagate to the parent with lineage attribution, and tool calls get explicit
|
||||
lifecycle events — so OpenHuman's `AgentProgress` layer becomes a thin
|
||||
projection and the C4 progress_tracing deletion has full-fidelity input.
|
||||
|
||||
## Current state (from 01)
|
||||
|
||||
- Provider SSE is genuinely incremental (`openai/mod.rs:1020-1073`) and
|
||||
tool-call arg fragments stream (`ToolCallDelta`, `:796-799`).
|
||||
- But `invoke_streaming` returns a completed `AgentRun`
|
||||
(`agent_loop/mod.rs:195-241`); consumers must attach an `EventSink` or
|
||||
middleware to see deltas.
|
||||
- Sub-agents run the non-streaming path (`subagent/mod.rs:535`, `:179-189`)
|
||||
— a child's tokens/thinking/tool activity are invisible until it returns.
|
||||
- No tool-call started/completed events; `MockModel::stream` replays a
|
||||
completed response (`model/types.rs:536-549`).
|
||||
|
||||
## Step 1 — `invoke_stream`: a Stream-returning entry point
|
||||
|
||||
- New API: `AgentHarness::invoke_stream(...) ->
|
||||
(impl Stream<Item = AgentStreamItem>, JoinHandle<Result<AgentRun>>)` (or a
|
||||
single stream whose terminal item carries the `AgentRun`).
|
||||
- `AgentStreamItem` (new enum, `harness/stream/`): `TurnStarted`,
|
||||
`ModelDelta(ModelDelta)` (text + reasoning + tool_call, post doc 02),
|
||||
`ToolCallStarted { call_id, tool_name }`, `ToolCallArgsDelta`,
|
||||
`ToolCallCompleted { call_id, outcome }`, `AssistantMessage`,
|
||||
`UsageUpdated`, `RunCompleted/Failed` — each stamped with
|
||||
`run_id / parent_run_id / root_run_id / depth`.
|
||||
- Implementation: a bounded `mpsc` fed from the existing emit points in
|
||||
`invoke_model_streaming_once` (`agent_loop/mod.rs:929-994`) and the tool
|
||||
execution section (`:526-659`). The `EventSink` path stays; this is a
|
||||
convenience projection over the same events, not a parallel system.
|
||||
- Backpressure: bounded channel + documented drop-oldest vs block policy
|
||||
(default: block; the loop already awaits network).
|
||||
|
||||
## Step 2 — Sub-agent delta propagation
|
||||
|
||||
- `SubAgentTool::call` switches to the streaming child path and forwards
|
||||
child `AgentStreamItem`s into the parent's stream/sink with the child's
|
||||
`run_id` + inherited `root_run_id` and `depth+1`
|
||||
(`subagent/mod.rs:535` → `SubAgent::invoke_stream`).
|
||||
- Recursion-safe: items flow through the parent's channel; no unbounded
|
||||
fan-in (children are executed serially today; revisit with doc 04 §4).
|
||||
- Closes goal.md §3's "attribute every delta to parent/root run id".
|
||||
|
||||
## Step 3 — Tool-call lifecycle events
|
||||
|
||||
- Emit `ToolCallStarted` as soon as the terminal `Completed` (or the first
|
||||
named `ToolCallDelta`, with doc 02's tool-name-on-start) identifies the
|
||||
tool — this is what OpenHuman's UI needs to show "running X…" before args
|
||||
finish streaming.
|
||||
- Emit `ToolCallCompleted` with outcome + duration after the wrap-onion tool
|
||||
call returns.
|
||||
|
||||
## Step 4 — Test/mock fidelity
|
||||
|
||||
- `MockModel::stream` gains scripted incremental emission (list of
|
||||
`ModelStreamItem`s with optional delays) so streaming tests are truly
|
||||
incremental (`model/types.rs:536-549`).
|
||||
- Tests: caller consumes `invoke_stream` and observes interleaved model/tool
|
||||
items; nested sub-agent test asserts child deltas arrive with correct
|
||||
lineage before the parent's final message.
|
||||
|
||||
## Step 5 — OpenHuman follow-through
|
||||
|
||||
1. `run_turn_via_tinyagents_shared` adapters consume `invoke_stream` (or the
|
||||
sink projection) and map 1:1 onto `AgentProgress` — deleting the
|
||||
`ProviderDelta` bridge in `session/tool_progress.rs` (252).
|
||||
2. `SubagentThinkingDelta`/subagent progress becomes lineage-filtered
|
||||
projection — removes bespoke child-progress plumbing in
|
||||
`subagent_runner/ops/runner.rs`.
|
||||
3. C4 (journal-backed web progress) gains the missing fidelity: journals +
|
||||
live stream share one event vocabulary, enabling the
|
||||
`progress_tracing.rs` + `langfuse.rs` deletion (~2k).
|
||||
|
||||
Effort: **L** crate-side (Steps 1–4), **M** OpenHuman follow-through.
|
||||
Depends on: doc 02 for reasoning-in-deltas (can land in either order; both
|
||||
touch `invoke_model_streaming_once`, so sequence the merges).
|
||||
@@ -1,93 +0,0 @@
|
||||
# 04 — Performance (vendor crate work)
|
||||
|
||||
Goal: remove O(history) per-turn overhead from the agent loop. OpenHuman
|
||||
transcripts are long (multi-hundred-message sessions with large tool
|
||||
results); every listed cost below scales with transcript size and is paid
|
||||
one or more times **per model call**.
|
||||
|
||||
## 1. Stop cloning the transcript per turn/attempt — biggest win
|
||||
|
||||
Today:
|
||||
|
||||
- `ModelRequest::new(messages.clone())` per iteration
|
||||
(`agent_loop/mod.rs:356`).
|
||||
- `model.invoke(state, request.clone())` / `model.stream(state,
|
||||
request.clone())` per retry/fallback **attempt** (`:804`, `:937`) — a
|
||||
3-model fallback chain with 2 retries can clone the full history 6×.
|
||||
|
||||
Plan:
|
||||
|
||||
- Change `ModelRequest.messages` to `Arc<Vec<Message>>` (or `Arc<[Message]>`)
|
||||
with copy-on-write semantics (`Arc::make_mut` on the rare mutation paths —
|
||||
middleware that rewrites history). Public builder API stays source-
|
||||
compatible via `impl Into<Arc<Vec<Message>>>`.
|
||||
- Retry/fallback passes `&ModelRequest`; `ChatModel::invoke/stream` take
|
||||
`&ModelRequest` (breaking trait change — acceptable pre-2.0, coordinated
|
||||
with OpenHuman's adapter in the same gitlink bump).
|
||||
- The loop appends assistant/tool messages to its own `Vec` and rebuilds the
|
||||
`Arc` view per turn (one cheap `Arc::new` of a `Vec` it already owns, or
|
||||
an immutable persistent-list structure if profiling justifies it — start
|
||||
with `Arc<Vec>`, measure).
|
||||
|
||||
## 2. Cache tool schemas per run
|
||||
|
||||
`.with_tools(self.tools.schemas())` rebuilds every `ToolSchema` (cloning
|
||||
name/description/`parameters: Value`) each iteration (`agent_loop/mod.rs:356`).
|
||||
Tool sets are fixed for a run (dynamic-selection middleware operates on the
|
||||
request afterward). Plan: compute `Arc<Vec<ToolSchema>>` once at run start;
|
||||
middleware that filters tools clones only then (copy-on-filter).
|
||||
|
||||
## 3. Response-cache key without full serde round-trips
|
||||
|
||||
`cache_key` = `serde_json::to_value(request)` → canonicalize → `to_vec` →
|
||||
SHA-256 over the entire request per cached call (`cache/mod.rs:101-106`).
|
||||
Plan: maintain an incremental hash — hash each message once when appended
|
||||
(messages are immutable after append), fold message digests + tool-schema
|
||||
digest + params digest into the key. Falls out naturally once messages are
|
||||
`Arc`'d (attach a lazily-computed digest per message).
|
||||
|
||||
## 4. Parallel tool execution
|
||||
|
||||
Tools run strictly serially (`agent_loop/mod.rs:526-659`) while
|
||||
`parallel_tool_calls` exists only as a capability flag. Plan:
|
||||
|
||||
- Execute a turn's tool calls via `futures::stream::iter(...)
|
||||
.buffer_unordered(cap)` but **emit results in request order** (index-sorted
|
||||
reassembly) so transcripts stay deterministic.
|
||||
- Default cap: 1 (today's behavior) — opt-in via
|
||||
`AgentHarnessBuilder::with_tool_concurrency(n)` and a per-tool
|
||||
`ToolPolicy::serial` escape hatch (side-effecting tools opt out).
|
||||
- Middleware contract: `before_tool`/`after_tool` hooks fire per call; wrap
|
||||
middleware must be `Send + Sync` (already is). Events carry call ids so
|
||||
interleaving is attributable (pairs with doc 03 lifecycle events).
|
||||
- OpenHuman: keep cap=1 initially; enable for read-only tool families after
|
||||
the approval-gate interaction is reviewed (approval middleware must park
|
||||
the whole batch, not one call).
|
||||
|
||||
## 5. Provider translation
|
||||
|
||||
`translate_request` re-clones all messages/tool schemas and re-serializes
|
||||
tool args per call (`openai/mod.rs:404-449, 570-584`). Plan: with #1/#2 the
|
||||
inputs are `Arc`'d; add a per-run memo of the translated static prefix
|
||||
(system + tools JSON) keyed by the schema digest from #3. This also makes
|
||||
prefix-stability for provider prompt caches (05) explicit rather than
|
||||
incidental.
|
||||
|
||||
## 6. Minor
|
||||
|
||||
- `StreamAccumulator::push_tool_chunk` linear scan (`model/mod.rs:643-650`)
|
||||
→ index map by `call_id` (only matters for many-tool turns; cheap fix).
|
||||
- `InMemoryResponseCache` mutex per get/put (`cache/mod.rs:119-131`) — fine;
|
||||
re-check only if #3 makes cache use hot.
|
||||
|
||||
## Measurement (gate for merging #1–#3)
|
||||
|
||||
Add a criterion-style bench (or a `live_*`-excluded integration bench) in the
|
||||
crate: synthetic 500-message transcript, 40 tools, 20-turn loop against
|
||||
`MockModel` — assert allocations/turn and wall time before/after. OpenHuman
|
||||
side: `[budget_shadow]`-style log of per-turn overhead in the adapter for one
|
||||
release to confirm on real sessions.
|
||||
|
||||
Effort: #1 **M** (trait-breaking, coordinated bump), #2 **S**, #3 **S–M**,
|
||||
#4 **M**, #5 **S**. Order: #2/#3 first (non-breaking), then #1 (+#5), #4
|
||||
independent.
|
||||
@@ -1,83 +0,0 @@
|
||||
# 05 — Native Anthropic Provider + Prompt-Cache Request Shaping
|
||||
|
||||
Goal: a feature-flagged native Anthropic Messages-API provider in the crate
|
||||
(the spec already reserves the slot, `docs/spec/README.md:279-284`,
|
||||
`providers/types.rs:322-337`), plus wiring the existing cache-segment
|
||||
metadata to real wire fields. Highest-leverage provider gap: today
|
||||
`anthropic()` is an OpenAI-compat preset that silently loses prompt caching,
|
||||
thinking, and cache-write accounting.
|
||||
|
||||
## Why this matters to OpenHuman
|
||||
|
||||
- Claude-family models are primary; every turn without `cache_control`
|
||||
breakpoints pays full input-token price on a growing transcript.
|
||||
- Extended thinking + tool use cannot work correctly at all without
|
||||
signature replay (see 02 §4).
|
||||
- `Usage.cache_creation_tokens` is structurally present but always 0 —
|
||||
OpenHuman's cost accounting (C3 flip) undercounts cache writes.
|
||||
- OpenHuman's own `inference/` layer already handles this correctly; a
|
||||
native crate provider is the precondition for ever routing provider calls
|
||||
through the crate harness (the standing `reliable.rs` gate, old-plan 02.2).
|
||||
|
||||
## Step 1 — `providers/anthropic/` behind `feature = "anthropic"`
|
||||
|
||||
- Messages API: `system` as top-level blocks, `tools` array, `tool_choice`,
|
||||
`max_tokens` required, `anthropic-version` header; reuse the crate's
|
||||
reqwest/rustls stack. No new deps.
|
||||
- Response mapping → `ModelResponse`: content blocks (text, tool_use,
|
||||
thinking, redacted_thinking per doc 02), `stop_reason`, and usage incl.
|
||||
`cache_creation_input_tokens` / `cache_read_input_tokens` →
|
||||
`Usage.cache_creation_tokens` / `cache_read_tokens`.
|
||||
- SSE streaming: `message_start` / `content_block_start` /
|
||||
`content_block_delta` (`text_delta`, `input_json_delta`,
|
||||
`thinking_delta`, `signature_delta`) / `content_block_stop` /
|
||||
`message_delta` — mapped onto `ModelStreamItem` incl. tool-name-on-start
|
||||
(`content_block_start` carries the tool name — doc 02 §3 / doc 03 §3 get
|
||||
this for free on Anthropic).
|
||||
- `ProviderKind::Anthropic` switches from preset to native when the feature
|
||||
is on; preset remains as fallback for compat gateways.
|
||||
|
||||
## Step 2 — `cache_control` shaping from `cache_segments`
|
||||
|
||||
`ModelRequest` already carries `cache_segments` / `prompt_fingerprint` /
|
||||
`cacheable_prefix_ids()` (`model/types.rs:385-405`) — currently ignored by
|
||||
translation. Plan:
|
||||
|
||||
- Translation places up to 4 `cache_control: {type: "ephemeral"}`
|
||||
breakpoints at segment boundaries: tools block, system tail, and the two
|
||||
highest-value conversation prefixes (mirrors OpenHuman's proven layout).
|
||||
- `PromptCacheGuardMiddleware` (`middleware/mod.rs:19-60`) is promoted from
|
||||
observer to enforcement partner: layout-change events now correspond to
|
||||
actual cache invalidation, so its warnings become actionable.
|
||||
- OpenAI path: no wire field needed (implicit prefix caching), but #04 §5's
|
||||
stable-prefix memo guarantees byte-stable prefixes — document that as the
|
||||
OpenAI half of "cache-aware shaping".
|
||||
- TTL/beta options (e.g. 1h cache) via `provider_options` passthrough.
|
||||
|
||||
## Step 3 — Capability & catalog integration
|
||||
|
||||
- `ModelProfile` gains `supports_thinking`, `supports_cache_control`,
|
||||
cache-write pricing multipliers; `ModelCatalog` entries for the Claude
|
||||
family map cache read/write token prices into `CostTotals` (feeds
|
||||
old-plan C3's "pricing table wired" criterion and sdk-gaps §7/§8).
|
||||
|
||||
## Step 4 — Tests
|
||||
|
||||
- Wire-fixture tests: request JSON golden files (system/tools/cache_control
|
||||
placement, thinking replay with signatures); SSE fixture → accumulator →
|
||||
message + usage assertions (cache read/write, thinking tokens).
|
||||
- `live_anthropic.rs` behind `ANTHROPIC_API_KEY`: cache-hit-on-second-call,
|
||||
multi-turn thinking + tool-use replay.
|
||||
|
||||
## Step 5 — OpenHuman follow-through (deliberately conservative)
|
||||
|
||||
- Do **not** immediately reroute OpenHuman's turn traffic — `inference/`
|
||||
stays authoritative (credentials, billing classification, OAuth).
|
||||
- First consumer: sub-agent/background/eval traffic behind a flag, comparing
|
||||
cost + cache-hit telemetry against `inference/` for the same models.
|
||||
- Re-evaluate the `reliable.rs` (900) verdict and old-plan 02.2 only after
|
||||
divergence-free telemetry; that decision gets its own migration note.
|
||||
|
||||
Effort: **L→XL** (Step 1–2 ≈ 2 weeks incl. tests; Steps 3–5 incremental).
|
||||
Depends on doc 02 (thinking blocks in `ContentBlock`) for full value; can
|
||||
land Step 1 text/tool support before thinking if 02 lags.
|
||||
@@ -1,98 +0,0 @@
|
||||
# 06 — Upstream Extraction: OpenHuman logic to move INTO tinyagents
|
||||
|
||||
Extends the old plan's C5 batch with the vendor-submodule workflow: extract
|
||||
generic harness logic into `vendor/tinyagents`, bump the gitlink, shrink the
|
||||
local file to product residue, then delete. Two extractions already shipped
|
||||
this way (`NoProgressTracker` → 1.5.0, MicrocompactMiddleware → `ac73382`,
|
||||
upstream PR pending on `feat/microcompact-middleware`).
|
||||
|
||||
Ordered by value (generic-LOC reclaimed locally + capability gained by the
|
||||
crate). Figures are non-test lines; tests roughly double each.
|
||||
|
||||
## 1. Tool-call dialect layer (~1,900) — `pformat.rs` + `dispatcher.rs` + `harness/parse.rs`
|
||||
|
||||
- Crate gains: a `ToolCallDialect` trait beside native tool calling —
|
||||
P-Format compact positional calls (`name[a|b]`, ~80% token cut on
|
||||
tool-heavy turns — a genuine crate selling point for small/local models
|
||||
that lack native tool calling), permissive XML/JSON parsing with arg-key
|
||||
drift recovery (`TOOL_ARG_KEYS`: arguments|args|parameters|params|input).
|
||||
- Product residue: none identified — zero DomainEvent coupling.
|
||||
- Unlock: old-plan C1 step 4 becomes a pure local delete once no live path
|
||||
parses provider text (transcript compat reads move with it).
|
||||
|
||||
## 2. Multimodal attachment resolver (~1,550 of 1,690) — `multimodal.rs`
|
||||
|
||||
- Crate gains: marker → provider content-block resolution, mime allowlist,
|
||||
PDF text extraction, fetch gating, truncation budgets.
|
||||
- Product residue: the `[IMAGE:]`/`[FILE:]` marker convention itself.
|
||||
- Note: land after doc 02 so thinking/reasoning block handling doesn't need
|
||||
to be ported twice.
|
||||
|
||||
## 3. Overflow-to-artifact tool-result store (~500) — `tool_result_artifacts/`
|
||||
|
||||
- Already built on crate `Store`; overflow-to-artifact is a generic harness
|
||||
concern (pairs with the crate's microcompact). Residue: PII scrub hook.
|
||||
- Include `subagent_runner/extract_tool.rs` (612) + `handoff.rs` (287):
|
||||
progressive-disclosure Q&A over handoff-cached oversized results — the
|
||||
natural companion API.
|
||||
|
||||
## 4. Durable TaskStore upgrade + detached lifecycle (~crate work enabling a 1,631-line local delete)
|
||||
|
||||
- Crate gains: SQLite-backed `TaskStore` (behind the existing `sqlite`
|
||||
feature) with lifecycle history, cancellation records, wait/kill/steer
|
||||
handles, replay/listing by parent/root/thread (goal.md §4/§11).
|
||||
- Unlock: `agent_orchestration/running_subagents.rs` 1931 → ≤300 (old-plan
|
||||
C6.2) stops being an adapter squeeze and becomes a projection.
|
||||
|
||||
## 5. Hook trait machinery (~450) — `hooks.rs` + `stop_hooks.rs`
|
||||
|
||||
- Crate gains: `PostTurnHook`/`StopHook` traits + scheduling shell (incl.
|
||||
the archivist's ~200-line hook-scheduling shell). Product hook bodies
|
||||
(archivist, memory, cost stop-hooks) stay local.
|
||||
|
||||
## 6. Host runtime adapter core (~350) — `host_runtime.rs`
|
||||
|
||||
- Native/Docker `RuntimeAdapter` overlaps crate `WorkspaceIsolation`;
|
||||
merge as backends of one crate abstraction.
|
||||
|
||||
## 7. Fuzzy tool ranker (~250) — `tool_filter.rs`
|
||||
|
||||
- Generic ranking into `ContextualToolSelectionMiddleware` (whose shadow
|
||||
flip is old-plan C3.1); Composio input types stay product.
|
||||
|
||||
## 8. LLM triage node (~800 of 3,779) — `triage/` evaluator core
|
||||
|
||||
- Crate gains: a generic "classify with tiered model fallback + cache +
|
||||
verdict parse" graph node. Envelope/escalation/events (Composio,
|
||||
DomainEvents, agent ids) stay product.
|
||||
|
||||
## 9. Small fry
|
||||
|
||||
- `ArgRecoveryMiddleware` core (~150).
|
||||
- Task-local context carriers (`fork_context`, `sandbox_context`,
|
||||
`task_recency_context`, `turn_attachments_context`, ~500): not an
|
||||
extraction — replace with typed fields on the crate execution context
|
||||
(`ToolExecutionContext`/`RunContext`), old-plan C6.4. Requires a crate API
|
||||
addition (generic `extensions: TypeMap` on the run context is the clean
|
||||
form) — do that API change first, then delete all four locally.
|
||||
|
||||
## Workflow per extraction (checklist)
|
||||
|
||||
1. Branch in `vendor/tinyagents` (`feat/<name>`); port code + tests to crate
|
||||
idioms (types.rs/test.rs split, ≤500-line files, module README).
|
||||
2. `cargo fmt && cargo clippy --all-targets -- -D warnings && cargo test`
|
||||
in the submodule.
|
||||
3. Bump gitlink in OpenHuman on a paired branch; swap local callers to the
|
||||
crate type; shrink local file to residue (or delete); run OpenHuman
|
||||
suites per the two-branch CI model.
|
||||
4. Tick `99-deletion-ledger.md` in the old plan folder (it remains the
|
||||
master ledger) and note the extraction here.
|
||||
5. Push submodule branch to `tinyhumansai/tinyagents`, open upstream PR.
|
||||
Local gitlink may run ahead of upstream merge (already the case for
|
||||
`ac73382`) — keep the queue shallow: ≤2 unmerged submodule branches.
|
||||
|
||||
## License note
|
||||
|
||||
tinyagents is GPL-3.0-only; OpenHuman consumes it in-tree already, so
|
||||
extraction changes nothing legally — but extracted code becomes GPL. Flag
|
||||
any file with third-party-licensed snippets before moving (none known).
|
||||
@@ -1,80 +0,0 @@
|
||||
# 07 — Execution Order, Gates, and Reclaim
|
||||
|
||||
This plan runs **alongside** the old plan's C0–C7 (which continues:
|
||||
wave 3 = flip `session_shadow_reads`, flip crate BudgetMiddleware to
|
||||
enforcing, microcompact upstream PR). The vendor workstreams here are named
|
||||
**V1–V6** to avoid collision.
|
||||
|
||||
## Workstreams
|
||||
|
||||
| WS | Doc | Theme | Effort | Blocks / unblocks |
|
||||
| -- | --- | ----- | ------ | ----------------- |
|
||||
| V1 | 04 §2–3 | Non-breaking perf (schema cache, incremental cache key) | S–M | none; immediate win |
|
||||
| V2 | 02 | Reasoning end-to-end | L | deletes `ThinkingForwarder` (C7); feeds C3 pricing |
|
||||
| V3 | 03 | invoke_stream + sub-agent propagation + tool lifecycle | L | C4 progress_tracing deletion; `tool_progress.rs` delete |
|
||||
| V4 | 04 §1,4,5 | Arc'd messages (trait break), parallel tools, translation memo | M | coordinated gitlink bump with adapter changes |
|
||||
| V5 | 05 | Native Anthropic + cache_control shaping | L→XL | needs V2 for thinking; enables 02.2/`reliable.rs` re-eval |
|
||||
| V6 | 06 | Upstream extraction batch (dialects, multimodal, artifacts, TaskStore, hooks, ranker, triage node) | rolling | each unlocks a ledger delete; TaskStore item unlocks C6.2 |
|
||||
|
||||
## Suggested sequence
|
||||
|
||||
```
|
||||
now ──► V1 (quick, non-breaking)
|
||||
├─► V2 reasoning ──► V5 Anthropic (thinking replay) ─► 02.2 re-eval
|
||||
├─► V3 streaming ──► C4 progress deletion
|
||||
├─► V4 perf-breaking (single coordinated bump after V2/V3 merge)
|
||||
└─► V6 extractions (continuous, 1–2 in flight, ≤2 unmerged submodule branches)
|
||||
old plan in parallel: wave 3 (C1 read flip, C3 budget flip) — independent
|
||||
```
|
||||
|
||||
Rationale:
|
||||
|
||||
- V1 first: zero-risk, benefits every live turn, no API changes.
|
||||
- V2 before V5: thinking blocks must exist in `ContentBlock` before the
|
||||
Anthropic adapter can replay them; V5 Step 1 (text/tools) may start early.
|
||||
- V2 and V3 both touch `invoke_model_streaming_once` — land V2 first
|
||||
(smaller diff), rebase V3.
|
||||
- V4's `ChatModel` trait break rides one coordinated gitlink bump so
|
||||
OpenHuman's adapter updates land atomically with it.
|
||||
- C1 (sessions read flip) is **independent** of all V-work — don't serialize
|
||||
behind it; but the dialect extraction (V6.1) should merge before C1
|
||||
step 4's dispatcher/parse/pformat delete.
|
||||
|
||||
## Gates (each item ships only when its gate is green)
|
||||
|
||||
| Item | Gate |
|
||||
| ---- | ---- |
|
||||
| V2 merge | crate reasoning e2e tests + OpenHuman turn with thinking model shows deltas + persisted blocks + nonzero reasoning_tokens |
|
||||
| ThinkingForwarder delete | V2 gitlink bump live in one release, no `[thinking]` divergence logs |
|
||||
| V3 merge | nested sub-agent stream test green; `AgentProgress` projection parity vs current UI events (fixture diff) |
|
||||
| progress_tracing delete | C4 journal parity AND V3 projection parity |
|
||||
| V4 merge | crate bench: ≥50% allocation reduction on 500-msg synthetic; OpenHuman conformance suite green single-threaded |
|
||||
| Parallel tools ON (cap>1) | approval-gate batch-parking reviewed; read-only tool families only |
|
||||
| V5 first traffic | flag-gated non-turn traffic; cost/cache telemetry matches `inference/` within tolerance |
|
||||
| Each V6 extraction | crate tests + local residue shrink + ledger tick + upstream PR opened |
|
||||
|
||||
## Expected reclaim (local, incl. tests; adds to old plan §5's ~29k)
|
||||
|
||||
| Source | Lines |
|
||||
| ------ | ----- |
|
||||
| `ThinkingForwarder` + `tool_progress.rs` + progress projection residue (V2+V3) | ~700 |
|
||||
| progress_tracing stack (C4, unblocked by V3) | ~2,000 (already counted in old plan) |
|
||||
| Dialect layer local delete (V6.1 + C1.4) | ~1,900+tests (counted in old plan C1) |
|
||||
| `running_subagents.rs` squeeze via durable TaskStore (V6.4 + C6) | ~1,600 (counted in old plan C6) |
|
||||
| Task-local context carriers via crate RunContext extensions (V6.9) | ~500 |
|
||||
|
||||
Net-new local reclaim beyond the old plan: **~1–2k lines** — the headline
|
||||
value of V-work is not deletion but **capability and cost**: correct thought
|
||||
tokens, live sub-agent streaming, prompt-cache dollars, and a faster loop on
|
||||
every turn. The old plan's ~29k reclaim proceeds in parallel and several of
|
||||
its gates (C4, C6.2, C1.4, C3 pricing, C7 ThinkingForwarder) become
|
||||
*reachable* only because of V2/V3/V5/V6.
|
||||
|
||||
## Tracking
|
||||
|
||||
- Deletions continue to tick `../tinyagents-full-migration-plan/99-deletion-ledger.md`.
|
||||
- Submodule branch queue + upstream PR status tracked in this folder's
|
||||
README as extractions ship (keep ≤2 unmerged).
|
||||
- Re-audit `docs/tinyagents-sdk-gaps.md` after V2/V3/V5 land — they close
|
||||
gaps §3 (reasoning stream), part of §6 (event fidelity), §7 pricing
|
||||
inputs, and the provider half of §8.
|
||||
@@ -1,79 +0,0 @@
|
||||
# TinyAgents Vendor Improvement & Harness Migration Plan
|
||||
|
||||
Status: new plan (2026-07-04). Branch: `docs/tinyagents-vendor-audit-plan`.
|
||||
|
||||
This folder is the successor planning surface to
|
||||
[`../tinyagents-full-migration-plan/`](../tinyagents-full-migration-plan/)
|
||||
(whose `CONTINUATION-2026-07.md` C0–C7 workstreams remain the authoritative
|
||||
*migration* backlog). What changed and why this folder exists:
|
||||
|
||||
1. **TinyAgents is now a vendored, editable submodule** (`vendor/tinyagents`,
|
||||
pinned at `v1.5.0` + `ac73382` MicrocompactMiddleware, patched into both
|
||||
Cargo worlds via `[patch.crates-io]`). Every "file an upstream issue and
|
||||
wait" item in the old plan is now **direct work we can do in-tree**, test
|
||||
against OpenHuman immediately, and PR upstream from the submodule.
|
||||
2. A fresh ground-truth audit (2026-07-04, three parallel sweeps: harness
|
||||
inventory, crate internals, docs status) surfaced **crate-side defects and
|
||||
gaps that no prior doc captured** — dropped reasoning tokens, a
|
||||
preset-only "Anthropic" provider, observability-only prompt caching, and
|
||||
hot-loop cloning. Fixing these in the vendor crate multiplies the value of
|
||||
every migration workstream.
|
||||
|
||||
## Folder map
|
||||
|
||||
| Doc | Theme |
|
||||
| --- | ----- |
|
||||
| [`00-audit-harness.md`](00-audit-harness.md) | What's left in `src/openhuman/agent/` (+ orchestration/inference), classified: adapter / migratable / product |
|
||||
| [`01-audit-tinyagents.md`](01-audit-tinyagents.md) | Crate capability map + concrete defects found (file:line) |
|
||||
| [`02-reasoning-thought-tokens.md`](02-reasoning-thought-tokens.md) | End-to-end thought-token plan: `ContentBlock::Thinking`, signatures, deltas, usage/cost |
|
||||
| [`03-streaming.md`](03-streaming.md) | Stream-returning harness entry point, sub-agent delta propagation, tool-call lifecycle events |
|
||||
| [`04-performance.md`](04-performance.md) | Clone elimination, schema caching, cache-key hashing, parallel tool execution |
|
||||
| [`05-anthropic-prompt-caching.md`](05-anthropic-prompt-caching.md) | Native Anthropic Messages provider + `cache_control` request shaping |
|
||||
| [`06-upstream-extraction.md`](06-upstream-extraction.md) | OpenHuman logic to move INTO tinyagents (extends old-plan C5) + deletions unlocked |
|
||||
| [`07-execution-order.md`](07-execution-order.md) | Sequencing, gates, dependency graph vs C0–C7, reclaim estimates |
|
||||
|
||||
## Headline findings (TL;DR)
|
||||
|
||||
**Migration headroom.** `src/openhuman/agent/` is ~57k LOC (+~26k in
|
||||
`agent_orchestration/`, ~12k adapter in `src/openhuman/tinyagents/`). The turn
|
||||
loop already runs on tinyagents (`run_turn_via_tinyagents_shared`); the
|
||||
remaining deletable/migratable surface is ~29k LOC, dominated by the session/
|
||||
transcript shell (~13k), sub-agent runner (~6k), orchestration lifecycle glue
|
||||
(~3.7k), progress/tracing projection (~2.9k), and tool-call dialects (~1.9k).
|
||||
See `00-audit-harness.md`.
|
||||
|
||||
**Crate defects that block deletions today** (all now fixable in-vendor):
|
||||
|
||||
- **Reasoning is dropped end-to-end**: the OpenAI SSE path hardcodes
|
||||
`reasoning: ""`, `StreamAccumulator::finish` discards accumulated
|
||||
reasoning, the loop's middleware `ModelDelta` omits it, and
|
||||
`Usage.reasoning_tokens` is never populated. This is why OpenHuman's
|
||||
`ThinkingForwarder` shim still exists.
|
||||
- **"Anthropic" is an OpenAI-compat preset**, not a Messages-API adapter: no
|
||||
`cache_control` prompt caching, no thinking blocks, no
|
||||
`cache_creation_input_tokens` accounting.
|
||||
- **Prompt-cache shaping is observability-only**: `cache_segments` /
|
||||
`cacheable_prefix_ids()` never reach any wire field.
|
||||
- **Hot-loop cloning**: full message history cloned per turn *and* per
|
||||
retry/fallback attempt; tool schemas rebuilt per iteration; response-cache
|
||||
key re-serializes the whole transcript per call.
|
||||
- **No caller-facing stream**: `invoke_streaming` returns a completed run;
|
||||
sub-agents never stream to parents; no tool-call start/complete events.
|
||||
- **Tools execute strictly serially** despite a `parallel_tool_calls`
|
||||
capability flag.
|
||||
|
||||
**Sequencing in one line**: fix reasoning + streaming + perf in the vendor
|
||||
crate first (02/03/04 — they unblock `ThinkingForwarder` deletion and improve
|
||||
every live turn immediately), land the Anthropic provider (05) next, and run
|
||||
upstream extraction (06) in parallel with the old plan's C1 sessions cutover.
|
||||
|
||||
## Rules (inherited, unchanged)
|
||||
|
||||
- Approval/security/sandbox/workspace/credential boundaries are inviolate.
|
||||
- JSON-RPC contracts stay stable unless a migration note lands with the change.
|
||||
- Adapter first → proven parity → delete; deletions are mandatory and named.
|
||||
- Vendor-crate changes: commit in `vendor/tinyagents` on a feature branch,
|
||||
bump the gitlink in OpenHuman, PR the submodule diff upstream
|
||||
(precedent: `NoProgressTracker` #7, MicrocompactMiddleware `ac73382`).
|
||||
- Stage files explicitly (`git add <paths>`); verify branch before commit.
|
||||
- Keep every doc in this folder ≤500 lines.
|
||||
@@ -1,115 +0,0 @@
|
||||
# Directory listing: batch follow data via GraphQL
|
||||
|
||||
**Status:** proposed (backend work) · **Owner:** Agent World / tiny.place
|
||||
**Related:** `app/src/agentworld/pages/DirectorySection.tsx`, `src/openhuman/tinyplace/manifest.rs`
|
||||
|
||||
## Problem
|
||||
|
||||
The Directory page (`DirectorySection.tsx`) renders one card per registered agent.
|
||||
Historically each card issued **two** REST/JSON-RPC calls on mount:
|
||||
|
||||
- `follows.stats(agentId)` — to show the follower count.
|
||||
- `follows.followers(agentId, { limit: 100 })` — only to decide whether *the
|
||||
current user* follows that agent.
|
||||
|
||||
For an `N`-agent directory that is `1 + 2N` requests on a single page load
|
||||
(e.g. 50 agents → ~101 requests), which trips tiny.place rate limits.
|
||||
|
||||
## What already shipped (frontend-only mitigation)
|
||||
|
||||
`DirectorySection` now fetches the viewer's **following set once** via
|
||||
`follows.following(myAgentId, { limit: 500 })` and derives each card's
|
||||
follow-state locally (`useMyFollowing`). That removes the per-card
|
||||
`followers` lookup → roughly halves the requests (`1 + N + 1`).
|
||||
|
||||
The per-card **follower count** (`follows.stats`) is still `N` requests because
|
||||
`directory.listAgents()` does not return counts. Eliminating those needs the
|
||||
backend change below.
|
||||
|
||||
## Proposed backend change
|
||||
|
||||
Add a single GraphQL query on the tiny.place backend that returns the directory
|
||||
already joined with follow data, so the whole page is **one** request.
|
||||
|
||||
### GraphQL schema (tiny.place backend repo: `tinyhumansai/backend`)
|
||||
|
||||
```graphql
|
||||
type DirectoryAgent {
|
||||
agentId: String!
|
||||
name: String
|
||||
description: String
|
||||
username: String
|
||||
skills: [String!]
|
||||
tags: [String!]
|
||||
followerCount: Int! # aggregate from the follows table
|
||||
isFollowedByViewer: Boolean! # viewer follows this agent (join on viewerAgentId)
|
||||
}
|
||||
|
||||
type DirectoryAgentsResult {
|
||||
agents: [DirectoryAgent!]!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
directoryAgents(
|
||||
viewerAgentId: String # optional; null → isFollowedByViewer = false
|
||||
limit: Int = 100
|
||||
offset: Int = 0
|
||||
): DirectoryAgentsResult!
|
||||
}
|
||||
```
|
||||
|
||||
`followerCount` should be a grouped aggregate (`COUNT(*) … GROUP BY followee`)
|
||||
and `isFollowedByViewer` a left-join on `(follower = viewerAgentId, followee =
|
||||
agentId)` — both computed in one query, no per-agent fan-out.
|
||||
|
||||
### Rust SDK passthrough (`tinyplace` crate)
|
||||
|
||||
Mirror the existing GraphQL methods (e.g. `ledger_transactions`):
|
||||
|
||||
```rust
|
||||
// tinyplace SDK
|
||||
pub async fn directory_agents(
|
||||
&self,
|
||||
params: Option<&DirectoryAgentsParams>,
|
||||
) -> Result<DirectoryAgentsResult, Error> { /* GraphQL POST */ }
|
||||
```
|
||||
|
||||
### OpenHuman core controller (`src/openhuman/tinyplace/manifest.rs`)
|
||||
|
||||
Add a handler alongside `handle_tinyplace_graphql_ledger_transactions`:
|
||||
|
||||
```rust
|
||||
// method: "openhuman.tinyplace_graphql_directory_agents"
|
||||
pub(crate) fn handle_tinyplace_graphql_directory_agents(
|
||||
params: Map<String, Value>,
|
||||
) -> ControllerFuture { /* deserialize params → client.directory_agents(...) → JSON */ }
|
||||
```
|
||||
|
||||
Register it in the same controller table as the other `tinyplace_graphql_*`
|
||||
methods, and add a `*_degrade` fallback (empty list) consistent with the
|
||||
existing passthroughs.
|
||||
|
||||
### Frontend client (`app/src/lib/agentworld/invokeApiClient.ts`)
|
||||
|
||||
Add under the `graphql` namespace:
|
||||
|
||||
```ts
|
||||
directoryAgents: (params?: { viewerAgentId?: string; limit?: number; offset?: number }) =>
|
||||
call<DirectoryAgentsResult>('openhuman.tinyplace_graphql_directory_agents', { ...params }),
|
||||
```
|
||||
|
||||
### Frontend page (`DirectorySection.tsx`)
|
||||
|
||||
Replace `directory.listAgents()` + `useMyFollowing` + per-card `follows.stats`
|
||||
with a single `graphql.directoryAgents({ viewerAgentId: myAgentId })` call.
|
||||
Each card then reads `followerCount` and `isFollowedByViewer` straight off the
|
||||
row — **one** request for the whole page. Keep `follows.follow/unfollow` for
|
||||
the optimistic toggle.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Loading an `N`-agent directory issues exactly **1** data request (plus wallet
|
||||
identity), down from `1 + 2N`.
|
||||
- Follower counts and follow-state match the current per-card behavior.
|
||||
- Pagination supported via `limit`/`offset`.
|
||||
@@ -278,7 +278,7 @@ Memory encryption keys derive from user credentials via Argon2id, ensuring memor
|
||||
- **Auth handoff**: Web-to-desktop authentication uses single-use login tokens with 5-minute TTL, exchanged via Rust HTTP client (bypasses CORS)
|
||||
- **Network TLS**: All WebSocket and HTTP connections use rustls, no dependency on platform OpenSSL
|
||||
- **State management**: Sensitive data lives in Redux (memory) and OS keychain (persistent). No localStorage for credentials or tokens
|
||||
- **Prompt injection guard**: User prompts are normalized/scored and enforced server-side (`allow | review | block`) before model/tool execution. See [`docs/PROMPT_INJECTION_GUARD.md`](../../docs/PROMPT_INJECTION_GUARD.md)
|
||||
- **Prompt injection guard**: User prompts are normalized/scored and enforced server-side (`allow | review | block`) before model/tool execution. See `src/openhuman/prompt_injection/`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -57,8 +57,6 @@ OpenHuman pins `tinyagents = { version = "1.5.0", features = ["sqlite"] }` (see
|
||||
- **`repl` / expressive-language features unused.** OpenHuman drives graphs from Rust (`GraphBuilder`), not the crate's `.rag` REPL language.
|
||||
- **Adapter map (feature-gated SDK piece → OpenHuman replacement):** provider clients → `ProviderModel`; crate SQLite checkpointer rows not yet adopted → `SqlRunLedgerCheckpointer`; task/status stores not yet controller-canonical → OpenHuman SQL/JSON run ledgers (`running_subagents`, `workflow_runs`, `agent_teams`, `command_center`). The generic harness/graph/middleware/event primitives are used as-is.
|
||||
|
||||
Migration backlog and per-phase tasks live in [`docs/tinyagents-migration-spec.md`](../../../docs/tinyagents-migration-spec.md). The current harness inventory snapshot lives in [`docs/tinyagents-harness-migration-audit.md`](../../../docs/tinyagents-harness-migration-audit.md).
|
||||
|
||||
The agent harness is the runtime that turns a user message (or a webhook fire, or a cron tick) into a complete, tool-using LLM interaction. It owns the tool-call loop, sub-agent dispatch, the trigger-triage pipeline, and the hook surface around them. It does **not** own provider HTTP transport, tool implementations, prompt-section assembly, or memory storage - those are separate domains the harness composes.
|
||||
|
||||
This page walks through what happens in one turn, then zooms in on each of the moving parts.
|
||||
@@ -458,7 +456,7 @@ Every agent turn — chat (`harness/session/turn/core.rs`), channel/CLI (`harnes
|
||||
- **Sub-agent build pipeline** (`subagent_runner/`) — definition resolution, archetype tool filtering, provider resolution, narrow prompt building, memory context, worker-thread mirror, handoff cache, checkpoint/resume — stays openhuman-owned. Sub-agents already *execute* on the harness; the crate's generic `SubAgentTool` would discard this pipeline for marginal crate-native depth tracking (openhuman's `spawn_depth_context` already bounds recursion).
|
||||
- **Durable run ledgers** (`workflow_runs`, `agent_teams`, `command_center`, `subagent_sessions`) stay on openhuman SQLite/JSON until their controller projections and restart semantics are mapped onto TinyAgents task/status/journal records. The `agent_teams` race-safe SQL compare-and-swap task claim remains OpenHuman-owned.
|
||||
|
||||
> **Note:** TinyAgents 1.3+ ships harness store/cache/session primitives (`harness::store` with JSONL append stores, `harness::cache`, `harness::subagent`, lineage-aware status) plus graph task stores and conformance contracts. The plan for migrating the session shell, sub-agent pipeline, and detached-task lifecycle onto those primitives lives in [`docs/tinyagents-harness-migration-audit.md`](../../../docs/tinyagents-harness-migration-audit.md).
|
||||
> **Note:** TinyAgents 1.3+ ships harness store/cache/session primitives (`harness::store` with JSONL append stores, `harness::cache`, `harness::subagent`, lineage-aware status) plus graph task stores and conformance contracts. The session shell, sub-agent pipeline, and detached-task lifecycle are still being migrated onto those primitives.
|
||||
|
||||
## See also
|
||||
|
||||
|
||||
@@ -180,5 +180,4 @@ Each connected account gets its own profile and its own IDB. CDP can snapshot on
|
||||
|
||||
## See also
|
||||
|
||||
* [`docs/TAURI_CEF_FINDINGS_AND_CHANGES.md`](../../docs/TAURI_CEF_FINDINGS_AND_CHANGES.md). the notification-permission deep dive.
|
||||
* [`CLAUDE.md`](../../CLAUDE.md). the canonical "no new JS injection" rule.
|
||||
|
||||
Reference in New Issue
Block a user