Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6824129
fix(ci): unblock release workflow actions (#4610)
senamakel Jul 7, 2026
03a05be
chore(staging): v0.58.13 [skip ci]
github-actions[bot] Jul 8, 2026
81df38f
chore(staging): v0.58.14 [skip ci]
github-actions[bot] Jul 8, 2026
c2c2036
docs(tinyagents): add P1-8 model-layer inversion drift row
senamakel Jul 9, 2026
b8eda9d
refactor(inference): migrate one-shot callers to crate ChatModel
senamakel Jul 9, 2026
8090f13
docs(tinyagents): record P1-8/P1-9 inversion findings
senamakel Jul 9, 2026
eea3a7e
docs(tinyagents): record P1-9 harness-inversion blocker
senamakel Jul 9, 2026
9ff3cab
docs(tinyagents): Phase 3 router-registry design + premise correction
senamakel Jul 9, 2026
9701b86
feat(docs): add referral system documentation
senamakel Jul 9, 2026
b1e2f3c
Merge remote-tracking branch 'upstream/main' into feat/inference-mode…
senamakel Jul 9, 2026
30c7dfd
refactor(agent): channel/bus turn path holds TurnModelSource, not Pro…
senamakel Jul 9, 2026
0f487f7
docs(tinyagents): record Motion A progress — channel/bus path migrated
senamakel Jul 9, 2026
8db8887
refactor(agent): subagent turn path holds TurnModelSource, not Provider
senamakel Jul 9, 2026
11166cd
docs(tinyagents): record subagent-runner path migrated (Motion A)
senamakel Jul 9, 2026
6106ced
refactor(agent): extract_from_result tool holds TurnModelSource, not …
senamakel Jul 9, 2026
9112330
refactor(agent): Agent session path holds TurnModelSource, not Provider
senamakel Jul 9, 2026
0d4a219
docs(tinyagents): Motion A structurally complete (all turn paths migr…
senamakel Jul 9, 2026
4116f22
style(agent): cargo fmt the TurnModelSource migration
senamakel Jul 9, 2026
d313a2c
chore: drop unrelated e2e spec churn from upstream-sync merge
senamakel Jul 9, 2026
e8afef1
chore(vendor): bump tinyagents to main w/ OpenAiModel auth styles (ti…
senamakel Jul 9, 2026
0c85113
chore(vendor): pin tinyagents to openai-host-compat integration (auth…
senamakel Jul 10, 2026
c4ea1cc
feat(inference): scaffold crate-native OpenAiModel client builder (Mo…
senamakel Jul 10, 2026
10e719c
chore(vendor): re-pin tinyagents to released main (auth+temp+merge me…
senamakel Jul 10, 2026
8d2321d
feat(inference): factory-level crate OpenAiModel builder (Motion B cu…
senamakel Jul 10, 2026
b6e95c1
feat(inference): managed OpenHuman backend as host ChatModel (Motion B)
senamakel Jul 10, 2026
7e98c1b
feat(inference): route managed create_chat_model to crate-native back…
senamakel Jul 10, 2026
8ad2a15
feat(inference): crate-native local-runtime ChatModel builder + tinya…
senamakel Jul 10, 2026
034fac3
Merge remote-tracking branch 'upstream/main' into feat/inference-crat…
senamakel Jul 10, 2026
6ce3a6f
test(inference): managed-vs-local create_chat_model routing (Motion B)
senamakel Jul 10, 2026
85f7c4e
fix(agent): repair Motion A test-debt + bump tinyagents to v1.8.0 (#4…
senamakel Jul 10, 2026
5109b2a
fix(test): repair async device-tool calls in orchestration e2e (unblo…
senamakel Jul 10, 2026
82ccb3f
fix(tests,cron): repair stale behavior-level tests + surface provider…
senamakel Jul 10, 2026
9e3f748
fix: resolve remaining coverage-job failures (3 merge-debt tests + cr…
senamakel Jul 10, 2026
b1594d6
test(inference): serialize create_chat_provider tests with the global…
senamakel Jul 10, 2026
48815f4
docs(tinyagents): record PR #4769 CI-green milestone
senamakel Jul 10, 2026
e226aba
feat(inference): cut local runtimes to crate-native ChatModel (Motion B)
senamakel Jul 10, 2026
dfe9403
docs(tinyagents): local runtimes cut over; BYOK cloud deferred to Pha…
senamakel Jul 10, 2026
9a0aa1b
Merge remote-tracking branch 'upstream/main' into feat/inference-crat…
senamakel Jul 10, 2026
36c1dca
fix(cron): defer loopback-offline to upstream's "stays retryable" beh…
senamakel Jul 11, 2026
aa06b44
test(raw_coverage): restore turn_model_source in tests taken from ups…
senamakel Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 82 additions & 1 deletion docs/tinyagents-drift-ledger.md

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions docs/tinyagents-phase3-router-registry-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Phase 3 — RouterProvider → crate ModelRegistry: design & ground truth

**Status:** design (starting Phase 3). Grounded in a full read of both the crate
(`vendor/tinyagents/src/harness/model`, `harness/agent_loop`, `harness/retry`,
`registry/`) and the host seam (`src/openhuman/tinyagents/{mod,routes,model}.rs`,
`inference/provider/{router,factory}.rs`).
**Relates to:** `docs/tinyagents-inference-migration-plan.md` (Phase 3),
`docs/tinyagents-drift-ledger.md` (P1-9), issue #4249.

---

## 1. The premise correction (important)

The plan framed Phase 3 as "implement RouterProvider → crate ModelRegistry in
`vendor/tinyagents`." **Investigation shows the registry migration is already
done at the seam, and the crate already does everything the host router needs —
there is no hard upstream gap.** Specifically, `assemble_turn_harness`
(`tinyagents/mod.rs:1240-1353`) already:

- Registers the primary and every workload-tier route into the crate
`ModelRegistry` (`harness.register_model(name, model)`; primary via
`set_default_model`).
- Wires cross-route fallback as a **crate** `RunPolicy.fallback =
routes::route_fallback_policy(model)` — traversed natively by
`agent_loop::invoke_model_resolving`.
- Gates vision via the crate: `RequiredCapabilitiesMiddleware` stamps
`image_in` onto the `ModelRequest`, and the crate's `resolve_request` filters
named candidates by `ModelProfile::satisfies`.
- Emits the `FallbackSelected` parity event via `FallbackObserverMiddleware`.

The two things the crate *lacks* (per the crate audit) are **not needed** by the
host:

- **Capability-based *selection*** (scan the registry for a capable model): the
host never does this — the **caller picks the tier** upstream
(`subagent_runner/ops/graph.rs` sets `model = vision-v1` for image turns); the
middleware only *validates/enforces*, it doesn't select.
- **Capability-aware *fallback***: the host's fallback chains are hand-built to
be capability-safe (`routes::same_family_fallbacks`: `vision-v1 → []`; the
same-family text alternates are all text-capable), so the crate's
non-capability-filtered `next_after` is benign here.

**Conclusion:** RouterProvider is *already* projected onto the crate registry.
What remains is host-side, and needs no crate release.

## 2. What actually survives (the real remaining work)

`RouterProvider` (`inference/provider/router.rs`) survives only as the **per-call
BYOK alias resolver *inside* the wrapped `Provider`** — at dispatch it maps a
tier alias (`reasoning-v1`, …) → concrete `(provider, model)` (issue #2079: raw
aliases 400 on OpenAI/DeepSeek). The registered tier models are still
`ProviderModel`s that wrap this host `Provider`. So the harness holds a
`Provider` for exactly one reason: **`build_turn_models` builds the per-turn
primary + routes + summarizer `ProviderModel`s from one `Provider` handle**
(`tinyagents/mod.rs:1139-1175`, `routes::build_route_models`).

Two independent motions remain, in priority order:

### Motion A — Harness holds crate `ChatModel`s (this Phase; host-only)
Move `build_turn_models` construction from the harness turn path
(`agent/harness/graph.rs::run_channel_turn_via_graph`, session turn path) to the
**producer/factory boundary**, so `agent/` holds crate model types, not
`Provider`. The harness turn path today reads four `Provider` methods before
building — all are already available without the trait:

| harness reads today | crate-native source |
| --- | --- |
| `provider.supports_native_tools()` | `TurnModels.primary.profile().tool_calling` |
| `provider.supports_vision()` | `…profile().modalities.image_in` |
| `provider.effective_context_window(model).await` | resolved at build time → `…profile().max_input_tokens` |
| `provider.telemetry_provider_id()` | new `TurnModels.provider_id: String` |

So `TurnModels` becomes the unit the harness holds. Design points:

1. **Extend `TurnModels`** with `provider_id: String` and small accessors
(`native_tools()`, `supports_vision()`, `context_window()`) reading
`primary.profile()`. Removes every raw-`Provider` read in the harness graph.
2. **New factory entry** `inference::provider::factory::create_turn_models(
role_or_model, config, temperature) -> anyhow::Result<TurnModels>`: builds the
`Provider` internally (existing `create_chat_provider` path), resolves the
async `effective_context_window`, computes `telemetry_provider_id`, and calls
the seam `build_turn_models`. All `Provider` naming stays in
`inference/provider/` + the seam.
3. **Lifecycle:** `build_turn_models` is per-`(provider, model)`; the harness
builds a fresh `TurnModels` per turn today. Keep that — the producer builds
`TurnModels` at turn-request assembly (channels processor, session turn entry,
subagent runner) instead of the harness graph doing it. `error_slot` stays
per-turn (correct — it recovers *this* turn's provider error).
4. **Type swap:** `AgentTurnRequest.provider: Arc<dyn Provider>` → carries the
built `TurnModels` (+ the `model`/`provider_name` it already has);
`Agent`/`AgentBuilder.provider` likewise. `run_channel_turn_via_graph` /
session `turn/graph.rs` receive `TurnModels`, read caps via the accessors, and
pass it straight to `run_turn_via_tinyagents_shared` (which already takes
`TurnModels`). `IntelligentRoutingProvider` stays a `Provider` impl
(provider-stack member); the factory wraps it before `build_turn_models`.

Exit: no `agent/` file names the `Provider` trait; `ProviderModel` built only in
the seam (`model.rs` / `build_turn_models`), reached only via the factory entry.
Zero behavior change (same `ProviderModel`s, same registry/fallback wiring).

### Motion B — Registered models become crate-native (later; the big LOC win)
Replace `ProviderModel` wrappers with crate `providers::openai` clients built
from config (BYOK slugs, Ollama/LM Studio base URLs), registering per-tier
clients directly so `RouterProvider`'s per-call alias resolution disappears
(each tier is its own registered client). This is inference-plan **Phase 2**
(client swap, deletes `compatible*.rs`) + the remainder of Phase 3, and keeps the
bespoke providers (managed backend, claude-code, codex) as host `ChatModel`
impls (Phase 4). Out of scope for Motion A; unblocked by it.

## 3. Optional crate nicety (not required)
If we later want capability-*aware* fallback (so a hand-built chain isn't the
only safety net), the one-line crate change is to re-apply `model_eligible`
to `FallbackPolicy::next_after` targets in `invoke_model_resolving`
(`vendor/tinyagents/src/harness/agent_loop/model_call.rs:186-204`). File as a
separate small upstream PR only if Motion B introduces capability-divergent
fallback chains. Not needed for Motion A.

## 4. First implementation slice (Motion A)
1. `TurnModels`: add `provider_id` + `native_tools()/supports_vision()/context_window()` accessors (seam-internal; behavior-neutral).
2. `create_turn_models(...)` factory entry (wraps `create_chat_provider` +
async context-window resolve + `build_turn_models`).
3. Cut `run_channel_turn_via_graph` to take `TurnModels` and read caps via
accessors (delete the 4 raw-provider reads); channel producer
(`channels/runtime/dispatch/processor.rs`) builds `TurnModels` via the factory
and puts it on `AgentTurnRequest`.
4. Repeat for the session turn path + subagent runner; swap `Agent.provider`.
5. Update tests to build `TurnModels`/crate `MockModel` instead of hand-rolled
`Provider` impls.
6. Verify: both Cargo worlds green; `json_rpc_e2e`; streaming/cost/tool-timeline
parity on a mock-backend turn (the #4460 / $0-turn / tool-timeline hazards).

## 5. Verification & parity locks (unchanged from the plan)
Provider-string grammar, `inference.*` RPC, tier alias set, fallback ordering
(single same-family alternate; vision primary-only), the once-per-logical-call
FIFO usage push (charged-USD-over-estimate precedence, graceful degradation), and
the `FallbackSelected` event must all be preserved. These are already crate-wired
— Motion A only moves *where the models are built*, not how they route.
11 changes: 6 additions & 5 deletions src/openhuman/accessibility/automate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,13 +724,14 @@ impl AutomateBackend for RealBackend {
description: &str,
) -> Result<Option<(i32, i32)>, String> {
// Use the main `chat` provider's vision model (per plan): reliable UI
// grounding, and the fallback only fires when AX is empty (rare).
let (provider, model) =
crate::openhuman::inference::provider::create_chat_provider("chat", &self.config)
// grounding, and the fallback only fires when AX is empty (rare). The
// crate `ChatModel` bakes the resolved model + temperature; the vision
// request pins temperature 0.0 itself.
let model =
crate::openhuman::inference::provider::create_chat_model("chat", &self.config, 0.0)
.map_err(|e| format!("vision provider unavailable: {e}"))?;
let coords =
super::vision_click::locate_via_vision(&*provider, &model, screenshot, description)
.await?;
super::vision_click::locate_via_vision(&model, screenshot, description).await?;
Ok(coords.map(|(px, py)| super::vision_click::image_to_screen(geom, px, py)))
}

Expand Down
20 changes: 16 additions & 4 deletions src/openhuman/accessibility/vision_click.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,28 @@ pub(crate) fn capture_window_geometry(
/// Ask the vision model for the target's pixel coordinates within `screenshot`.
/// Returns `Ok(None)` when the model reports the element isn't visible.
pub(crate) async fn locate_via_vision(
provider: &dyn crate::openhuman::inference::provider::Provider,
model: &str,
model: &std::sync::Arc<dyn tinyagents::harness::model::ChatModel<()>>,
screenshot_data_uri: &str,
description: &str,
) -> Result<Option<(i32, i32)>, String> {
use tinyagents::harness::message::Message;
use tinyagents::harness::model::ModelRequest;
// The image rides as an `[IMAGE:<uri>]` marker in the user text (promoted to a
// real image part in the provider request builder); the marker survives the
// crate `Message` → host `ChatMessage` round-trip verbatim. The vision call is
// deterministic, so pin temperature 0.0 on the request (overrides the model's
// construction temperature).
let user = build_locate_user(description, screenshot_data_uri);
let raw = provider
.chat_with_system(Some(locate_system_prompt()), &user, model, 0.0)
let request = ModelRequest::new(vec![
Message::system(locate_system_prompt().to_string()),
Message::user(user),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve image content for managed vision calls

When the chat role resolves to the managed OpenHuman backend, create_chat_model now returns OpenHumanBackendModel, which forwards this ModelRequest directly to the crate OpenAiModel instead of going through the host compatible provider's MessageContent::from_chat_text marker parser. In that path this Message::user(user) is only a text block containing [IMAGE:data:...], so the screenshot is not sent as an image and locate_via_vision cannot actually inspect the UI. Build a crate message with an actual image content block here, or keep managed vision calls on the host provider path that expands [IMAGE:] markers.

Useful? React with 👍 / 👎.

])
.with_temperature(0.0);
let response = model
.invoke(&(), request)
.await
.map_err(|e| format!("vision model call failed: {e}"))?;
let raw = response.text();
log::debug!("{LOG_PREFIX} locate raw response: {raw:?}");
parse_locate_response(&raw)
}
Expand Down
23 changes: 14 additions & 9 deletions src/openhuman/agent/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::openhuman::agent::progress::AgentProgress;
use crate::openhuman::agent::turn_origin::{self, AgentTurnOrigin};
use crate::openhuman::config::MultimodalConfig;
use crate::openhuman::inference::provider::{
current_resolved_provider_route, with_resolved_provider_route_scope, ChatMessage, Provider,
current_resolved_provider_route, with_resolved_provider_route_scope, ChatMessage,
};
use crate::openhuman::prompt_injection::{
enforce_prompt_input, PromptEnforcementAction, PromptEnforcementContext,
Expand All @@ -46,9 +46,11 @@ pub const AGENT_RUN_TURN_METHOD: &str = "agent.run_turn";
/// therefore pass trait objects (`Arc<dyn Provider>`, tool trait-object
/// registries) and streaming senders (`on_delta`) through unchanged.
pub struct AgentTurnRequest {
/// LLM provider, already constructed and warmed up by the caller.
/// Shared via Arc to allow sub-agents to reuse the same connection pool.
pub provider: Arc<dyn Provider>,
/// The turn's model source — the seam handle that builds this turn's tiered
/// crate `ChatModel` set (issue #4249, Phase 3 / Motion A). Replaces the raw
/// `Arc<dyn Provider>`: the bus/harness path names crate model types only,
/// and the `Provider` stays confined to the inference factory + seam.
pub turn_model_source: crate::openhuman::tinyagents::TurnModelSource,

/// Full conversation history including system prompt and the incoming
/// user message. The handler mutates an internal clone of this during
Expand Down Expand Up @@ -183,7 +185,7 @@ impl AgentTurnResponse {

async fn handle_agent_run_turn(req: AgentTurnRequest) -> Result<AgentTurnResponse, String> {
let AgentTurnRequest {
provider,
turn_model_source,
mut history,
tools_registry,
provider_name,
Expand Down Expand Up @@ -303,7 +305,7 @@ async fn handle_agent_run_turn(req: AgentTurnRequest) -> Result<AgentTurnRespons
// `on_progress` text deltas, so it's intentionally unused.
let _ = (&provider_name, silent, &channel_name, on_delta);
run_channel_turn_via_graph(
provider.clone(),
turn_model_source.clone(),
&mut history,
tools_registry.clone(),
extra_tools,
Expand Down Expand Up @@ -469,10 +471,11 @@ pub async fn use_real_agent_handler() -> tokio::sync::MutexGuard<'static, ()> {
mod tests {
use super::*;
use crate::core::event_bus::NativeRegistry;
use crate::openhuman::inference::provider::Provider;
use async_trait::async_trait;

/// Minimal `Provider` implementation used only to satisfy the
/// `Arc<dyn Provider>` type in [`AgentTurnRequest`]. The tests below
/// Minimal `Provider` implementation used only to build the
/// [`TurnModelSource`] in [`AgentTurnRequest`]. The tests below
/// override the bus handler with a stub that never calls any
/// provider methods, so this no-op is sufficient — the only required
/// trait method is `chat_with_system`, everything else has a default.
Expand All @@ -499,7 +502,9 @@ mod tests {
/// invoked — it only needs to satisfy the type.
fn test_request() -> AgentTurnRequest {
AgentTurnRequest {
provider: Arc::new(NoopProvider),
turn_model_source: crate::openhuman::tinyagents::TurnModelSource::new(Arc::new(
NoopProvider,
)),
history: vec![
ChatMessage::system("you are a test bot"),
ChatMessage::user("hello"),
Expand Down
7 changes: 5 additions & 2 deletions src/openhuman/agent/harness/agent_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ use tokio::sync::mpsc::Sender;
use crate::openhuman::agent::harness::run_queue::RunQueue;
use crate::openhuman::agent::harness::subagent_runner::SubagentRunError;
use crate::openhuman::agent::progress::AgentProgress;
use crate::openhuman::inference::provider::{ChatMessage, Provider};
use crate::openhuman::inference::provider::ChatMessage;
use crate::openhuman::tinyagents::TurnModelSource;
use crate::openhuman::tools::{Tool, ToolSpec};

/// The assembled inputs for one sub-agent turn, handed to a custom
Expand All @@ -38,7 +39,9 @@ use crate::openhuman::tools::{Tool, ToolSpec};
/// future without borrowing the caller's stack — mirrors the positional
/// arguments the default `run_subagent_via_graph` takes.
pub struct AgentTurnRequest {
pub provider: Arc<dyn Provider>,
/// The turn's model source — builds the sub-agent's tiered crate `ChatModel`
/// set (issue #4249, Phase 3 / Motion A). Replaces the raw `Arc<dyn Provider>`.
pub turn_model_source: TurnModelSource,
pub model: String,
pub temperature: f64,
/// Full working transcript for the turn (system + prior + this user turn).
Expand Down
9 changes: 5 additions & 4 deletions src/openhuman/agent/harness/fork_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

use crate::openhuman::agent::progress::AgentProgress;
use crate::openhuman::config::AgentConfig;
use crate::openhuman::inference::provider::Provider;
use crate::openhuman::memory::Memory;
use crate::openhuman::skills::Workflow;
use crate::openhuman::tinyagents::TurnModelSource;
use crate::openhuman::tools::{Tool, ToolSpec};
use std::collections::HashSet;
use std::path::PathBuf;
Expand All @@ -40,9 +40,10 @@ pub struct ParentExecutionContext {
/// generic `spawn_subagent` tool. Empty means no generic subagent spawns.
pub allowed_subagent_ids: HashSet<String>,

/// Parent's provider — sub-agents call into the same instance so
/// connection pools, retry budgets, and credentials are shared.
pub provider: Arc<dyn Provider>,
/// Parent's model source — sub-agents build off the same source so
/// connection pools, retry budgets, and credentials are shared (issue #4249,
/// Phase 3 / Motion A; replaces the raw `Arc<dyn Provider>`).
pub turn_model_source: TurnModelSource,

/// Parent's full tool registry. The sub-agent runner re-filters this
/// per-archetype before handing it to the sub-agent's tool loop.
Expand Down
Loading
Loading