chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
# LLM Ensemble Design: Static Lineups & Dynamic Router Selection
|
||||
|
||||
`llm_ensemble` runs a **B5 fusion** turn: several *proposer* models each draft
|
||||
an answer, and one *aggregator* model fuses those drafts into the final
|
||||
response. This document describes how the **set of models** is chosen for a
|
||||
turn. It does not cover the ensemble runtime mechanics (streaming, timeouts,
|
||||
quorum, fallback) — only model selection.
|
||||
|
||||
## Why an ensemble instead of a single model
|
||||
|
||||
Any single model has a fixed set of blind spots: the failure modes of its
|
||||
training data, its decoding randomness, and its particular biases. Asking that
|
||||
one model again doesn't remove them — it re-rolls the *same* distribution. An
|
||||
ensemble attacks the problem from a different angle: draft an answer with
|
||||
several *different* models, then have an aggregator reconcile them. The wins
|
||||
over a single-model turn:
|
||||
|
||||
- **Error cancellation / higher accuracy.** Independent models rarely make the
|
||||
*same* mistake on the same input. When drafts disagree, the aggregator can
|
||||
cross-check them and keep the answer the majority supports; when they agree,
|
||||
that agreement is real signal that the answer is solid. Idiosyncratic
|
||||
one-off errors get outvoted instead of shipped.
|
||||
- **Coverage through diversity.** Different vendors/families/architectures have
|
||||
genuinely different strengths — one is better at code, another at long-form
|
||||
reasoning, another at careful instruction-following. A lineup that spans them
|
||||
covers more of the input space than any single model's strong suit. This is
|
||||
exactly why selection *rewards diversity* (distinct vendor/family/
|
||||
architecture) rather than picking the top-N by raw quality.
|
||||
- **Robustness & availability.** A single model is a single point of failure —
|
||||
one timeout, rate-limit, or degraded response fails the whole turn. With a
|
||||
quorum of proposers the turn still succeeds as long as *enough* drafts come
|
||||
back, and the aggregator simply fuses what arrived.
|
||||
- **Reduced variance.** Fusing several drafts smooths out per-call sampling
|
||||
noise, so repeated runs of the same prompt are more stable and less
|
||||
sensitive to an unlucky roll of the dice on any one model.
|
||||
- **A critic pass, not just a vote.** The aggregator is a model in its own
|
||||
right: it can spot a draft that's confidently wrong, prefer the
|
||||
better-reasoned answer over the more verbose one, and synthesize the best
|
||||
parts of several drafts — a step a single-model turn never gets.
|
||||
|
||||
The cost is real — an N-proposer turn spends roughly N+1 model calls and its
|
||||
latency is bounded by the slowest proposer plus the aggregator. The selection
|
||||
strategies below exist to spend that budget well: match ensemble size and
|
||||
composition to how hard the turn actually is, rather than always paying for the
|
||||
largest lineup. Lower-difficulty turns get a small, cheap lineup; harder turns
|
||||
get more proposers and stronger critics.
|
||||
|
||||
## Selection strategies
|
||||
|
||||
There are three selection strategies, dispatched by
|
||||
`llm_ensemble.selection_mode` in
|
||||
`build_ensemble_provider_from_config`
|
||||
(`src/opensquilla/provider/ensemble.py`):
|
||||
|
||||
| `selection_mode` | Family | Status |
|
||||
|------------------|--------|--------|
|
||||
| `static_openrouter_b5` | Static lineup | Default for fresh configs |
|
||||
| `static_tokenrhythm_b5` | Static lineup | Supported |
|
||||
| `custom_b5` | Static lineup (user-authored) | Supported |
|
||||
| `router_dynamic` | Dynamic selection | Legacy |
|
||||
|
||||
The first two families are **static**: the lineup is fixed ahead of the turn,
|
||||
either from a packaged preset or from an explicit user-authored list. The last
|
||||
is **dynamic**: the lineup is scored and assembled per turn from the router's
|
||||
own tier decision.
|
||||
|
||||
Fresh configs default to `static_openrouter_b5`. The Web UI offers only the
|
||||
static families (preset + custom); `router_dynamic` is no longer offered there
|
||||
and stored configs surface a one-click migration to `custom_b5`. Direct
|
||||
TOML/RPC configuration keeps working for every mode.
|
||||
|
||||
---
|
||||
|
||||
# Part 1 — Static Lineups (current design)
|
||||
|
||||
A static lineup is fixed before the turn runs: a set of proposer models plus one
|
||||
aggregator model, all known ahead of time. No per-turn scoring happens. Two
|
||||
variants share this shape:
|
||||
|
||||
- **Presets** — `static_openrouter_b5` / `static_tokenrhythm_b5`: packaged,
|
||||
hard-coded lineups on a single provider.
|
||||
- **Custom** — `custom_b5`: an explicit user-authored lineup with
|
||||
role-labelled candidates and a single aggregator.
|
||||
|
||||
Both variants belong to the same **fixed-lineup defaults family**
|
||||
(`is_static_b5` in the builder) and therefore inherit the same runtime defaults
|
||||
(quorum, timeouts, no shuffle, quorum grace) — see
|
||||
[Shared fixed-lineup defaults](#shared-fixed-lineup-defaults).
|
||||
|
||||
## 1.1 Static presets
|
||||
|
||||
Source: `_build_static_b5_members`, `STATIC_B5_PROFILES`
|
||||
(`src/opensquilla/provider/ensemble.py`).
|
||||
|
||||
Each preset is a `StaticB5Profile` — four fixed proposers plus one aggregator,
|
||||
all bound to a single provider:
|
||||
|
||||
| Profile | Provider | Proposers | Aggregator |
|
||||
|---------|----------|-----------|------------|
|
||||
| `static_openrouter_b5` | `openrouter` | `deepseek/deepseek-v4-pro`, `z-ai/glm-5.2`, `moonshotai/kimi-k2.7-code`, `qwen/qwen3.7-max` | `z-ai/glm-5.2` |
|
||||
| `static_tokenrhythm_b5` | `tokenrhythm` | `deepseek-v4-pro`, `glm-5.2`, `kimi-k2.7-code`, `qwen3.7-max` | `glm-5.2` |
|
||||
|
||||
The TokenRhythm profile is a mirror of the OpenRouter one: same aggregation
|
||||
shape and defaults, the same four models, only the provider and the model-id
|
||||
naming differ (OpenRouter-style `vendor/model` slugs vs. TokenRhythm's bare
|
||||
names).
|
||||
|
||||
`_build_static_b5_members` simply materializes the profile: each proposer model
|
||||
becomes an `EnsembleMemberConfig` labeled `proposer_1..N`, the aggregator model
|
||||
becomes one labeled `aggregator`, and the selection plan records the profile
|
||||
name, proposer/aggregator models, and proposer count. There is nothing to score
|
||||
— the lineup is the profile.
|
||||
|
||||
### Credential gate
|
||||
|
||||
`static_b5_credential_available` decides whether the ensemble may run: it
|
||||
resolves an API key for every member (all four proposers + aggregator) using the
|
||||
same key-resolution order as the runtime (see
|
||||
[Member provider resolution](#member-provider-resolution)). A user whose active
|
||||
provider differs but whose environment carries the profile provider's env key
|
||||
(e.g. `OPENROUTER_API_KEY`, `TOKENRHYTHM_API_KEY`) is treated as opted in. If any
|
||||
member cannot resolve a key, the ensemble is skipped rather than posting a turn
|
||||
upstream with an empty bearer token.
|
||||
|
||||
## 1.2 Custom lineup (`custom_b5`)
|
||||
|
||||
Source: `_build_custom_b5_members`, `_custom_b5_candidates`
|
||||
(`src/opensquilla/provider/ensemble.py`); schema `LlmEnsembleCandidateConfig`
|
||||
(`src/opensquilla/gateway/config.py`).
|
||||
|
||||
`custom_b5` lets an operator author the lineup explicitly via
|
||||
`llm_ensemble.candidates`. Each candidate row carries:
|
||||
|
||||
- **`provider`** / **`model`** — required, non-empty; provider is lower-cased.
|
||||
- **`role`** — advisory label, one of `""` (unassigned), `primary`, `contrast`,
|
||||
`fast_check`, `critic`, or the structural `aggregator`. Unknown values coerce
|
||||
to `""` instead of failing, so a hand-edited config never blocks boot.
|
||||
- **`enabled`** — disabled rows are kept for read compatibility but never
|
||||
counted or run.
|
||||
|
||||
Lineup assembly (`_build_custom_b5_members`):
|
||||
|
||||
1. Every enabled row whose role is **not** `aggregator` runs as a proposer,
|
||||
labeled by its role (or `proposer_N` when unassigned).
|
||||
2. The single row with role `aggregator` fuses the drafts. Proposer rows dedupe
|
||||
on `(provider, model)`; the aggregator row may legitimately reuse a
|
||||
proposer's model (a model both drafts and fuses).
|
||||
3. **Fallback:** if no `aggregator` row exists, the aggregator falls back to the
|
||||
currently routed model — the same model the user would have gotten without
|
||||
the ensemble — so a proposer-only config still runs instead of erroring at
|
||||
turn time. The plan records `aggregator.source` as `candidate_role` or
|
||||
`inherited_model` accordingly.
|
||||
|
||||
### Lineup bounds & validation
|
||||
|
||||
Enforced by `LlmEnsembleConfig._validate_custom_b5_lineup`
|
||||
(`src/opensquilla/gateway/config.py`), checked **only** when
|
||||
`selection_mode == "custom_b5"` (presets carry fixed lineups; `router_dynamic`
|
||||
selects per turn):
|
||||
|
||||
- At most **one** enabled candidate may carry role `aggregator`.
|
||||
- Enabled proposer count must stay within
|
||||
`[CUSTOM_B5_MIN_PROPOSERS=2, CUSTOM_B5_MAX_PROPOSERS=6]`.
|
||||
- Total per-turn calls are capped at `CUSTOM_B5_MAX_TOTAL_CALLS=8`
|
||||
(proposers + aggregator).
|
||||
|
||||
### Readiness gate
|
||||
|
||||
`custom_b5_lineup_ready` returns `(ready, reason)` before wrapping the turn. It
|
||||
fails closed with a machine-readable reason when the lineup is not runnable:
|
||||
`no_proposers`, `unknown_provider:<p>`, or `missing_credential:<p>` (a member
|
||||
whose provider requires a key but resolves none). This mirrors the static-preset
|
||||
gate — a member with an empty bearer token would post the conversation upstream
|
||||
unauthenticated, so the wrap is skipped.
|
||||
|
||||
## 1.3 Shared fixed-lineup defaults
|
||||
|
||||
Both static families set `is_static_b5 = True` in
|
||||
`build_ensemble_provider_from_config`, which swaps the legacy per-turn defaults
|
||||
for the fixed-lineup family defaults. The swap is **only** applied when the
|
||||
configured value still equals the legacy default (`_static_default_if_legacy`),
|
||||
so any operator override is preserved:
|
||||
|
||||
| Parameter | Legacy (`router_dynamic`) | Fixed-lineup default |
|
||||
|-----------|---------------------------|----------------------|
|
||||
| `min_successful_proposers` | 1 | 3 (presets) / `N-1` (custom, "all but one") |
|
||||
| `proposer_timeout_seconds` | 3600 | 300 |
|
||||
| `aggregator_timeout_seconds` | 3600 | 480 |
|
||||
| `shuffle_candidates` | `True` | `False` |
|
||||
| `quorum_grace_seconds` | 0 | 30 |
|
||||
|
||||
`min_successful_proposers` is additionally clamped down to the actual proposer
|
||||
count. Both the configured and effective values (min-success, timeouts, shuffle)
|
||||
are recorded in the selection plan for debugging.
|
||||
|
||||
## 1.4 Member provider resolution
|
||||
|
||||
Every member (static or custom) resolves its concrete `ProviderConfig` through
|
||||
`_member_provider_config`, which layers member intent over the inherited/routed
|
||||
provider config:
|
||||
|
||||
- **API key** — a member-level `api_key_env` env var if set; else the inherited
|
||||
key when the member's provider matches the active provider; else the provider
|
||||
registry's env key (e.g. `OPENROUTER_API_KEY`).
|
||||
- **`base_url`** — member override, else the inherited base URL (same provider)
|
||||
or the provider spec's default base URL.
|
||||
- **`proxy` / `org_id` / `provider_routing`** — inherited only when the member
|
||||
shares the active provider; otherwise reset.
|
||||
|
||||
This is what lets a static/custom lineup run against a provider the user isn't
|
||||
actively routing to, as long as that provider's credential is present in the
|
||||
environment.
|
||||
|
||||
## 1.5 Configuration surface
|
||||
|
||||
```toml
|
||||
[llm_ensemble]
|
||||
enabled = true
|
||||
selection_mode = "static_openrouter_b5" # or static_tokenrhythm_b5 / custom_b5
|
||||
```
|
||||
|
||||
Custom lineup:
|
||||
|
||||
```toml
|
||||
[llm_ensemble]
|
||||
enabled = true
|
||||
selection_mode = "custom_b5"
|
||||
|
||||
[[llm_ensemble.candidates]]
|
||||
provider = "openrouter"
|
||||
model = "deepseek/deepseek-v4-pro"
|
||||
role = "primary"
|
||||
|
||||
[[llm_ensemble.candidates]]
|
||||
provider = "openrouter"
|
||||
model = "z-ai/glm-5.2"
|
||||
role = "contrast"
|
||||
|
||||
[[llm_ensemble.candidates]]
|
||||
provider = "openrouter"
|
||||
model = "z-ai/glm-5.2"
|
||||
role = "aggregator"
|
||||
```
|
||||
|
||||
Static presets expose no lineup tuning — the models are fixed in code. Custom
|
||||
lineups are tuned entirely through the `candidates` list (subject to the bounds
|
||||
above). Both share the fixed-lineup runtime defaults, which an operator may
|
||||
still override explicitly (`min_successful_proposers`,
|
||||
`proposer_timeout_seconds`, `aggregator_timeout_seconds`, `shuffle_candidates`).
|
||||
|
||||
---
|
||||
|
||||
# Part 2 — `router_dynamic` Selection (legacy)
|
||||
|
||||
> **Status: legacy.** `router_dynamic` remains fully supported for existing
|
||||
> configs but is no longer offered in the Web UI. Stored `router_dynamic`
|
||||
> configs surface a one-click migration to `custom_b5`. Direct TOML/RPC
|
||||
> configuration keeps working as described below.
|
||||
|
||||
`router_dynamic` is the dynamic model-selection strategy: instead of a fixed
|
||||
lineup, it picks proposers and the aggregator **per turn**, driven by
|
||||
SquillaRouter's tier decision for that turn. Enable it with
|
||||
`llm_ensemble.selection_mode = "router_dynamic"`.
|
||||
|
||||
Source: `src/opensquilla/provider/ensemble.py`
|
||||
(`_candidate_pool`, `_score_dynamic_candidate`, `_select_dynamic_candidate`,
|
||||
`_build_router_dynamic_members`).
|
||||
|
||||
## 2.1 Why dynamic selection
|
||||
|
||||
A fixed proposer/aggregator list can't adapt to the model actually chosen for a
|
||||
turn, and forces operators to hand-tune which models pair well together at each
|
||||
router tier. `router_dynamic` instead:
|
||||
|
||||
- reuses the model SquillaRouter already picked for the turn as the **anchor**
|
||||
proposer, so the ensemble never contradicts the router's own tier decision;
|
||||
- fills the remaining proposer slots and the aggregator slot by scoring a pool
|
||||
of candidate models against a per-tier "slot template";
|
||||
- penalizes re-selecting a model that's already in the ensemble, so proposers
|
||||
stay diverse instead of collapsing onto a few high-quality models.
|
||||
|
||||
## 2.2 Inputs
|
||||
|
||||
`_build_router_dynamic_members` takes three things:
|
||||
|
||||
1. **`inherited_provider_config`** — the provider/model SquillaRouter already
|
||||
resolved for this turn (becomes the anchor).
|
||||
2. **`turn_metadata`** — carries `routed_tier` (`c0`–`c3`), `routing_confidence`
|
||||
(0.0–1.0), and `routing_extra` (`final_tier`/`base_tier` fallbacks used if
|
||||
`routed_tier` is missing). Defaults to tier `c1` if nothing usable is found.
|
||||
3. **`config`** — `llm_ensemble.model_options` and `squilla_router.tiers`,
|
||||
used to build the candidate pool.
|
||||
|
||||
## 2.3 Candidate pool
|
||||
|
||||
`_candidate_pool` assembles a deduplicated list of `(provider, model)`
|
||||
candidates, in this order:
|
||||
|
||||
1. **Router anchor** — the inherited provider/model (`source="router_anchor"`).
|
||||
This is always `pool[0]` and always becomes the first proposer.
|
||||
2. **`llm_ensemble.model_options`** — the operator-configured candidate list
|
||||
(`source="model_options"`). If a model string contains `/` it's assumed to
|
||||
be an OpenRouter-style id and routed via `openrouter`; otherwise it inherits
|
||||
the anchor's provider.
|
||||
3. **`squilla_router.tiers[*].model`** — every model configured for a
|
||||
SquillaRouter tier (`source="router_tier:<tier>"`), so tier-specific models
|
||||
the operator has wired into the router are eligible even if not listed in
|
||||
`model_options`.
|
||||
|
||||
Each candidate is annotated with priors from `_DYNAMIC_MODEL_CATALOG` — a
|
||||
built-in table of ~14 known models with `tier`, `quality` (0–1), `cost_latency`
|
||||
(0–1, higher = cheaper/faster), `family`, `vendor`, and `architecture`. Models
|
||||
not in the catalog fall back to tier-average priors (`_tier_quality_prior`,
|
||||
`_tier_cost_latency_prior`) derived from the model string or tier hint.
|
||||
|
||||
## 2.4 Slot templates
|
||||
|
||||
Each router tier maps to an ordered list of proposer "slots"
|
||||
(`_DYNAMIC_TIER_SLOTS`):
|
||||
|
||||
| Tier | Slots |
|
||||
|------|-------|
|
||||
| `c0` | `anchor`, `cheap_contrast` |
|
||||
| `c1` | `anchor`, `balanced_contrast` |
|
||||
| `c2` | `anchor`, `adjacent_tier_check`, `orthogonal_family` |
|
||||
| `c3` | `anchor`, `strong_critic`, `orthogonal_family`, `fast_sanity` |
|
||||
|
||||
Lower tiers (cheap/simple turns) get a small, cost-biased ensemble; higher
|
||||
tiers (hard turns) get more proposers with slots biased toward quality and
|
||||
contrast. The `anchor` slot is always filled by the router's own model and is
|
||||
never scored — it's taken as-is.
|
||||
|
||||
Each tier also maps to an aggregator slot (`_DYNAMIC_AGGREGATOR_SLOT`):
|
||||
`c0→aggregator_fast`, `c1→aggregator_balanced`, `c2`/`c3→aggregator_strong`.
|
||||
|
||||
## 2.5 Scoring a candidate for a slot
|
||||
|
||||
For every non-anchor slot, every pool candidate is scored and the best one is
|
||||
selected (`_select_dynamic_candidate` → `_score_dynamic_candidate`):
|
||||
|
||||
```
|
||||
score = weights.quality * quality_prior
|
||||
+ weights.affinity * router_affinity_score
|
||||
+ weights.diversity * diversity_score
|
||||
+ weights.cost * cost_latency_prior
|
||||
+ weights.role * role_match_score(slot)
|
||||
- duplicate_penalty
|
||||
```
|
||||
|
||||
Each slot has its own weight vector (`_DYNAMIC_SLOT_WEIGHTS`), e.g.
|
||||
`cheap_contrast` weights `cost` and `role` heavily and `affinity` lightly,
|
||||
while `strong_critic` weights `quality` and `role` heavily and `cost` almost
|
||||
not at all.
|
||||
|
||||
### Score components
|
||||
|
||||
- **`router_affinity_score`** — how close the candidate's tier prior is to the
|
||||
turn's `routed_tier`, scaled by `routing_confidence`. Low router confidence
|
||||
relaxes tier matching instead of forcing a brittle lock, since a
|
||||
low-confidence route is itself uncertain about the right tier.
|
||||
- **`diversity_score`** — rewards a candidate whose family/vendor/provider/
|
||||
tier/architecture aren't already represented among the proposers picked so
|
||||
far in this turn (checked incrementally, slot by slot).
|
||||
- **`role_match_score`** — slot-specific logic (see below), combining tier
|
||||
targeting, contrast against the anchor, quality, or cost depending on what
|
||||
that slot is supposed to contribute.
|
||||
- **`duplicate_penalty`** — `_DYNAMIC_SELECTED_PENALTY[slot] * times_already_selected`.
|
||||
Selecting the same `(provider, model)` again is allowed but costs
|
||||
increasingly more as the same model keeps winning slots.
|
||||
|
||||
### Role match by slot
|
||||
|
||||
`_role_match_score` differs by slot — this is where each slot's intent is
|
||||
actually encoded:
|
||||
|
||||
- **`cheap_contrast`** — favors tier `c0`/`c1`, contrast with the anchor, and
|
||||
cost/latency. A cheap "second opinion."
|
||||
- **`balanced_contrast`** — favors tier `c1`/`c2`, contrast, and quality.
|
||||
- **`adjacent_tier_check`** — favors a tier one step above/below the routed
|
||||
tier (`adjacent_distance == 1`), plus quality. Checks whether a
|
||||
slightly-different-strength model agrees.
|
||||
- **`orthogonal_family`** — favors contrast and diversity above all — a
|
||||
model from a different vendor/family/architecture than the anchor.
|
||||
- **`strong_critic`** — favors tier `c3` and quality heavily — the strongest
|
||||
available model as a critic, used only at higher tiers.
|
||||
- **`fast_sanity`** — favors tier `c0`/`c1` and cost/latency — a fast,
|
||||
cheap sanity check, used only at `c3`.
|
||||
- **`aggregator_fast` / `aggregator_balanced` / `aggregator_strong`** — each
|
||||
balances tier targeting and quality differently; `aggregator_strong`
|
||||
weights quality highest and cost lowest, since the aggregator's output is
|
||||
the final response.
|
||||
|
||||
### Tie-breaking
|
||||
|
||||
Candidates are sorted by `(score, quality_prior, cost_latency_prior,
|
||||
-pool_index)` descending, so ties fall back to higher quality, then higher
|
||||
cost/latency score, then earlier pool position (closer to the anchor/operator-
|
||||
configured list) wins.
|
||||
|
||||
## 2.6 Selection order
|
||||
|
||||
`_build_router_dynamic_members` runs slots in the tier's template order:
|
||||
|
||||
1. `anchor` — taken directly, no scoring.
|
||||
2. Remaining proposer slots, in order — each selection is added to `selected`
|
||||
and `selected_counts` before the next slot is scored, so later slots see
|
||||
updated diversity/duplicate state.
|
||||
3. The aggregator slot, scored last, against the same accumulated `selected`
|
||||
state as the proposers (so it also gets a duplicate penalty if it repeats
|
||||
a proposer's model).
|
||||
|
||||
## 2.7 Output
|
||||
|
||||
The function returns `(profile_name, proposers, aggregator, selection_plan)`:
|
||||
|
||||
- `profile_name` — `"router_dynamic/<tier>"`, e.g. `"router_dynamic/c2"`.
|
||||
- `proposers` — one `EnsembleMemberConfig` per slot, labeled by slot name
|
||||
(`anchor`, `cheap_contrast`, ...).
|
||||
- `aggregator` — one `EnsembleMemberConfig`, labeled `aggregator`.
|
||||
- `selection_plan` — a full trace for observability, including the resolved
|
||||
tier/confidence, the anchor, the slot template, per-slot score breakdowns
|
||||
(`_score_trace`, including the top-3 scored candidates per slot for
|
||||
debugging near-misses), the aggregator's score breakdown, the full
|
||||
candidate pool, and `duplicate_policy: "selected_penalty"`.
|
||||
|
||||
`build_ensemble_provider_from_config` (the public entrypoint) additionally
|
||||
clamps `min_successful_proposers` down to `len(proposers)` if the configured
|
||||
value exceeds how many proposer slots the tier's template actually produced
|
||||
— e.g. configuring `min_successful_proposers=4` at tier `c0` (2 slots) yields
|
||||
an effective minimum of 2. Both the configured and effective values are
|
||||
recorded in `selection_plan` for debugging.
|
||||
|
||||
## 2.8 Configuration surface
|
||||
|
||||
```toml
|
||||
[llm_ensemble]
|
||||
enabled = true
|
||||
selection_mode = "router_dynamic"
|
||||
```
|
||||
|
||||
What operators can tune:
|
||||
|
||||
- `llm_ensemble.model_options` — extends the candidate pool beyond the router
|
||||
anchor and configured router tiers.
|
||||
- `llm_ensemble.min_successful_proposers` — desired minimum successful
|
||||
proposers (clamped per-turn as described above).
|
||||
- `squilla_router.tiers[*].model` — indirectly expands the candidate pool and
|
||||
determines which model becomes the anchor for a given tier.
|
||||
|
||||
There is no operator control over slot templates, weights, or the model
|
||||
catalog priors — those are fixed in code. Unlike the static families,
|
||||
`router_dynamic` keeps the legacy runtime defaults (timeouts 3600s,
|
||||
`shuffle_candidates=True`, `min_successful_proposers=1`).
|
||||
@@ -0,0 +1,395 @@
|
||||
# Attachment Drag-and-Drop Upload Spec
|
||||
|
||||
Date: 2026-06-29
|
||||
Status: Draft
|
||||
|
||||
## Problem
|
||||
|
||||
OpenSquilla should let users attach files by dragging them into the chat surface
|
||||
in both the browser Web UI and the Electron desktop client. The behavior must be
|
||||
consistent across surfaces and must use the existing gateway attachment
|
||||
ingestion path rather than adding a parallel desktop-only file pipeline.
|
||||
|
||||
The current code already contains most of the upload path:
|
||||
|
||||
- `opensquilla-webui/src/views/ChatView.vue` listens for drag/drop on the chat
|
||||
thread and passes dropped files to `addAttachment`.
|
||||
- `opensquilla-webui/src/composables/chat/useChatAttachments.ts` validates file
|
||||
type and size, inlines small files, and stages large files through
|
||||
`/api/v1/files/upload`.
|
||||
- `opensquilla-webui/src/composables/chat/useChatSend.ts` sends staged files as
|
||||
`{ type, file_uuid, mime, name }`.
|
||||
- `src/opensquilla/gateway/uploads.py` exposes the multipart upload route and
|
||||
stores staged bytes behind an opaque `file_uuid`.
|
||||
- `src/opensquilla/gateway/attachment_ingest.py` resolves `file_uuid` entries
|
||||
into transcript material references before a turn runs.
|
||||
- `desktop/electron/src/main.ts` creates a sandboxed, context-isolated browser
|
||||
window; `desktop/electron/src/preload.cts` exposes only explicit desktop APIs.
|
||||
|
||||
The missing product-quality layer is a complete, reviewed drag-upload UX and
|
||||
test contract that covers both browser and desktop surfaces, including auth,
|
||||
error states, retry behavior, and cross-platform desktop validation.
|
||||
|
||||
## Goals
|
||||
|
||||
- Support dragging files from the OS/browser file picker into the Web UI chat
|
||||
surface.
|
||||
- Support the same drag behavior in the Electron desktop client without a
|
||||
separate upload implementation.
|
||||
- Preserve the existing attachment policy: max count, per-category size caps,
|
||||
total size cap, MIME sniffing, and staged upload TTL. Any file type is
|
||||
admitted; the MIME set only routes representation (rendered families are
|
||||
extracted or inlined, everything else stages as an opaque workspace file).
|
||||
- Make upload progress and failure states visible in the composer.
|
||||
- Prevent sending while any attachment is still reading or uploading.
|
||||
- Ensure token-authenticated gateways can upload files by using the same bearer
|
||||
token source as the WebSocket/RPC client.
|
||||
- Keep raw local file paths out of renderer code and out of chat payloads.
|
||||
- Persist only stable transcript material references after `chat.send` accepts
|
||||
the turn; do not persist `file_uuid` in transcripts.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement folder drag-and-drop.
|
||||
- Do not upload directory trees or recursively enumerate local paths.
|
||||
- Do not expose arbitrary local filesystem reads through Electron preload.
|
||||
- Do not make Electron bypass the gateway upload endpoint.
|
||||
- Do not add resumable/chunked uploads in this iteration.
|
||||
- Do not add per-type client-side gating: admission is any-type (opaque
|
||||
staging), and rejections happen only on size/count policy, mirroring the
|
||||
gateway's category router in `contracts/attachments.py`.
|
||||
- Do not support remote URL drag/drop as file upload unless the browser supplies
|
||||
a real `File` object.
|
||||
|
||||
## Existing Surfaces
|
||||
|
||||
### Web UI
|
||||
|
||||
- `ChatView.vue` owns high-level chat surface events, including drag/drop and
|
||||
paste.
|
||||
- `ChatComposer.vue` renders pending attachment chips and the file input.
|
||||
- `useChatAttachments.ts` owns attachment state, MIME resolution, inline reads,
|
||||
staged uploads, and local validation.
|
||||
- `useChatSend.ts` serializes pending attachments into the `chat.send` RPC
|
||||
payload.
|
||||
|
||||
### Gateway
|
||||
|
||||
- `contracts/attachments.py` defines allowed media types and size limits.
|
||||
- `uploads.py` handles multipart staging and returns `file_uuid`.
|
||||
- `attachment_ingest.py` validates inline and staged attachments, writes
|
||||
transcript material, and returns consumed staged UUIDs.
|
||||
- `rpc_sessions.py` evicts consumed `file_uuid` values only after the turn has
|
||||
been accepted into the runtime.
|
||||
|
||||
### Desktop
|
||||
|
||||
- The Electron renderer runs the same Vue Web UI as the browser.
|
||||
- The Electron window is sandboxed and context-isolated.
|
||||
- The preload API currently exposes gateway status, settings, onboarding, and
|
||||
artifact open helpers; it does not expose raw file reads.
|
||||
|
||||
## Recommended Design
|
||||
|
||||
Use one shared Web UI upload path for browser and Electron:
|
||||
|
||||
```text
|
||||
User drops files
|
||||
-> ChatView drop handler
|
||||
-> useChatAttachments.addAttachments(files)
|
||||
-> inline small files OR POST large files to /api/v1/files/upload
|
||||
-> ChatComposer renders pending chips
|
||||
-> useChatSend serializes attachments
|
||||
-> chat.send RPC
|
||||
-> gateway attachment_ingest resolves file_uuid
|
||||
-> turn runtime receives stable attachment refs
|
||||
```
|
||||
|
||||
Electron should not get a special upload bridge in the first iteration. When a
|
||||
file is dragged from the OS into the Electron browser window, Chromium exposes
|
||||
it to renderer code as a `File`. That is the same object shape the browser path
|
||||
uses, so the renderer can call the same composable.
|
||||
|
||||
Add a desktop-specific bridge only if manual validation proves a platform cannot
|
||||
produce usable `DataTransfer.files`, or if a later feature explicitly needs
|
||||
native behavior such as folder selection, revealing local file paths, or
|
||||
watching local files for changes.
|
||||
|
||||
## User Experience
|
||||
|
||||
### Drop Zone
|
||||
|
||||
The primary drop zone is the chat thread/composer area.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- `dragenter` / `dragover` with at least one file item shows a visible drop
|
||||
affordance.
|
||||
- `dragleave` uses a drag-depth counter or equivalent containment check so the
|
||||
affordance does not flicker when moving across child elements.
|
||||
- `drop` extracts only `File` objects and ignores non-file drag data.
|
||||
- Dropping files focuses the composer and appends attachments to the pending
|
||||
list.
|
||||
- Unsupported items produce a toast and do not block valid files in the same
|
||||
drop.
|
||||
|
||||
### Attachment Chips
|
||||
|
||||
Each pending attachment chip should show:
|
||||
|
||||
- name;
|
||||
- MIME or concise type label;
|
||||
- size;
|
||||
- state: reading, uploading, ready, failed;
|
||||
- thumbnail for image inline attachments when available;
|
||||
- remove button;
|
||||
- retry button for staged upload failures.
|
||||
|
||||
Sending is disabled while any attachment is `reading` or `uploading`.
|
||||
|
||||
### Empty Message Behavior
|
||||
|
||||
If the user sends with attachments and no text, the existing fallback message
|
||||
`Describe these attachments` remains acceptable.
|
||||
|
||||
The visible bubble should still show the user's display text when present and
|
||||
attachment chips/previews when attachments exist.
|
||||
|
||||
## Attachment State Model
|
||||
|
||||
Use an explicit state machine in `useChatAttachments.ts`.
|
||||
|
||||
Recommended internal states:
|
||||
|
||||
| State | Meaning | Sendable |
|
||||
| --- | --- | --- |
|
||||
| `inline_pending` | FileReader is reading a small file. | No |
|
||||
| `inline` | Base64 data is ready in the pending payload. | Yes |
|
||||
| `uploading` | Multipart upload is in progress. | No |
|
||||
| `staged` | Gateway returned a `file_uuid`. | Yes |
|
||||
| `failed` | Read or upload failed and can be removed or retried. | No |
|
||||
|
||||
The public type may keep the existing names, but code paths should treat them as
|
||||
states rather than ad-hoc variants.
|
||||
|
||||
`addAttachment(file)` should be implemented in terms of
|
||||
`addAttachments(files: File[])` so file picker, paste, and drag/drop share batch
|
||||
validation behavior.
|
||||
|
||||
## Data Contracts
|
||||
|
||||
### Inline Attachment Payload
|
||||
|
||||
Small files are sent through `chat.send` as:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "image/png",
|
||||
"mime": "image/png",
|
||||
"name": "screenshot.png",
|
||||
"data": "<base64>"
|
||||
}
|
||||
```
|
||||
|
||||
### Staged Upload Request
|
||||
|
||||
Large files are uploaded first:
|
||||
|
||||
```http
|
||||
POST /api/v1/files/upload
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Multipart fields:
|
||||
|
||||
- `file`: the file bytes and filename;
|
||||
- `mime`: the client-resolved MIME, used only as a claim; gateway revalidates.
|
||||
|
||||
Expected success response:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_uuid": "u-...",
|
||||
"filename": "report.pdf",
|
||||
"mime": "application/pdf",
|
||||
"size": 12345
|
||||
}
|
||||
```
|
||||
|
||||
### Staged `chat.send` Payload
|
||||
|
||||
After staging, `chat.send` sends:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "application/pdf",
|
||||
"mime": "application/pdf",
|
||||
"name": "report.pdf",
|
||||
"file_uuid": "u-..."
|
||||
}
|
||||
```
|
||||
|
||||
The gateway resolves this into a stable attachment ref and must not persist
|
||||
`file_uuid` into transcript envelopes.
|
||||
|
||||
## Auth Requirements
|
||||
|
||||
The multipart upload route intentionally requires header-based auth in token
|
||||
mode. Query-string token auth must remain rejected for uploads.
|
||||
|
||||
Vue upload requests must therefore include the same token source used by the
|
||||
WebSocket/RPC connection:
|
||||
|
||||
- read `sessionStorage.getItem("opensquilla.wsToken")`;
|
||||
- when present, set `Authorization: Bearer <token>`;
|
||||
- keep `credentials: "same-origin"` for same-origin cookies and browser policy;
|
||||
- never place the token in the upload URL.
|
||||
|
||||
This is important because the current Vue staged upload path uses
|
||||
`credentials: "same-origin"` but does not include an authorization header. The
|
||||
legacy static chat upload path already includes the bearer token and should be
|
||||
used as the behavior reference.
|
||||
|
||||
## Security and Safety Rules
|
||||
|
||||
- Frontend validation is advisory UX only; gateway validation remains
|
||||
authoritative.
|
||||
- The renderer must never send local filesystem paths in chat payloads.
|
||||
- Electron preload must not expose an arbitrary `readFile(path)` API for this
|
||||
feature.
|
||||
- `file_uuid` is a short-lived upload-store identifier, not a durable reference.
|
||||
- Staged upload bytes are evicted only after the turn is accepted.
|
||||
- Failed `chat.send` after successful staging must keep the staged file retryable
|
||||
until TTL expiry.
|
||||
- MIME sniffing mismatches are handled by gateway policy, not by trusting the
|
||||
browser-provided `File.type`.
|
||||
- Directory entries and zero-byte files should be rejected with clear UX.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Frontend
|
||||
|
||||
1. Add `addAttachments(files: File[])` in `useChatAttachments.ts`.
|
||||
2. Route file picker, paste, and drop through `addAttachments`.
|
||||
3. Add drag-depth or containment-safe drop-zone state in `ChatView.vue`.
|
||||
4. Show a drop affordance only when the drag payload contains files.
|
||||
5. Add upload auth headers to staged upload requests.
|
||||
6. Add retry handling for staged upload failures.
|
||||
7. Keep `ChatComposer.vue` presentational: props down, events up.
|
||||
8. Ensure `useChatSend.ts` does not clear failed attachments as if they were
|
||||
successfully queued.
|
||||
|
||||
### Gateway
|
||||
|
||||
1. Keep `/api/v1/files/upload` as the only staged upload endpoint.
|
||||
2. Keep the upload route header-token requirement unchanged.
|
||||
3. Confirm `UploadStore` and `attachment_ingest` reject unsupported MIME, over
|
||||
size, unknown UUID, restart-lost UUID, and total size overflow.
|
||||
4. Add targeted tests only where the new frontend contract exposes gaps.
|
||||
|
||||
### Desktop
|
||||
|
||||
1. Validate drag/drop in packaged or dev Electron on macOS, Windows, and Linux
|
||||
where available.
|
||||
2. Keep Electron preload unchanged unless validation proves native bridging is
|
||||
required.
|
||||
3. If a bridge becomes required, expose only a narrow capability such as
|
||||
`stageDroppedFile(handle)` and keep all byte validation in the gateway.
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Frontend Unit Tests
|
||||
|
||||
- `resolveAttachmentMime()` prefers allowed browser MIME and falls back to
|
||||
extension.
|
||||
- Unknown UTF-8 text degrades to `text/plain`.
|
||||
- Unknown binary files are rejected.
|
||||
- Oversize files are rejected by per-MIME cap.
|
||||
- `addAttachments()` handles mixed valid and invalid files without dropping the
|
||||
valid ones.
|
||||
- Inline files transition from `inline_pending` to `inline`.
|
||||
- Large stageable files transition from `uploading` to `staged`.
|
||||
- Failed staged upload leaves a failed/removable state.
|
||||
- Upload requests include `Authorization: Bearer <token>` when the token exists.
|
||||
- Upload requests do not put the token in the URL.
|
||||
|
||||
### Vue Component Tests
|
||||
|
||||
- Dragging files over the chat surface shows the drop affordance.
|
||||
- Dragging non-file data does not show the drop affordance.
|
||||
- Dropping files calls the attachment composable once with all dropped files.
|
||||
- Sending is disabled while any attachment is reading or uploading.
|
||||
- Removing an attachment updates the pending list without mutating child props.
|
||||
|
||||
### Gateway Tests
|
||||
|
||||
- Multipart upload rejects missing auth header in token mode.
|
||||
- Multipart upload accepts bearer token in token mode.
|
||||
- Multipart upload rejects query-token-only auth.
|
||||
- Staged `file_uuid` resolution returns an attachment ref with no `file_uuid` or
|
||||
inline `data`.
|
||||
- Restart-lost UUID produces the re-upload error path.
|
||||
- Successful turn acceptance evicts consumed UUIDs.
|
||||
- Failed turn acceptance does not evict consumed UUIDs.
|
||||
|
||||
### Browser E2E
|
||||
|
||||
- Drop one small image into Web UI, send, and assert `chat.send` carries inline
|
||||
base64 attachment data.
|
||||
- Drop one large PDF into Web UI, wait for staged chip, send, and assert
|
||||
`chat.send` carries `file_uuid`.
|
||||
- Drop a valid file and an invalid file together; valid file remains pending and
|
||||
invalid file produces a toast.
|
||||
- Verify mobile and desktop layouts do not overflow with long filenames or wide
|
||||
image thumbnails.
|
||||
|
||||
### Desktop Manual Smoke
|
||||
|
||||
Run on Electron dev and packaged builds:
|
||||
|
||||
1. Start the desktop client.
|
||||
2. Drag a small PNG from the OS file manager into the chat thread.
|
||||
3. Confirm the composer shows a thumbnail chip.
|
||||
4. Send and confirm the user bubble renders the attachment.
|
||||
5. Drag a PDF larger than the inline threshold.
|
||||
6. Confirm upload progress changes to staged/ready.
|
||||
7. Send and confirm the gateway accepts the turn.
|
||||
8. Repeat with token auth enabled.
|
||||
9. Confirm no local file path appears in the chat payload, transcript, or logs.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Browser Web UI supports drag-and-drop upload for every MIME currently allowed
|
||||
by `contracts/attachments.py`.
|
||||
- Electron desktop supports the same drag-and-drop behavior without a separate
|
||||
upload code path.
|
||||
- Token-authenticated uploads succeed because the frontend sends bearer auth
|
||||
headers.
|
||||
- Query-token-only multipart upload remains rejected.
|
||||
- Users cannot send while attachments are still reading or uploading.
|
||||
- Failed uploads are visible and removable or retryable.
|
||||
- Staged attachments are sent as `file_uuid` and are materialized into stable
|
||||
transcript attachment refs before runtime execution.
|
||||
- `file_uuid` never appears in persisted transcript envelopes.
|
||||
- Successful turn acceptance evicts consumed staged uploads.
|
||||
- Frontend and backend tests cover valid, invalid, oversize, auth, retry, and
|
||||
layout cases.
|
||||
|
||||
## Rollout
|
||||
|
||||
- Ship behind the normal chat composer behavior with no user-facing setting.
|
||||
- Keep existing file input and paste upload behavior working during rollout.
|
||||
- Validate Web UI first, then Electron dev, then packaged desktop.
|
||||
- Add troubleshooting guidance only if desktop platform differences require
|
||||
user-visible explanation.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should failed staged upload chips offer retry, or should failure remove the
|
||||
chip and rely on toast only?
|
||||
- Should duplicate files in the same drop be deduplicated by
|
||||
`name + size + lastModified`, or should duplicates be allowed?
|
||||
- Should the drop affordance cover only the chat thread or the full chat view?
|
||||
- Should future folder drag support be handled through a separate desktop-only
|
||||
import flow rather than this attachment pipeline?
|
||||
@@ -0,0 +1,198 @@
|
||||
# Bocha Search Provider Design
|
||||
|
||||
Date: 2026-06-25
|
||||
|
||||
## Goal
|
||||
|
||||
Add Bocha as a first-class OpenSquilla search provider using the existing search
|
||||
runtime, provider catalog, onboarding, settings, diagnostics, and test surfaces.
|
||||
The user experience should be: configure a Bocha key, then normal `web_search`
|
||||
and `web_discover` flows can use it automatically when it is the best available
|
||||
provider.
|
||||
|
||||
This design intentionally avoids a visible China-region strategy or profile. Bocha
|
||||
is a normal provider with capabilities, credentials, ordering, and diagnostics.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not add a visible `cn`, `domestic`, or region routing profile.
|
||||
- Do not add Zhipu, a separate reader abstraction, or multi-provider result fusion.
|
||||
- Do not change `search_provider` into a hard routing promise. It remains the
|
||||
credential anchor for `search_api_key` and `search_api_key_env`.
|
||||
- Do not add user-facing provider priority controls in the MVP.
|
||||
- Do not broaden fallback to empty-result or low-quality-result retries by default.
|
||||
|
||||
## Existing Runtime Boundaries
|
||||
|
||||
OpenSquilla already has the right extension points:
|
||||
|
||||
- Provider specs and provider factories live in `opensquilla.search.registry`.
|
||||
- Runtime availability and provider ordering live in `SearchRuntimeConfig` and
|
||||
`ResolvedSearchRuntime`.
|
||||
- `web_search` uses the canonical search pipeline.
|
||||
- `web_discover` has a lighter provider path and must be updated separately.
|
||||
- Onboarding and settings consume the provider catalog payload.
|
||||
- `search_fallback_policy` currently supports `off` and `network`.
|
||||
|
||||
The implementation should use those surfaces instead of introducing a second
|
||||
search router.
|
||||
|
||||
## Provider Behavior
|
||||
|
||||
Add `BochaSearchProvider` under `src/opensquilla/search/providers/bocha.py`.
|
||||
|
||||
Expected API shape:
|
||||
|
||||
- Endpoint: `https://api.bochaai.com/v1/web-search`
|
||||
- Authentication: `Authorization: Bearer <api key>`
|
||||
- Default environment variable: `BOCHA_SEARCH_API_KEY`
|
||||
- Request options:
|
||||
- query text
|
||||
- max result count
|
||||
- freshness mapping when `SearchOptions.recency` is set
|
||||
- summary enabled when supported
|
||||
|
||||
The provider should normalize Bocha results into `SearchResult` fields:
|
||||
|
||||
- title from `name`
|
||||
- url from `url`
|
||||
- snippet from `snippet`
|
||||
- provider content from `summary` when present
|
||||
- published timestamp from `datePublished`
|
||||
- source/site metadata from `siteName`, `displayUrl`, or equivalent fields
|
||||
|
||||
Bocha summaries should count as useful provider content so the canonical pipeline
|
||||
does not fetch pages unnecessarily when Bocha already returned enough source
|
||||
text.
|
||||
|
||||
## Provider Spec
|
||||
|
||||
Register Bocha with a `SearchProviderSpec`:
|
||||
|
||||
```python
|
||||
SearchProviderSpec(
|
||||
provider_id="bocha",
|
||||
requires_api_key=True,
|
||||
env_key="BOCHA_SEARCH_API_KEY",
|
||||
capabilities=frozenset({"web", "freshness", "content"}),
|
||||
)
|
||||
```
|
||||
|
||||
Do not claim `domain_filter` unless the Bocha API supports an equivalent feature
|
||||
and tests cover it.
|
||||
|
||||
## Runtime Ordering
|
||||
|
||||
Bocha should participate in the existing automatic ordering. Suggested MVP
|
||||
ordering:
|
||||
|
||||
```python
|
||||
_GENERAL_TIE_BREAKER = ("bocha", "tavily", "brave", "exa", "duckduckgo")
|
||||
_TECHNICAL_TIE_BREAKER = ("exa", "bocha", "brave", "tavily", "duckduckgo")
|
||||
_FRESHNESS_TIE_BREAKER = ("bocha", "tavily", "brave", "exa")
|
||||
```
|
||||
|
||||
Rationale:
|
||||
|
||||
- General and freshness searches should prefer Bocha when it is configured,
|
||||
because it is the targeted improvement for reliable China-accessible search.
|
||||
- Technical searches should keep Exa first because its existing role is stronger
|
||||
for semantic and content-oriented research.
|
||||
- No-key and partial-key users are protected by the existing availability filter:
|
||||
unavailable keyed providers are skipped before ordering is returned.
|
||||
|
||||
## Fallback Policy
|
||||
|
||||
Keep fallback semantics narrow:
|
||||
|
||||
- `off`: surface the original provider error.
|
||||
- `network`: fallback only after network, timeout, rate-limit, retryable, or HTTP
|
||||
provider errors.
|
||||
|
||||
Do not fallback on:
|
||||
|
||||
- empty results
|
||||
- low-quality results
|
||||
- parse errors, unless those are explicitly classified as retryable provider
|
||||
failures later
|
||||
|
||||
This preserves cost, latency, and predictability for users who configured
|
||||
multiple paid providers.
|
||||
|
||||
## Configuration And Setup
|
||||
|
||||
Supported key resolution should remain layered:
|
||||
|
||||
1. active provider inline configured key
|
||||
2. active provider configured env var
|
||||
3. provider spec default env var
|
||||
|
||||
Bocha should support all existing search setup paths:
|
||||
|
||||
- CLI configuration
|
||||
- onboarding setup engine
|
||||
- gateway setup payload
|
||||
- desktop settings provider catalog
|
||||
- web UI setup provider catalog
|
||||
|
||||
The MVP should update fallback/static provider lists where the UI has offline
|
||||
defaults, but the canonical source of truth remains the backend provider catalog.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Search runtime status should show Bocha like other providers:
|
||||
|
||||
- available/unavailable
|
||||
- credential source
|
||||
- credential configured boolean
|
||||
- skipped reason
|
||||
- capabilities
|
||||
|
||||
Diagnostics must not expose the raw API key.
|
||||
|
||||
A live provider probe can be added later, but it is not required for the MVP if
|
||||
existing status remains buildability/configuration based.
|
||||
|
||||
## Testing
|
||||
|
||||
Required tests:
|
||||
|
||||
- Provider response normalization from a representative Bocha payload.
|
||||
- Missing-key behavior and credential source resolution for Bocha.
|
||||
- Runtime ordering for:
|
||||
- no keyed providers
|
||||
- only Bocha configured
|
||||
- Bocha plus existing keyed providers
|
||||
- technical mode
|
||||
- freshness/news mode
|
||||
- `web_search` accepts `provider="bocha"` and `provider="auto"` can select Bocha.
|
||||
- `web_discover` accepts and can build Bocha when configured.
|
||||
- Onboarding catalog includes Bocha with `BOCHA_SEARCH_API_KEY`.
|
||||
- Documentation and frontend catalog contract tests include Bocha.
|
||||
|
||||
Optional live tests:
|
||||
|
||||
- A manually gated Bocha smoke test using `BOCHA_SEARCH_API_KEY`.
|
||||
- The live test must be opt-in and must not run in normal CI.
|
||||
|
||||
## Documentation
|
||||
|
||||
Update:
|
||||
|
||||
- `docs/search.md`
|
||||
- `docs/configuration.md`
|
||||
- relevant onboarding or setup docs if provider lists are repeated there
|
||||
|
||||
Document Bocha as a normal runtime-supported provider. Avoid describing it as a
|
||||
region strategy.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A user with only `BOCHA_SEARCH_API_KEY` configured can run normal automatic web
|
||||
search successfully.
|
||||
- Existing users with only Brave, Tavily, Exa, or DuckDuckGo keep the same
|
||||
behavior except for Bocha appearing in provider catalogs when available.
|
||||
- All current fallback semantics remain unchanged.
|
||||
- Bocha appears in CLI/onboarding/settings provider lists.
|
||||
- Tests cover provider mapping, ordering, configuration, and documentation
|
||||
contracts.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Compaction and Cache Continuity
|
||||
|
||||
Long agent sessions need context management. OpenSquilla uses compaction,
|
||||
bounded history, tool-result projection, and cache-aware prompt placement to
|
||||
keep long-running tasks moving.
|
||||
|
||||
Compaction is separate from memory. Memory is durable recall. Compaction is an
|
||||
active-session continuity tool.
|
||||
|
||||
## What Compaction Does
|
||||
|
||||
When session history approaches the configured context budget, OpenSquilla can
|
||||
compact older transcript entries into a durable summary and keep the recent
|
||||
tail active.
|
||||
|
||||
The goal is to preserve:
|
||||
|
||||
- user goal;
|
||||
- current status;
|
||||
- open steps;
|
||||
- changed files and artifacts;
|
||||
- known failures;
|
||||
- important tool results;
|
||||
- next action.
|
||||
|
||||
Compaction is not a guarantee that every old word remains model-visible. Export
|
||||
sessions or save files when exact historical text matters.
|
||||
|
||||
## User-Visible Lifecycle
|
||||
|
||||
Depending on surface and trigger, users may see:
|
||||
|
||||
- compaction started;
|
||||
- compaction skipped;
|
||||
- compaction completed;
|
||||
- compaction failed.
|
||||
|
||||
When no compaction is needed, OpenSquilla uses this stable message:
|
||||
|
||||
```text
|
||||
Already within context budget; no compact was applied
|
||||
```
|
||||
|
||||
That message is a no-op, not a failure.
|
||||
|
||||
## When to Compact Manually
|
||||
|
||||
Manual compaction is useful when:
|
||||
|
||||
- the session is long and you are about to start a new phase;
|
||||
- a previous tool-heavy turn produced a lot of context;
|
||||
- the UI indicates context pressure;
|
||||
- you want the next answer to focus on the current state rather than the whole
|
||||
transcript.
|
||||
|
||||
Avoid compact loops when the runtime says the session is already within budget.
|
||||
|
||||
## Passive Compaction
|
||||
|
||||
Passive compaction can happen when OpenSquilla detects context pressure before
|
||||
or during agent work. The exact trigger depends on model context limits,
|
||||
configured budgets, current history, and tool output size.
|
||||
|
||||
If passive compaction fails, the safest user response is usually:
|
||||
|
||||
1. let the current turn finish or fail cleanly;
|
||||
2. export the session if exact history matters;
|
||||
3. retry with a narrower request or manually save key artifacts;
|
||||
4. enable diagnostics if the failure repeats.
|
||||
|
||||
## Prompt Cache Continuity
|
||||
|
||||
Prompt caching works best when stable prompt parts stay stable. OpenSquilla
|
||||
tries to keep:
|
||||
|
||||
- stable system prompt and tool definitions early;
|
||||
- current request, volatile runtime context, retrieved history, and tool results
|
||||
near the tail;
|
||||
- model/provider switches visible through diagnostics when they may affect
|
||||
cache continuity.
|
||||
|
||||
Cache continuity is best-effort. Routing, tools, attachments, provider changes,
|
||||
or a large new context can reduce cache reuse.
|
||||
|
||||
## Related Commands and Surfaces
|
||||
|
||||
Manual compaction is primarily surfaced in chat and Web UI flows. For
|
||||
inspection and recovery:
|
||||
|
||||
```sh
|
||||
opensquilla sessions show <session-key>
|
||||
opensquilla sessions export <session-key>
|
||||
opensquilla diagnostics on
|
||||
```
|
||||
|
||||
For memory repair surfaces related to degraded compaction records:
|
||||
|
||||
```sh
|
||||
opensquilla memory repair list
|
||||
opensquilla memory repair show --summary-id <id>
|
||||
opensquilla memory raw-fallbacks list
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep important final artifacts in files or published artifacts.
|
||||
- Use memory for durable preferences and reusable project facts.
|
||||
- Use session export for exact old transcripts.
|
||||
- Use manual compaction before a new phase in a very long session.
|
||||
- Do not repeatedly compact a short or already-within-budget session.
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,207 @@
|
||||
# Desktop DMG Startup Window Recovery Spec
|
||||
|
||||
## Background
|
||||
|
||||
Users reported two macOS DMG installation issues:
|
||||
|
||||
1. After installing the latest DMG, the first launch shows no window.
|
||||
2. In an earlier DMG, closing the client window with the red close button leaves the OpenSquilla app running in the Dock, but clicking the Dock icon does not reopen any window.
|
||||
|
||||
This spec covers the desktop startup and packaging fix. It is separate from the gateway/yoyo migration lock recovery spec, although both can surface during first-run gateway startup.
|
||||
|
||||
## Evidence
|
||||
|
||||
Local packaged logs showed the bundled gateway crashing during first launch:
|
||||
|
||||
```text
|
||||
AttributeError: '_AsyncConnection' object has no attribute 'create_function'
|
||||
```
|
||||
|
||||
The crash path is:
|
||||
|
||||
```text
|
||||
opensquilla/session/storage.py -> SessionStorage.connect()
|
||||
```
|
||||
|
||||
The packaged runtime under the built app was missing `create_function` in `opensquilla.compat.aiosqlite._AsyncConnection`, while current source has the compatibility method.
|
||||
|
||||
The packaged Electron app also still had this startup order:
|
||||
|
||||
```ts
|
||||
const gateway = await startGateway()
|
||||
const window = await createMainWindow()
|
||||
```
|
||||
|
||||
That means any gateway crash or long wait can prevent the first visible desktop window from being created.
|
||||
|
||||
For the Dock behavior, macOS intentionally keeps the application process alive after all windows are closed. The existing behavior is acceptable only if `activate` reliably recreates or focuses the main window and reuses the already-running gateway instead of trying to perform a full duplicate startup.
|
||||
|
||||
## Goals
|
||||
|
||||
- Show a desktop window immediately on launch and Dock activation, before gateway startup can block or fail.
|
||||
- Surface gateway startup failure inside the desktop UI instead of leaving the user with no visible window.
|
||||
- Ensure the packaged gateway runtime contains the aiosqlite compatibility API needed by session storage.
|
||||
- Reuse an already-owned, healthy gateway on Dock activation instead of spawning a duplicate process.
|
||||
- Prevent duplicate gateway startup while a startup attempt is already in progress.
|
||||
- Make the release validation catch stale packaged runtime contents before distributing a DMG.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not change macOS close-button semantics by default. Closing the last window may keep the app running unless product decides to switch to quit-on-close.
|
||||
- Do not disable yoyo migration locking.
|
||||
- Do not change gateway state directory, auth token, or persistence layout.
|
||||
- Do not treat a previously built DMG as fixed unless it is rebuilt from the fixed source and revalidated.
|
||||
- Do not treat unsigned local validation as equivalent to a distributable release validation.
|
||||
|
||||
## Root Causes
|
||||
|
||||
### 1. Packaged Gateway Runtime Was Stale
|
||||
|
||||
The built app included a runtime copy of `opensquilla.compat.aiosqlite` without `_AsyncConnection.create_function`. Source had moved forward, but the packaged runtime did not contain that fix.
|
||||
|
||||
When session storage calls `create_function`, the packaged gateway crashes before becoming healthy.
|
||||
|
||||
### 2. Electron Created the Window After Gateway Startup
|
||||
|
||||
The app waited for `startGateway()` before calling `createMainWindow()`. If gateway startup crashes, hangs, or waits on a lock, the desktop app can remain running without any visible window.
|
||||
|
||||
### 3. macOS Activation Path Did Not Reliably Resume UI
|
||||
|
||||
On macOS, `window-all-closed` does not quit the app. After the red close button, the Dock dot remains because the app process and gateway may still be alive.
|
||||
|
||||
Clicking the Dock icon must focus an existing window or create a new one and attach it to the existing gateway state. If activation re-enters a full startup path, it can wait unnecessarily, hit duplicate startup guards, or fail silently.
|
||||
|
||||
## Design
|
||||
|
||||
### Electron Startup
|
||||
|
||||
`bootDesktopApp()` must create the main window before gateway startup:
|
||||
|
||||
```ts
|
||||
const window = await createMainWindow()
|
||||
const gateway = await startGateway()
|
||||
```
|
||||
|
||||
The initial window should display a boot/loading state. If gateway startup succeeds, it loads the gateway URL. If gateway startup fails, it renders the existing startup error state with log details and retry actions.
|
||||
|
||||
### Idempotent Gateway Startup
|
||||
|
||||
`startGateway()` should become explicitly idempotent:
|
||||
|
||||
- If an owned gateway process is already healthy, return the existing `gatewayState`.
|
||||
- If startup is already in progress, do not spawn another gateway. Focus or keep the boot window visible.
|
||||
- If the owned process exited, clear stale owned state before starting a new process.
|
||||
- If a gateway URL is present, health-check it before deciding to reuse it.
|
||||
|
||||
This prevents Dock activation, second-instance activation, and retry actions from racing into duplicate gateway processes.
|
||||
|
||||
### Dock Activation
|
||||
|
||||
The `activate` handler should call a single helper that:
|
||||
|
||||
1. Focuses an existing non-destroyed main window, or creates a new boot window.
|
||||
2. If `gatewayState.status === "ready"` and the URL passes health check, loads that URL.
|
||||
3. If startup is in progress, keeps the boot window visible.
|
||||
4. Otherwise starts gateway once and then loads or shows an error state.
|
||||
|
||||
The same helper should be used by `second-instance` handling so a second launch focuses the original instance and does not start another gateway against the same state directory.
|
||||
|
||||
### Window Close Semantics
|
||||
|
||||
The macOS red close button can keep the process alive, but the implementation must clear stale window references on `closed` and make activation recreate the window.
|
||||
|
||||
If product later chooses quit-on-close, that should be a separate product decision. It changes expected macOS app behavior and gateway lifetime, so it should not be bundled into this bug fix.
|
||||
|
||||
### Gateway Runtime Compatibility
|
||||
|
||||
The compatibility layer must provide `create_function` on both the protocol and wrapper implementation:
|
||||
|
||||
```py
|
||||
class Connection(Protocol):
|
||||
async def create_function(...)
|
||||
|
||||
class _AsyncConnection:
|
||||
async def create_function(...)
|
||||
```
|
||||
|
||||
The wrapper should delegate to the underlying sqlite connection in the same thread-safe style as the other compatibility methods.
|
||||
|
||||
### Packaging Guard
|
||||
|
||||
Before building a distributable DMG:
|
||||
|
||||
1. Rebuild the gateway runtime from current source.
|
||||
2. Fail the release if `desktop/electron/runtime/gateway` is empty or stale.
|
||||
3. Build the Electron app from current source.
|
||||
4. Inspect the packaged runtime and assert `create_function` exists.
|
||||
5. Inspect packaged `app.asar` and assert the main window is created before gateway startup.
|
||||
6. Sign, notarize, staple, and validate the final DMG.
|
||||
|
||||
The previously generated DMG in `dist/desktop-electron` should be treated as stale for this bug because it was built before the latest desktop/runtime fixes.
|
||||
|
||||
### Unsigned Functional Testing
|
||||
|
||||
It is acceptable to build an unsigned or ad-hoc-signed DMG first for fast local functional testing. This phase should verify application behavior only:
|
||||
|
||||
- first launch shows a window immediately;
|
||||
- advanced configuration can start the gateway;
|
||||
- closing the window with the red close button and clicking the Dock icon reopens the UI;
|
||||
- no duplicate gateway process is created;
|
||||
- logs do not contain the `create_function` crash or duplicate lock failures.
|
||||
|
||||
Code signing and notarization do not rewrite Electron or Python business logic, so they should not change the intended desktop/gateway behavior. However, they can change the effective macOS runtime environment through Gatekeeper, quarantine handling, Hardened Runtime, entitlements, nested executable signing, dynamic library loading, and child process launch policy.
|
||||
|
||||
Therefore, unsigned validation is only a pre-release smoke test. The final artifact sent to external users must still be rebuilt, signed, notarized, stapled, and validated end to end.
|
||||
|
||||
The final release must not sign only the outer `.dmg`. The `.app` bundle and nested executables must be signed before creating/signing/notarizing the DMG.
|
||||
|
||||
## Validation Plan
|
||||
|
||||
### Automated
|
||||
|
||||
- Run gateway compatibility tests covering `_AsyncConnection.create_function`.
|
||||
- Run gateway startup/lock tests to ensure duplicate startup and stale yoyo lock handling still work.
|
||||
- Add or keep an Electron-side regression check for startup order, ideally asserting `createMainWindow()` runs before `startGateway()` in the desktop boot path.
|
||||
- Add an Electron-side test or factored helper test for activation reuse:
|
||||
- ready gateway state is reused;
|
||||
- startup-in-progress does not spawn a second gateway;
|
||||
- exited owned process is cleared before restart.
|
||||
|
||||
### Manual DMG Smoke Test
|
||||
|
||||
For the first local pass, this test may use an unsigned or ad-hoc-signed DMG. For external distribution, repeat the same test with a freshly built, signed, notarized, and stapled DMG.
|
||||
|
||||
1. Remove or move aside existing user data for a clean first-run test.
|
||||
2. Install the app from the DMG.
|
||||
3. Launch from `/Applications`; a window must appear immediately.
|
||||
4. Complete or enter advanced configuration; the gateway should start and the UI should load.
|
||||
5. Close the window with the red close button.
|
||||
6. Confirm the Dock dot remains.
|
||||
7. Click the Dock icon; the window must reopen and load the existing gateway UI.
|
||||
8. Confirm only one `opensquilla-gateway` process is running.
|
||||
9. Confirm logs do not contain the `create_function` AttributeError.
|
||||
10. Confirm logs do not show duplicate gateway state-dir lock failures during Dock activation.
|
||||
|
||||
### Release Validation
|
||||
|
||||
Release validation applies only to the final signed and notarized artifact.
|
||||
|
||||
Run these checks on the final artifact:
|
||||
|
||||
```bash
|
||||
spctl --assess --type open --context context:primary-signature -v dist/desktop-electron/OpenSquilla-*.dmg
|
||||
spctl --assess --type execute -v dist/desktop-electron/mac-arm64/OpenSquilla.app
|
||||
xcrun stapler validate dist/desktop-electron/OpenSquilla-*.dmg
|
||||
hdiutil verify dist/desktop-electron/OpenSquilla-*.dmg
|
||||
```
|
||||
|
||||
Also mount the DMG and inspect the installation window layout.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- First launch from a clean DMG install always shows a desktop window before gateway health checks finish.
|
||||
- Gateway startup failures are visible in the app window with actionable error UI.
|
||||
- Packaged gateway runtime contains `_AsyncConnection.create_function`.
|
||||
- Dock activation after red-window-close reopens the UI and reuses the existing healthy gateway.
|
||||
- Second launch/focus behavior does not spawn a duplicate gateway process.
|
||||
- Final DMG is signed, notarized, stapled, and passes Gatekeeper validation.
|
||||
@@ -0,0 +1,219 @@
|
||||
# Gateway Startup Locking Spec
|
||||
|
||||
Date: 2026-06-27
|
||||
Status: Draft
|
||||
|
||||
## Problem
|
||||
|
||||
Desktop first-run startup can leave the bundled gateway unable to boot when the
|
||||
initial process is interrupted or a second gateway process is started while the
|
||||
first process is still in schema migration.
|
||||
|
||||
The observed failure pattern is:
|
||||
|
||||
- A fresh desktop state starts successfully enough to seed the agent workspace.
|
||||
- The first bundled gateway process does not reach `gateway.started`.
|
||||
- Later gateway processes fail in `apply_pending()` with yoyo `LockTimeout`.
|
||||
- The yoyo error reports the same earlier process id for every retry.
|
||||
|
||||
The direct failure is a stale or active row in the yoyo migration lock table for
|
||||
the desktop `sessions.db`. Once this state exists, repeated desktop retries do
|
||||
not recover by themselves.
|
||||
|
||||
## Goals
|
||||
|
||||
- Prevent the Electron shell from launching duplicate gateway processes for the
|
||||
same desktop profile.
|
||||
- Keep the gateway pid lock alive for the full server lifetime and release it
|
||||
on graceful shutdown.
|
||||
- Recover from a stale yoyo migration lock only when the recorded pid is proven
|
||||
dead.
|
||||
- Preserve yoyo's safety guarantee when another process may still be migrating.
|
||||
- Surface actionable startup errors instead of an opaque PyInstaller traceback.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not disable yoyo migration locking.
|
||||
- Do not blindly run `break-lock` on every migration lock timeout.
|
||||
- Do not change the session database schema as part of this fix.
|
||||
- Do not make the desktop gateway share a process with Electron.
|
||||
- Do not broaden gateway binding or auth behavior.
|
||||
|
||||
## Existing Surfaces
|
||||
|
||||
- Electron launches the bundled gateway in `desktop/electron/src/main.ts` via
|
||||
`spawn(...)`.
|
||||
- Electron waits up to 45 seconds for `/healthz` before reporting that the
|
||||
gateway did not become healthy.
|
||||
- The gateway acquires `GatewayPidLock` before `build_services()` and before
|
||||
session database migration.
|
||||
- `build_services()` runs `apply_pending(session_db_path, migrations_dir)` before
|
||||
opening `SessionStorage`.
|
||||
- `apply_pending()` delegates locking to `with backend.lock():`.
|
||||
- yoyo implements the migration lock as a row in `yoyo_lock`, keyed by pid, and
|
||||
removes it in a `finally` block. A hard process exit can leave the row behind.
|
||||
|
||||
## Design
|
||||
|
||||
Implement three defensive layers. Each layer addresses a different failure mode
|
||||
and should be independently testable.
|
||||
|
||||
### Layer 1: Electron Single Instance Guard
|
||||
|
||||
Add `app.requestSingleInstanceLock()` near Electron app startup, before any
|
||||
gateway startup work can run.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- If the lock cannot be acquired, the new Electron process exits immediately.
|
||||
- On `second-instance`, the existing main window is restored and focused.
|
||||
- `bootDesktopApp()` must not run in the second Electron process.
|
||||
- The existing process remains the only owner allowed to spawn a gateway.
|
||||
|
||||
This reduces duplicate gateway starts caused by double-clicking the app,
|
||||
reopening from Finder, or launching from a DMG while a first run is still in
|
||||
progress.
|
||||
|
||||
### Layer 2: Gateway PID Lock Lifetime
|
||||
|
||||
Store the acquired `GatewayPidLock` on the returned `GatewayServer` object and
|
||||
release it in `GatewayServer.close()`.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- `start_gateway_server()` acquires the lock before database migration, as it
|
||||
does today.
|
||||
- The lock object remains strongly referenced for the whole server lifetime.
|
||||
- `GatewayServer.close()` calls `release()` exactly once, after shutdown work is
|
||||
complete enough that a new gateway may safely start.
|
||||
- `release()` remains idempotent.
|
||||
- Existing `atexit` and signal cleanup remain as best-effort fallback paths.
|
||||
|
||||
This makes normal gateway shutdown explicit instead of relying only on process
|
||||
exit behavior.
|
||||
|
||||
### Layer 3: Conservative Yoyo Stale Lock Recovery
|
||||
|
||||
Wrap yoyo `LockTimeout` handling inside `apply_pending()`.
|
||||
|
||||
When yoyo reports a lock timeout:
|
||||
|
||||
1. Inspect the lock table for recorded pids.
|
||||
2. If any recorded pid is alive, fail without clearing the lock.
|
||||
3. If every recorded pid is dead or invalid, delete the yoyo lock rows.
|
||||
4. Retry migration once.
|
||||
5. If the retry fails, surface the second failure without another recovery loop.
|
||||
|
||||
The liveness check should use platform-appropriate process probing:
|
||||
|
||||
- POSIX: `os.kill(pid, 0)`.
|
||||
- Windows: `OpenProcess` or equivalent existing helper behavior.
|
||||
|
||||
The lock recovery path must log structured events:
|
||||
|
||||
- `migrator.lock_timeout`
|
||||
- `migrator.lock_held_by_live_process`
|
||||
- `migrator.stale_lock_cleared`
|
||||
- `migrator.stale_lock_retry_failed`
|
||||
|
||||
The operator-facing error should explain whether the gateway is still starting,
|
||||
another gateway is running, or a stale migration lock could not be recovered.
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Never clear a yoyo lock held by a live pid.
|
||||
- Never clear the lock if the database cannot be inspected safely.
|
||||
- Never retry migration more than once after clearing a stale lock.
|
||||
- Keep migration failure loud if schema state is uncertain.
|
||||
- Prefer false negatives over false positives: failing startup is safer than
|
||||
corrupting a migration.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Electron:
|
||||
|
||||
- Add single-instance handling in `desktop/electron/src/main.ts`.
|
||||
- Keep the current `startupInProgress` guard for the existing process.
|
||||
- On a second instance, restore and focus `mainWindow` if it exists.
|
||||
|
||||
Gateway:
|
||||
|
||||
- Extend `GatewayServer` with an optional pid lock field.
|
||||
- Assign the acquired lock after `GatewayPidLock.acquire()` succeeds.
|
||||
- Release the lock in `GatewayServer.close()` in a `finally`-safe path.
|
||||
|
||||
Migrator:
|
||||
|
||||
- Import yoyo `LockTimeout` explicitly.
|
||||
- Add a small helper to query `yoyo_lock` rows through the yoyo backend or a
|
||||
separate SQLite connection to the same local database.
|
||||
- Add a small helper to clear the yoyo lock table.
|
||||
- Keep `:memory:` behavior unchanged.
|
||||
- Keep normal no-pending migration behavior unchanged.
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `GatewayPidLock` rejects a second lock acquisition for the same state dir while
|
||||
the first lock is held.
|
||||
- `GatewayServer.close()` releases the stored gateway pid lock.
|
||||
- `apply_pending()` does not clear a yoyo lock for a live pid.
|
||||
- `apply_pending()` clears a yoyo lock for a dead pid and retries once.
|
||||
- `apply_pending()` does not enter an unbounded retry loop after stale-lock
|
||||
recovery fails.
|
||||
- `apply_pending()` preserves normal migration success and no-op behavior.
|
||||
|
||||
### Electron Tests
|
||||
|
||||
- A second Electron instance does not call `bootDesktopApp()`.
|
||||
- A second Electron instance focuses the existing window.
|
||||
- Desktop retry does not spawn a new gateway while startup is already in
|
||||
progress.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Create a temporary desktop state with `sessions.db` containing a stale
|
||||
`yoyo_lock` row. Gateway startup clears it and completes migration.
|
||||
- Create a temporary desktop state with a live helper process recorded in
|
||||
`yoyo_lock`. Gateway startup fails with a clear, non-destructive error.
|
||||
- Start two gateway processes against the same state dir. The second process
|
||||
fails before database migration.
|
||||
|
||||
### Manual DMG Smoke
|
||||
|
||||
- Install the DMG into `/Applications`.
|
||||
- Start from a clean desktop user data directory.
|
||||
- Double-click the app repeatedly during first-run startup.
|
||||
- Confirm only one gateway process is launched.
|
||||
- Confirm the app becomes ready or surfaces a clear single startup error.
|
||||
- Force quit during first-run migration, relaunch, and confirm stale-lock
|
||||
recovery works when the recorded pid is no longer alive.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Fresh DMG first run cannot spawn two owned gateway processes from two Electron
|
||||
instances.
|
||||
- A stale yoyo migration lock whose pid is dead is cleared automatically and the
|
||||
gateway starts.
|
||||
- A yoyo migration lock whose pid is alive is never cleared automatically.
|
||||
- A second gateway using the same desktop state fails before migration work.
|
||||
- Graceful gateway shutdown removes `gateway.pid` and releases
|
||||
`gateway.pid.lock`.
|
||||
- Failure messages distinguish active startup, already-running gateway, and
|
||||
unrecoverable migration lock states.
|
||||
- CI covers stale-lock recovery and live-lock non-recovery.
|
||||
|
||||
## Rollout
|
||||
|
||||
- Ship behind normal startup behavior, with no user-facing setting.
|
||||
- Keep structured logs in the desktop gateway log for post-incident diagnosis.
|
||||
- Add a short troubleshooting entry once the behavior is implemented.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether yoyo lock inspection should use yoyo backend APIs only or direct
|
||||
SQLite for local file databases.
|
||||
- Whether desktop startup should extend the first-run health timeout after it
|
||||
detects schema migration is active.
|
||||
- Whether the boot splash should show a distinct "Preparing database" phase.
|
||||
@@ -0,0 +1,129 @@
|
||||
# Memory
|
||||
|
||||
OpenSquilla memory helps the agent recall durable context without replaying
|
||||
every old conversation. Use it for stable preferences, reusable project facts,
|
||||
previous decisions, and notes that should survive across sessions.
|
||||
|
||||
Memory is separate from skills. Skills teach the agent how to do a task; memory
|
||||
stores useful facts and context the agent may need later.
|
||||
|
||||
## What to Store
|
||||
|
||||
Good memory entries are stable and reusable:
|
||||
|
||||
- user preferences;
|
||||
- project conventions;
|
||||
- recurring output formats;
|
||||
- names of important repositories, directories, or services;
|
||||
- decisions the user wants reused;
|
||||
- brief notes from completed tasks.
|
||||
|
||||
Avoid memory for:
|
||||
|
||||
- API keys or secrets;
|
||||
- raw private data that does not need long-term recall;
|
||||
- one-off instructions for the current turn;
|
||||
- noisy dumps that would pollute future retrieval;
|
||||
- exact transcripts that should instead be exported as session records.
|
||||
|
||||
## Common Commands
|
||||
|
||||
Inspect memory health:
|
||||
|
||||
```sh
|
||||
opensquilla memory status
|
||||
opensquilla memory status --deep
|
||||
```
|
||||
|
||||
Index and list memory sources:
|
||||
|
||||
```sh
|
||||
opensquilla memory index
|
||||
opensquilla memory list
|
||||
```
|
||||
|
||||
Search and inspect memory:
|
||||
|
||||
```sh
|
||||
opensquilla memory search "release note format"
|
||||
opensquilla memory show <path>
|
||||
```
|
||||
|
||||
Search previous sessions as well as memory:
|
||||
|
||||
```sh
|
||||
opensquilla memory search "deployment decision" --source all
|
||||
```
|
||||
|
||||
## Natural Chat Usage
|
||||
|
||||
Ask naturally when something should be remembered:
|
||||
|
||||
```text
|
||||
Remember that I prefer concise release notes with a risk section.
|
||||
```
|
||||
|
||||
Later, refer to the preference:
|
||||
|
||||
```text
|
||||
Use my usual release-note format for this changelog.
|
||||
```
|
||||
|
||||
When memory seems stale, ask the agent to search explicitly:
|
||||
|
||||
```text
|
||||
Search memory for my release-note preferences before drafting this.
|
||||
```
|
||||
|
||||
## Session-Derived Memory
|
||||
|
||||
For long or important sessions, flush session state into memory before
|
||||
archiving, compacting, or switching tasks:
|
||||
|
||||
```sh
|
||||
opensquilla memory flush-session <session-key>
|
||||
```
|
||||
|
||||
Use session export when exact old wording matters:
|
||||
|
||||
```sh
|
||||
opensquilla sessions export <session-key>
|
||||
```
|
||||
|
||||
Memory is for useful recall. Session export is for exact records.
|
||||
|
||||
## Maintenance and Repair
|
||||
|
||||
Refresh the index after editing memory files or changing memory configuration:
|
||||
|
||||
```sh
|
||||
opensquilla memory index --force
|
||||
```
|
||||
|
||||
Inspect fallback and repair surfaces:
|
||||
|
||||
```sh
|
||||
opensquilla memory raw-fallbacks list
|
||||
opensquilla memory repair list
|
||||
```
|
||||
|
||||
Show or repair a degraded compaction memory record when instructed by
|
||||
diagnostics:
|
||||
|
||||
```sh
|
||||
opensquilla memory repair show --summary-id <id>
|
||||
opensquilla memory repair run --summary-id <id>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep entries short and sourceable.
|
||||
- Prefer "Remember X for project Y" over vague "remember this."
|
||||
- Search memory before assuming the agent forgot.
|
||||
- Remove or revise stale preferences instead of adding contradictory ones.
|
||||
- Keep secrets out of memory.
|
||||
- Use artifacts or files for large reference material.
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,439 @@
|
||||
# OpenSquilla MetaSkill User Guide
|
||||
|
||||
MetaSkill lets OpenSquilla move from figuring out complex work from scratch on
|
||||
every turn to reusable, explicitly launchable, auditable, and improvable task
|
||||
protocols.
|
||||
|
||||
A normal conversation solves one request. A MetaSkill preserves a way of doing
|
||||
high-value work.
|
||||
|
||||
## Important Notice
|
||||
|
||||
Some MetaSkills in OpenSquilla, and some of the skills they call, are authored,
|
||||
revised, or composed with AI assistance based on intended functionality,
|
||||
available capabilities, and usage scenarios.
|
||||
|
||||
This means:
|
||||
|
||||
- MetaSkills are not merely a collection of fully hand-written scripts. They are
|
||||
part of a system where AI can help formalize and evolve reusable task
|
||||
protocols.
|
||||
- AI-authored or AI-assisted MetaSkills should be reviewed through structural
|
||||
validation, trigger-surface checks, runtime testing, human review, and
|
||||
safety-boundary assessment before they are treated as ready for use.
|
||||
- MetaSkill outputs are decision-support materials and work-product drafts. They
|
||||
are not final professional advice in legal, medical, financial, hiring,
|
||||
academic, security, or other high-stakes contexts.
|
||||
- Actions such as publishing, applying, installing, paying, signing, messaging,
|
||||
or modifying production systems require explicit user authorization and remain
|
||||
the user's responsibility.
|
||||
- When a MetaSkill relies on search, document parsing, LLM judgment, or
|
||||
third-party tools, the result may be affected by source quality, model
|
||||
limitations, tool availability, context completeness, and time-sensitive
|
||||
changes.
|
||||
- Users should review facts, citations, assumptions, risks, and unverifiable
|
||||
claims, especially in high-stakes situations.
|
||||
|
||||
In short: MetaSkill turns high-value work into reusable, auditable, and
|
||||
improvable AI collaboration protocols. It does not remove the need for review,
|
||||
judgment, or accountability.
|
||||
|
||||
## What It Is
|
||||
|
||||
OpenSquilla is an open-source AI agent runtime. MetaSkill is its task-protocol
|
||||
layer.
|
||||
|
||||
A MetaSkill does not introduce new execution atoms. It defines a way to organize
|
||||
existing atoms, such as skills, tools, LLM calls, and sub-agents, into a
|
||||
reusable task protocol.
|
||||
|
||||
The analogy is a Makefile and shell commands. A Makefile does not replace
|
||||
commands; it defines how commands are composed. A MetaSkill does not replace
|
||||
skills or tools; it tells OpenSquilla how a class of high-value work should be
|
||||
understood, structured, checked, and delivered.
|
||||
|
||||
MetaSkill provides four main advantages:
|
||||
|
||||
- protocolized capability captured in a `SKILL.md` file with `kind: meta` and
|
||||
`composition.steps`;
|
||||
- explicit launch through `/meta`, with optional automatic triggering only when
|
||||
`meta_skill.auto_trigger = true`;
|
||||
- auditable and replayable step inputs, outputs, status, and results;
|
||||
- improvable over time as repeated collaboration patterns become proposals.
|
||||
|
||||
## Default Launch Model
|
||||
|
||||
MetaSkills are manual-only by default. On supported chat surfaces, use `/meta`
|
||||
to list available workflows and `/meta <name>` to run one. This keeps workflow
|
||||
launches deliberate, reviewable, and easier to explain.
|
||||
|
||||
Web chat and the CLI gateway TUI support both list and run:
|
||||
|
||||
```text
|
||||
/meta
|
||||
/meta meta-kid-project-planner
|
||||
```
|
||||
|
||||
Channel surfaces support `/meta` listing only. Standalone CLI chat requires
|
||||
gateway mode for `/meta`.
|
||||
|
||||
To restore the older automatic behavior, set:
|
||||
|
||||
```toml
|
||||
[meta_skill]
|
||||
auto_trigger = true
|
||||
```
|
||||
|
||||
With `auto_trigger = true`, OpenSquilla may consider MetaSkills during ordinary
|
||||
natural-language turns. Leave it off when you want workflows to run only after
|
||||
an explicit `/meta <name>` command.
|
||||
|
||||
## User Mental Model
|
||||
|
||||
Using a MetaSkill is not just asking a question. It is delegating OpenSquilla to
|
||||
produce a reviewable result.
|
||||
|
||||
A strong MetaSkill request contains four things:
|
||||
|
||||
1. Outcome: what you want to receive.
|
||||
2. Context: materials, entities, time range, and constraints that matter.
|
||||
3. Standard: what "good" means for this task.
|
||||
4. Boundaries: what must not happen, what must not be invented, and what requires
|
||||
confirmation.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
/meta meta-kid-project-planner
|
||||
|
||||
I need a safe weekend project plan, not a generic list of ideas.
|
||||
Use only materials that are easy to buy locally.
|
||||
Separate adult-only steps from child-safe steps.
|
||||
Do not include flames, blades, solvents, or risky chemicals.
|
||||
```
|
||||
|
||||
The user defines the target and standard; OpenSquilla organizes the execution.
|
||||
|
||||
## Current Built-In MetaSkills
|
||||
|
||||
The retained built-in MetaSkills cover a focused set of high-value task classes.
|
||||
|
||||
| MetaSkill | Positioning |
|
||||
| --- | --- |
|
||||
| `meta-kid-project-planner` | Produces safe, age-appropriate plans for school projects, show-and-tell, or science activities. |
|
||||
| `meta-paper-write` | Supports academic drafts, manuscript structure, citation planning, experiment placeholders, and LaTeX/PDF paths. |
|
||||
| `meta-short-drama` | Produces short-drama scripts, visual prompts, video assembly plans, subtitles, and rendered local video artifacts. |
|
||||
| `meta-skill-creator` | Turns repeated multi-skill collaboration patterns into new MetaSkill proposals. |
|
||||
|
||||
These are designed around quality over quantity. Immature, duplicate, or
|
||||
single-skill wrapper MetaSkills should not remain in the bundled catalog.
|
||||
|
||||
## Requirements Before Running MetaSkills
|
||||
|
||||
The Skill page is the source of truth for current readiness. Open the skill
|
||||
detail dialog and check the **Requirements** section before running workflows
|
||||
that export files, compile PDFs, or render video.
|
||||
|
||||
Common setup surfaces:
|
||||
|
||||
- Paper/PDF workflows such as `meta-paper-write` require `xelatex` and
|
||||
`bibtex` on `PATH`. Install a TeX distribution such as TeX Live, MiKTeX, or
|
||||
BasicTeX before requesting compiled PDFs.
|
||||
- Video workflows such as `meta-short-drama` require `ffmpeg` and `ffprobe` on
|
||||
`PATH` for clip animation, merging, and subtitle burn-in.
|
||||
- Office-document workflows roll up requirements from child skills such as
|
||||
`docx`, `xlsx`, `pdf-toolkit`, and `pptx`; these usually surface Python
|
||||
package requirements in the Skill page.
|
||||
- Search, weather, image, and video-provider steps may require configured API
|
||||
keys or provider credentials. The workflow should treat missing credentials as
|
||||
setup blockers rather than silently degrading output.
|
||||
|
||||
## Two Ways to Use MetaSkill
|
||||
|
||||
### Default: Explicit Command
|
||||
|
||||
Start the workflow with `/meta <name>` and then describe the outcome:
|
||||
|
||||
```text
|
||||
/meta meta-kid-project-planner
|
||||
|
||||
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
|
||||
materials, steps, safety notes, and a simple presentation outline.
|
||||
```
|
||||
|
||||
This is the normal 0.4 release-line path. It is best for important, expensive,
|
||||
or easily confused tasks because the workflow launch is explicit.
|
||||
|
||||
### Compatibility: Automatic Triggering
|
||||
|
||||
If `meta_skill.auto_trigger = true` is set, OpenSquilla can consider MetaSkills
|
||||
from natural-language intent:
|
||||
|
||||
```text
|
||||
Use meta-skill `meta-kid-project-planner`.
|
||||
|
||||
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
|
||||
materials, steps, safety notes, and a simple presentation outline.
|
||||
```
|
||||
|
||||
This mode is for users who intentionally want the older auto-trigger behavior.
|
||||
It is not the default.
|
||||
|
||||
## Low-Cost, High-Quality Request Template
|
||||
|
||||
Recommended template:
|
||||
|
||||
```text
|
||||
/meta <name>
|
||||
|
||||
Outcome:
|
||||
Context:
|
||||
Decision standard:
|
||||
Expected output:
|
||||
Constraints:
|
||||
Do not:
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
/meta meta-kid-project-planner
|
||||
|
||||
Outcome: plan a child-safe weekend science project.
|
||||
Context: 7-year-old, balcony plants, 20 minutes of activity, ordinary household
|
||||
materials only.
|
||||
Decision standard: safe, age-appropriate, low mess, and easy to present at
|
||||
school.
|
||||
Expected output: materials list, adult setup, child steps, safety notes, and a
|
||||
presentation outline.
|
||||
Constraints: avoid flames, blades, solvents, and risky chemicals.
|
||||
Do not: ask the child to do adult-only setup alone.
|
||||
```
|
||||
|
||||
Useful constraints:
|
||||
|
||||
- Do not invent missing facts.
|
||||
- Separate facts, assumptions, and recommendations.
|
||||
- Use only pasted material unless sources are available.
|
||||
- Do not submit, publish, install, pay, send, or sign automatically.
|
||||
- Ask me if a decision depends on missing information.
|
||||
|
||||
## Built-In MetaSkill Usage Patterns
|
||||
|
||||
### `meta-kid-project-planner`
|
||||
|
||||
Use for child school projects, show-and-tell, science demos, and safe creative
|
||||
activities.
|
||||
|
||||
Good fit:
|
||||
|
||||
- science fair;
|
||||
- show-and-tell;
|
||||
- classroom demonstration;
|
||||
- child-safe craft or experiment;
|
||||
- low-burden parent preparation.
|
||||
|
||||
High-quality request:
|
||||
|
||||
```text
|
||||
/meta meta-kid-project-planner
|
||||
|
||||
Help my child prepare a second-grade science fair project about plant growth. We
|
||||
have beans, paper cups, cotton, water, and a sunny windowsill.
|
||||
|
||||
Keep it safe and simple.
|
||||
|
||||
Give me:
|
||||
- materials list
|
||||
- 3-day plan
|
||||
- what the child should observe
|
||||
- short presentation script
|
||||
- what remains unknown
|
||||
```
|
||||
|
||||
Expected result: safe, age-appropriate, source-strict output. It should not
|
||||
invent weather, school requirements, or child preferences.
|
||||
|
||||
### `meta-paper-write`
|
||||
|
||||
Use for academic papers, research manuscripts, and LaTeX-oriented deliverables.
|
||||
|
||||
Good fit:
|
||||
|
||||
- compact paper skeleton;
|
||||
- section structure;
|
||||
- citation plan;
|
||||
- experiment and figure/table placeholders;
|
||||
- LaTeX/PDF path when explicitly requested.
|
||||
|
||||
PDF compilation requires `xelatex` and `bibtex` on `PATH`. If those binaries are
|
||||
missing, use the LaTeX source output or install TeX Live, MiKTeX, or BasicTeX
|
||||
before asking for a compiled PDF.
|
||||
|
||||
High-quality request:
|
||||
|
||||
```text
|
||||
/meta meta-paper-write
|
||||
|
||||
Draft a compact research paper skeleton on retrieval-augmented generation for
|
||||
customer-support knowledge bases.
|
||||
|
||||
Include:
|
||||
- title
|
||||
- abstract
|
||||
- related work plan
|
||||
- method outline
|
||||
- experiment placeholders
|
||||
- figure/table placeholders
|
||||
- citation plan
|
||||
|
||||
Keep it compact first. Do not write a full manuscript unless I ask.
|
||||
```
|
||||
|
||||
Expected result: a paper-shaped deliverable, not a generic essay. Citations
|
||||
should not be presented as verified sources unless actually verified.
|
||||
|
||||
### `meta-skill-creator`
|
||||
|
||||
Use to create a new MetaSkill proposal.
|
||||
|
||||
Good fit:
|
||||
|
||||
- turning repeated multi-skill collaboration into a reusable capability;
|
||||
- defining trigger surfaces;
|
||||
- composing existing skills;
|
||||
- adding validation and risk checks;
|
||||
- producing a proposal for review.
|
||||
|
||||
Poor fit:
|
||||
|
||||
- creating a normal single-purpose skill;
|
||||
- analyzing existing skill lists without creating anything;
|
||||
- asking what MetaSkill is;
|
||||
- pasting old pages for diagnosis.
|
||||
|
||||
High-quality request:
|
||||
|
||||
```text
|
||||
/meta meta-skill-creator
|
||||
|
||||
Create a new meta-skill for product launch briefs. It should search current
|
||||
sources, collect product context, draft a launch memo, generate a DOCX handoff,
|
||||
check evidence gaps, and avoid publishing anything automatically.
|
||||
|
||||
Please propose:
|
||||
- name
|
||||
- description
|
||||
- triggers
|
||||
- steps
|
||||
- validation gates
|
||||
- collision checks
|
||||
```
|
||||
|
||||
Expected result: a proposal, not an immediate unreviewed production rollout.
|
||||
|
||||
## Avoiding Accidental Activation
|
||||
|
||||
If you paste old chat history, Web UI dumps, prompt examples, skill lists, or
|
||||
test material, mark it as quoted context:
|
||||
|
||||
```text
|
||||
The following is quoted context, not my current request.
|
||||
Do not run any skill.
|
||||
Do not create or persist any proposal.
|
||||
Only analyze this text.
|
||||
```
|
||||
|
||||
This matters because historical material may contain trigger words. Without a
|
||||
clear boundary, the system may confuse quoted content with current intent.
|
||||
|
||||
If you only want to analyze a MetaSkill and do not want proposal creation:
|
||||
|
||||
```text
|
||||
Only analyze. Do not create, assemble, preview, or persist any meta-skill
|
||||
proposal.
|
||||
```
|
||||
|
||||
## Run Progress Ribbon
|
||||
|
||||
While a MetaSkill runs, the WebUI shows a horizontal ribbon at the top
|
||||
of the agent reply listing every step in the workflow. The currently
|
||||
running chip is highlighted; succeeded steps show ✓, skipped ↷, failed
|
||||
✗, and `on_failure` substitutes show ⇄. Click any chip to scroll to
|
||||
that step's tool card. If a step fails, the ribbon also surfaces
|
||||
"Retry run", "Switch meta-skill", and "Show error detail" actions
|
||||
inline.
|
||||
|
||||
The ribbon survives disconnects: when the browser reconnects, the gateway
|
||||
replays the announce → state → completed events so the ribbon rebuilds
|
||||
to the latest state.
|
||||
|
||||
## Reading the Result
|
||||
|
||||
A strong MetaSkill result should explain:
|
||||
|
||||
- what it produced;
|
||||
- what facts or sources it used;
|
||||
- what is inferred or assumed;
|
||||
- what risks remain;
|
||||
- what the next action is;
|
||||
- what could not be verified;
|
||||
- whether any artifact or proposal was actually created.
|
||||
|
||||
Be cautious if the output:
|
||||
|
||||
- claims current facts without sources;
|
||||
- claims a file was created but no artifact exists;
|
||||
- hides tool failures as success;
|
||||
- gives generic advice instead of the requested deliverable;
|
||||
- ignores "do not create", "do not send", "do not publish", or "do not install".
|
||||
|
||||
## Correcting a Bad Run
|
||||
|
||||
If the wrong MetaSkill triggered:
|
||||
|
||||
```text
|
||||
Stop using the previous MetaSkill. Treat my earlier text as context only. Now
|
||||
use meta-skill `<correct_name>` for this goal: ...
|
||||
```
|
||||
|
||||
If no MetaSkill triggered:
|
||||
|
||||
```text
|
||||
Please rerun and explicitly use meta-skill `<name>`.
|
||||
```
|
||||
|
||||
If the output is too generic:
|
||||
|
||||
```text
|
||||
Redo this as a decision-ready deliverable with evidence, assumptions, risks, and
|
||||
next actions.
|
||||
```
|
||||
|
||||
If creator starts creating but you do not want creation:
|
||||
|
||||
```text
|
||||
Do not create, assemble, preview, or persist any meta-skill proposal. Only
|
||||
analyze.
|
||||
```
|
||||
|
||||
## Building Your Own MetaSkill
|
||||
|
||||
A task is a good MetaSkill candidate when:
|
||||
|
||||
- you repeatedly perform the same high-value task;
|
||||
- each run has multiple steps;
|
||||
- inputs are similar but details vary;
|
||||
- the output format is relatively stable;
|
||||
- review, audit, replay, or confirmation matters;
|
||||
- ordinary prompts require you to restate too many rules every time.
|
||||
|
||||
Poor candidates include one-line fact queries, single tool calls, casual
|
||||
conversation, brainstorming without stable output criteria, and high-risk
|
||||
automated action without human confirmation.
|
||||
|
||||
For the authoring protocol, read [`../authoring/meta-skills.md`](../authoring/meta-skills.md).
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,177 @@
|
||||
# Meta-Skills
|
||||
|
||||
Meta-skills package repeatable multi-step work as reusable, inspectable
|
||||
workflows. Use them when a request needs more than one normal skill, tool,
|
||||
checkpoint, or final synthesis pass.
|
||||
|
||||
For the full user-facing guide, read
|
||||
[`meta-skill-user-guide.md`](meta-skill-user-guide.md). For authoring rules,
|
||||
read [`../authoring/meta-skills.md`](../authoring/meta-skills.md).
|
||||
|
||||
## Skills vs Meta-Skills
|
||||
|
||||
| Capability | Use it for |
|
||||
| --- | --- |
|
||||
| Skill | One focused task pattern, instruction set, script, or tool helper. |
|
||||
| Meta-skill | A reusable workflow made of multiple steps, skills, checks, or outputs. |
|
||||
|
||||
For example, "summarize this document" is skill-shaped. "Plan a safe child
|
||||
science project with materials, adult setup, child steps, presentation notes, and
|
||||
final safety review" is meta-skill-shaped.
|
||||
|
||||
## Stable Built-In MetaSkills
|
||||
|
||||
The retained stable catalog is intentionally small:
|
||||
|
||||
| MetaSkill | Positioning |
|
||||
| --- | --- |
|
||||
| `meta-kid-project-planner` | Produces safe, age-appropriate plans for school projects, show-and-tell, or science activities. |
|
||||
| `meta-paper-write` | Supports academic drafts, manuscript structure, citation planning, experiment placeholders, and LaTeX/PDF paths. |
|
||||
| `meta-short-drama` | Produces short-drama scripts, visual prompts, subtitles, and local video artifacts. |
|
||||
| `meta-skill-creator` | Turns repeated multi-skill collaboration patterns into new MetaSkill proposals. |
|
||||
|
||||
Experimental meta-skills may exist under development trees, but this page lists
|
||||
only bundled built-ins that should be presented as retained product
|
||||
capabilities.
|
||||
|
||||
## Requirements
|
||||
|
||||
Use the Skill page detail dialog before running a MetaSkill. Its
|
||||
**Requirements** section shows the MetaSkill's own requirements plus one-hop
|
||||
requirements from child skills.
|
||||
|
||||
- `meta-paper-write` needs `xelatex` and `bibtex` for PDF compilation.
|
||||
- `meta-short-drama` needs `ffmpeg` and `ffprobe` for local video rendering,
|
||||
merge, and subtitle steps.
|
||||
- MetaSkills inherit readiness from their child skills; for example,
|
||||
`meta-paper-write` surfaces LaTeX/PDF requirements and
|
||||
`meta-short-drama` surfaces local video-tool requirements.
|
||||
|
||||
## Run MetaSkills
|
||||
|
||||
MetaSkills are manual-only by default. They do not auto-trigger from message
|
||||
keywords or appear in the runtime prompt unless you explicitly opt into the old
|
||||
automatic behavior.
|
||||
|
||||
In Web chat and the CLI gateway TUI:
|
||||
|
||||
```text
|
||||
/meta
|
||||
/meta meta-kid-project-planner
|
||||
```
|
||||
|
||||
`/meta` lists available MetaSkills. `/meta <name>` starts the selected
|
||||
workflow. Channel surfaces can list MetaSkills with `/meta`, but they do not run
|
||||
MetaSkills from chat text. Standalone CLI chat requires gateway mode for
|
||||
`/meta`.
|
||||
|
||||
To restore automatic model-triggered behavior, set:
|
||||
|
||||
```toml
|
||||
[meta_skill]
|
||||
auto_trigger = true
|
||||
```
|
||||
|
||||
Use this compatibility mode only when you want MetaSkills to be considered by
|
||||
the model during ordinary chat turns.
|
||||
|
||||
## How to Prepare the Request
|
||||
|
||||
Ask for the outcome and the standard:
|
||||
|
||||
```text
|
||||
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
|
||||
materials, adult setup, child steps, safety notes, and a presentation outline.
|
||||
```
|
||||
|
||||
When you start a workflow, include the task after the command:
|
||||
|
||||
```text
|
||||
/meta meta-kid-project-planner
|
||||
|
||||
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
|
||||
materials, adult setup, child steps, safety notes, and a presentation outline.
|
||||
```
|
||||
|
||||
A strong request usually includes:
|
||||
|
||||
- outcome;
|
||||
- context;
|
||||
- decision standard;
|
||||
- expected output;
|
||||
- constraints;
|
||||
- actions the agent must not take.
|
||||
|
||||
## Discover Meta-Skills
|
||||
|
||||
Use chat slash commands for the runtime list:
|
||||
|
||||
```text
|
||||
/meta
|
||||
```
|
||||
|
||||
Use the CLI for inventory and inspection:
|
||||
|
||||
```sh
|
||||
opensquilla skills list
|
||||
opensquilla skills search meta
|
||||
```
|
||||
|
||||
Inspect a meta-skill composition:
|
||||
|
||||
```sh
|
||||
opensquilla skills inspect <meta-skill-name>
|
||||
```
|
||||
|
||||
The inspect command shows the compiled step shape before you rely on a workflow.
|
||||
|
||||
## Inspect Run History
|
||||
|
||||
List recent runs:
|
||||
|
||||
```sh
|
||||
opensquilla skills meta runs list
|
||||
```
|
||||
|
||||
Inspect one run:
|
||||
|
||||
```sh
|
||||
opensquilla skills meta runs show <run-id>
|
||||
opensquilla skills meta runs steps <run-id>
|
||||
opensquilla skills meta runs failures --since 24h
|
||||
```
|
||||
|
||||
Preview replay shape without executing live work:
|
||||
|
||||
```sh
|
||||
opensquilla skills meta runs replay <run-id> --dry-run
|
||||
```
|
||||
|
||||
## Proposals
|
||||
|
||||
Meta-skill creation workflows may write proposals before they become managed
|
||||
skills. Inspect proposals:
|
||||
|
||||
```sh
|
||||
opensquilla skills meta proposals list
|
||||
opensquilla skills meta proposals show <proposal-id>
|
||||
```
|
||||
|
||||
Accept a proposal only after review:
|
||||
|
||||
```sh
|
||||
opensquilla skills meta proposals accept <proposal-id>
|
||||
```
|
||||
|
||||
## Safety Model
|
||||
|
||||
MetaSkill outputs are reviewable work products and decision-support drafts. They
|
||||
are not final professional advice in legal, medical, financial, hiring,
|
||||
academic, security, or other high-stakes contexts.
|
||||
|
||||
Actions such as publishing, applying, installing, paying, signing, messaging, or
|
||||
modifying production systems require explicit user authorization.
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,159 @@
|
||||
# Skills
|
||||
|
||||
Skills are task-specific instruction packages and scripts. They let OpenSquilla
|
||||
load relevant guidance only when a task needs it, instead of putting every
|
||||
possible instruction into every prompt.
|
||||
|
||||
Skills are separate from memory. Memory stores facts; skills describe repeatable
|
||||
ways to work.
|
||||
|
||||
## What Skills Are For
|
||||
|
||||
Use skills for repeatable work patterns such as:
|
||||
|
||||
- deep research;
|
||||
- summarization;
|
||||
- GitHub and PR workflows;
|
||||
- document generation;
|
||||
- spreadsheet, slide, PDF, and DOCX work;
|
||||
- web search;
|
||||
- weather lookup;
|
||||
- terminal or tmux monitoring;
|
||||
- subagent delegation;
|
||||
- skill creation and review.
|
||||
|
||||
If the workflow combines multiple skills or a reusable multi-step plan, use a
|
||||
meta-skill instead.
|
||||
|
||||
## Discover Installed Skills
|
||||
|
||||
List skills available in the current install:
|
||||
|
||||
```sh
|
||||
opensquilla skills list
|
||||
```
|
||||
|
||||
View one skill:
|
||||
|
||||
```sh
|
||||
opensquilla skills view <skill-name>
|
||||
```
|
||||
|
||||
Search community sources:
|
||||
|
||||
```sh
|
||||
opensquilla skills search pdf
|
||||
```
|
||||
|
||||
Some skills may be ineligible when optional dependencies are missing or when the
|
||||
skill is intentionally demo-only. `skills list` is the source of truth for your
|
||||
current install.
|
||||
|
||||
## Install, Update, and Remove Skills
|
||||
|
||||
Install a managed skill:
|
||||
|
||||
```sh
|
||||
opensquilla skills install <skill-name>
|
||||
```
|
||||
|
||||
Update one skill or all managed skills:
|
||||
|
||||
```sh
|
||||
opensquilla skills update <skill-name>
|
||||
opensquilla skills update --all
|
||||
```
|
||||
|
||||
Remove a managed skill:
|
||||
|
||||
```sh
|
||||
opensquilla skills uninstall <skill-name>
|
||||
```
|
||||
|
||||
## Manage Skill Sources
|
||||
|
||||
Custom source repositories are called taps:
|
||||
|
||||
```sh
|
||||
opensquilla skills tap list
|
||||
opensquilla skills tap add <owner/repo>
|
||||
opensquilla skills tap remove <owner/repo>
|
||||
```
|
||||
|
||||
Use taps when your team maintains its own skill catalog.
|
||||
|
||||
## Publish and Inspect
|
||||
|
||||
Publish a skill directory:
|
||||
|
||||
```sh
|
||||
opensquilla skills publish <path-to-skill>
|
||||
```
|
||||
|
||||
Inspect the compiled composition for a meta-skill:
|
||||
|
||||
```sh
|
||||
opensquilla skills inspect <meta-skill-name>
|
||||
```
|
||||
|
||||
For ordinary skill content, use:
|
||||
|
||||
```sh
|
||||
opensquilla skills view <skill-name>
|
||||
```
|
||||
|
||||
## How to Ask for a Skill
|
||||
|
||||
Ask for the outcome:
|
||||
|
||||
```text
|
||||
Create a PowerPoint deck summarizing this report.
|
||||
```
|
||||
|
||||
Better than:
|
||||
|
||||
```text
|
||||
Load the pptx skill and run its script.
|
||||
```
|
||||
|
||||
OpenSquilla can choose eligible skills from the current catalog when the task
|
||||
matches their description and triggers.
|
||||
|
||||
## Bundled Skill Families
|
||||
|
||||
| Family | Examples |
|
||||
| --- | --- |
|
||||
| Research | deep research, multi-source search, summarization |
|
||||
| Documents | DOCX, PPTX, XLSX, PDF, HTML-to-PDF |
|
||||
| Operations | cron, GitHub, terminal monitoring, subagents |
|
||||
| Memory | memory-oriented helpers and history exploration |
|
||||
| Creation | skill creator, skill review, proposal helpers |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a skill is not selected:
|
||||
|
||||
1. Confirm it appears in the installed catalog:
|
||||
|
||||
```sh
|
||||
opensquilla skills list
|
||||
```
|
||||
|
||||
2. Inspect its description and eligibility:
|
||||
|
||||
```sh
|
||||
opensquilla skills view <skill-name>
|
||||
```
|
||||
|
||||
3. Ask for the outcome in normal language. Skill names can help, but user
|
||||
intent should still be clear.
|
||||
|
||||
4. If optional dependencies are missing, install or update the skill and retry.
|
||||
|
||||
For composed workflows, read [`meta-skills.md`](meta-skills.md). For the full
|
||||
MetaSkill user guide, read [`meta-skill-user-guide.md`](meta-skill-user-guide.md).
|
||||
For authoring rules, read [`../authoring/meta-skills.md`](../authoring/meta-skills.md).
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,172 @@
|
||||
# SquillaRouter
|
||||
|
||||
SquillaRouter is OpenSquilla's local model-routing layer. It helps the agent
|
||||
choose an appropriate model tier for each turn so routine work does not always
|
||||
run on the most expensive model.
|
||||
|
||||
Use this page when you want to enable routing, understand what it changes, or
|
||||
decide whether a fixed provider/model is better for a specific run.
|
||||
|
||||
## Why Use It
|
||||
|
||||
SquillaRouter is useful when you want:
|
||||
|
||||
- lower cost for simple chat, edits, summaries, and routine tool work;
|
||||
- stronger models reserved for hard reasoning, recovery, and long tasks;
|
||||
- one OpenSquilla workflow that can route across provider profiles;
|
||||
- local routing decisions without sending prompts to a separate external
|
||||
classifier just to choose the model.
|
||||
|
||||
It is not required. OpenSquilla can also run in direct single-model mode.
|
||||
|
||||
## Enable Routing
|
||||
|
||||
Recommended first-run setup:
|
||||
|
||||
```sh
|
||||
opensquilla onboard --router recommended
|
||||
```
|
||||
|
||||
Reconfigure an existing install:
|
||||
|
||||
```sh
|
||||
opensquilla configure router --router recommended
|
||||
```
|
||||
|
||||
Use the OpenRouter mixed defaults:
|
||||
|
||||
```sh
|
||||
opensquilla configure router --router openrouter-mix
|
||||
```
|
||||
|
||||
Disable routing and use the configured provider/model directly:
|
||||
|
||||
```sh
|
||||
opensquilla configure router --router disabled
|
||||
```
|
||||
|
||||
## Inspect Provider Support
|
||||
|
||||
Check the provider catalog available in your install:
|
||||
|
||||
```sh
|
||||
opensquilla providers list
|
||||
```
|
||||
|
||||
If the gateway is running, inspect runtime provider health:
|
||||
|
||||
```sh
|
||||
opensquilla providers status
|
||||
```
|
||||
|
||||
Router-supported profiles depend on the installed OpenSquilla version,
|
||||
optional dependencies, and configured provider credentials. Common profiles
|
||||
include OpenRouter, OpenAI, DeepSeek, Gemini, DashScope, Moonshot, Volcengine,
|
||||
Zhipu, and compatible provider tiers exposed by the local catalog.
|
||||
|
||||
## What the Router Can Affect
|
||||
|
||||
Depending on configuration, SquillaRouter may influence:
|
||||
|
||||
- selected model tier;
|
||||
- direct model fallback;
|
||||
- reasoning level;
|
||||
- response policy;
|
||||
- image-capable model selection;
|
||||
- cache-continuity safeguards for recent higher-tier turns.
|
||||
|
||||
The exact decision is available through runtime metadata and diagnostics
|
||||
surfaces. Turn on diagnostics when you need to understand why a turn was routed
|
||||
to a particular model:
|
||||
|
||||
```sh
|
||||
opensquilla diagnostics on
|
||||
```
|
||||
|
||||
## Terminal Router HUD
|
||||
|
||||
Interactive terminal chat can surface routing decisions through a TUI Router HUD
|
||||
when router metadata is present and the selected backend supports the structured
|
||||
UI/plugin surface. In the current implementation, the OpenTUI preview footer is
|
||||
the primary terminal display for this HUD. The HUD is display-only: it consumes
|
||||
the same turn metadata and does not change model selection.
|
||||
|
||||
The HUD can show the selected tier, selected model, baseline model, route
|
||||
source, confidence, estimated savings, fallback state, thinking mode, prompt
|
||||
policy, whether routing was applied, and rollout phase.
|
||||
|
||||
Full routing is shown as an active route. Observe-only routing is shown as an
|
||||
observe decision, which means OpenSquilla recorded what the router would have
|
||||
chosen while keeping the configured baseline behavior. Fallback decisions use a
|
||||
warning style so provider or policy recovery is visible during the turn.
|
||||
|
||||
## Recommended Operating Modes
|
||||
|
||||
| Goal | Suggested mode |
|
||||
| --- | --- |
|
||||
| General personal-agent use | `recommended` |
|
||||
| Multi-provider cost optimization through OpenRouter | `openrouter-mix` |
|
||||
| Provider evaluation, billing audit, or reproducible benchmark run | `disabled` |
|
||||
| Debugging one provider-specific behavior | `disabled` |
|
||||
|
||||
For routine use, start with `recommended`. Disable routing only when the model
|
||||
choice itself is the thing you are testing.
|
||||
|
||||
## Example Requests
|
||||
|
||||
Good router-friendly requests describe the outcome, not the tier:
|
||||
|
||||
```text
|
||||
Summarize this long issue thread and list the decision points.
|
||||
```
|
||||
|
||||
```text
|
||||
Review my current diff and point out the highest-risk changes.
|
||||
```
|
||||
|
||||
Avoid asking the router to behave like a manual model picker unless you are
|
||||
debugging:
|
||||
|
||||
```text
|
||||
Use exactly this one model for every turn.
|
||||
```
|
||||
|
||||
For exact-model work, configure direct routing instead.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If routing does not appear to work:
|
||||
|
||||
1. Confirm the router is enabled:
|
||||
|
||||
```sh
|
||||
opensquilla config get router.enabled
|
||||
opensquilla config get llm.provider
|
||||
```
|
||||
|
||||
2. Check provider readiness:
|
||||
|
||||
```sh
|
||||
opensquilla providers status
|
||||
opensquilla doctor
|
||||
```
|
||||
|
||||
3. If SquillaRouter optional dependencies are missing, OpenSquilla can still run
|
||||
with direct single-model routing. On Windows, ONNX Runtime may require the
|
||||
Visual C++ Redistributable. On macOS terminal installs, LightGBM may require
|
||||
`libomp` from Homebrew:
|
||||
|
||||
```sh
|
||||
brew install libomp
|
||||
opensquilla gateway restart
|
||||
```
|
||||
|
||||
4. If you need deterministic model behavior for a run, disable routing:
|
||||
|
||||
```sh
|
||||
opensquilla configure router --router disabled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,118 @@
|
||||
# Tool Compression
|
||||
|
||||
OpenSquilla agents use tools. Tool calls can produce large outputs: command
|
||||
logs, JSON, web pages, search results, diffs, file contents, tables, and
|
||||
artifacts. Tool compression keeps those outputs useful without letting them
|
||||
consume the whole model context.
|
||||
|
||||
This is a user-facing context-management feature. It does not change what the
|
||||
tool returned; it changes how much of that result is shown to the model for the
|
||||
next step.
|
||||
|
||||
## Why It Matters
|
||||
|
||||
Tool compression helps when:
|
||||
|
||||
- a command prints a large log;
|
||||
- a web page or search result is too long for a useful prompt;
|
||||
- a file read returns more text than the next step needs;
|
||||
- a long session is close to the context budget;
|
||||
- you want raw results preserved while the model sees a compact preview.
|
||||
|
||||
Without compression, one large tool result can crowd out the user's goal,
|
||||
recent conversation, and next action.
|
||||
|
||||
## What Users May See
|
||||
|
||||
In long or tool-heavy turns, the model-visible result may include:
|
||||
|
||||
- a compact preview;
|
||||
- a note that a result was shortened;
|
||||
- a `tool_result_handle` for an out-of-band stored result;
|
||||
- estimated token-saving diagnostics when diagnostics are enabled.
|
||||
|
||||
This is expected. It means OpenSquilla is protecting the active context window.
|
||||
|
||||
## Product-Level Model
|
||||
|
||||
OpenSquilla separates two views:
|
||||
|
||||
| View | Purpose |
|
||||
| --- | --- |
|
||||
| Runtime view | The durable result OpenSquilla can preserve, inspect, or export. |
|
||||
| Provider view | The bounded text sent back to the model for the next reasoning step. |
|
||||
|
||||
The agent can continue from the important facts while large raw material stays
|
||||
available through files, session export, diagnostics, or tool-result handles
|
||||
when configured.
|
||||
|
||||
## Compression Modes
|
||||
|
||||
OpenSquilla supports several compression styles depending on configuration and
|
||||
tool output shape.
|
||||
|
||||
| Mode | Best for | Tradeoff |
|
||||
| --- | --- | --- |
|
||||
| `truncate` | Fast deterministic previews. | May omit useful middle sections. |
|
||||
| `summarize` | Slower/background workflows that benefit from semantic summaries. | Adds another model call and should be opt-in. |
|
||||
| Structured projection | Logs, diffs, JSON, tables, and known tool shapes. | Depends on reducer coverage for that output type. |
|
||||
|
||||
Most users should keep the default behavior and use diagnostics only when a
|
||||
workflow is still too large.
|
||||
|
||||
## How to Work With Large Outputs
|
||||
|
||||
Ask for focused follow-up reads:
|
||||
|
||||
```text
|
||||
Look at the failing test names and the last 80 lines of the log.
|
||||
```
|
||||
|
||||
Prefer handles, paths, and summaries:
|
||||
|
||||
```text
|
||||
Use the compacted result to identify likely causes, then read the exact file
|
||||
sections you need.
|
||||
```
|
||||
|
||||
Avoid asking the agent to paste every line of a huge result unless exact text is
|
||||
the deliverable:
|
||||
|
||||
```text
|
||||
Paste the entire 50,000-line log into chat.
|
||||
```
|
||||
|
||||
## Inspect and Debug
|
||||
|
||||
Turn on diagnostics when you need to understand context growth:
|
||||
|
||||
```sh
|
||||
opensquilla diagnostics on
|
||||
```
|
||||
|
||||
Export the session when you need to inspect durable history outside the chat
|
||||
surface:
|
||||
|
||||
```sh
|
||||
opensquilla sessions export <session-key>
|
||||
```
|
||||
|
||||
Review cost and usage after a large tool-heavy run:
|
||||
|
||||
```sh
|
||||
opensquilla cost
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep tool requests specific.
|
||||
- Ask for the smallest file ranges, log tail, or JSON fields that answer the
|
||||
question.
|
||||
- Use artifacts for large deliverables instead of forcing everything into chat.
|
||||
- Use session export for audit and debugging.
|
||||
- Treat tool compression as a continuity feature, not as a substitute for
|
||||
storing important files.
|
||||
|
||||
---
|
||||
|
||||
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
|
||||
@@ -0,0 +1,105 @@
|
||||
# TUI Frontend
|
||||
|
||||
OpenSquilla terminal chat has one stable default backend and one opt-in preview
|
||||
backend:
|
||||
|
||||
| Backend or target | Status | How to use | Requirements |
|
||||
| --- | --- | --- | --- |
|
||||
| `native` | Stable default | `opensquilla chat` | Python package only |
|
||||
| `opentui` | Preview opt-in | `OPENSQUILLA_TUI_BACKEND=opentui uv run opensquilla chat` | Source checkout, Bun, and local OpenTUI package dependencies |
|
||||
| `live-opentui` | Manual harness target | Real-terminal harness only | tmux, OpenTUI deps, and live provider config |
|
||||
|
||||
`live-opentui` is not an `OPENSQUILLA_TUI_BACKEND` value. It is a guarded test
|
||||
target that launches the OpenTUI preview path through the real CLI.
|
||||
|
||||
The TUI contracts are renderer-independent and built around two separate planes:
|
||||
|
||||
- **Streaming plane:** batches token deltas before writing to the terminal, so
|
||||
long answers do not redraw the whole interface for every token.
|
||||
- **Structured UI plane:** sends normalized TUI domain events to plugins. Plugin
|
||||
snapshots can be rendered by capable TUI backends and by future renderers.
|
||||
|
||||
The stable default terminal chat is Python-native and does not require Bun,
|
||||
npm, or OpenTUI node modules. OpenTUI is a source-checkout preview backend
|
||||
selected explicitly with `OPENSQUILLA_TUI_BACKEND=opentui`.
|
||||
|
||||
## Plugin Slots
|
||||
|
||||
Plugins consume renderer-independent events and publish small snapshots through
|
||||
named slots. Current slots include:
|
||||
|
||||
| Slot | Purpose |
|
||||
| --- | --- |
|
||||
| `router_hud` | Active-turn model-routing decision. |
|
||||
| `status` | Compact status or queue notices. |
|
||||
| `tool_activity` | Tool cards and tool summary history. |
|
||||
| `usage` | Token, cache, and cost summary. |
|
||||
| `inspector` | Optional detail panel state for selected items. |
|
||||
|
||||
The first plugin is `RouterHudPlugin`. It listens for
|
||||
`router_decision` events and updates the bottom toolbar without changing router
|
||||
selection behavior.
|
||||
|
||||
## Router HUD
|
||||
|
||||
When routing metadata is available, capable TUI backends can render a Router
|
||||
HUD. In the current implementation, the OpenTUI footer is the primary preview
|
||||
display for this HUD. The HUD is display-only: it consumes turn metadata and
|
||||
does not change model selection.
|
||||
|
||||
The HUD can show:
|
||||
|
||||
- selected tier and model;
|
||||
- baseline model;
|
||||
- route source;
|
||||
- confidence;
|
||||
- estimated savings;
|
||||
- fallback state;
|
||||
- thinking mode;
|
||||
- prompt policy;
|
||||
- whether routing was applied;
|
||||
- rollout phase.
|
||||
|
||||
`routing_applied=true` with a full rollout is shown as an active route.
|
||||
`routing_applied=false` or an observe rollout is shown as observe-only. Fallback
|
||||
routes use warning styling.
|
||||
|
||||
## Backend Selection
|
||||
|
||||
The default backend is stable Python-native terminal chat.
|
||||
|
||||
The internal backend selector reads `OPENSQUILLA_TUI_BACKEND`. Unset or empty
|
||||
values select stable terminal chat. Set the variable to `opentui` only in a
|
||||
source checkout when evaluating the preview backend. Legacy values fail before
|
||||
chat launch with a clear unsupported-backend error.
|
||||
|
||||
```sh
|
||||
bun install --frozen-lockfile --cwd=src/opensquilla/cli/tui/opentui/package
|
||||
OPENSQUILLA_TUI_BACKEND=opentui uv run opensquilla chat
|
||||
```
|
||||
|
||||
The preview backend is loaded from the OpenTUI package next to the running
|
||||
source tree; it is not required for normal terminal chat.
|
||||
|
||||
Do not add parallel terminal/frontend implementations without fresh product
|
||||
direction and replay plus real-terminal evidence.
|
||||
|
||||
## Replay Benchmarks
|
||||
|
||||
The replay harness measures the OpenTUI rendering path without a live provider:
|
||||
|
||||
```sh
|
||||
uv run python scripts/bench_tui_replay.py --renderer opentui --fixture long-stream --summary-json .artifacts/tui/opentui-long-stream.json
|
||||
uv run python scripts/bench_tui_replay.py --renderer opentui --fixture dense-history --summary-json .artifacts/tui/opentui-dense-history.json
|
||||
```
|
||||
|
||||
Summary fields include `renderer`, `fixture`, `available`, `skip_reason`,
|
||||
`event_count`, `text_chars`, `tool_count`, `router_decision_count`, `wall_ms`,
|
||||
`flush_count`, `max_buffer_chars`, `coalescing_ratio`, `transcript_items`,
|
||||
`visible_items`, `expanded_tools`, `projection_wall_ms`,
|
||||
`rendered_text_matches`, `plugin_error_count`, and `errors`.
|
||||
|
||||
Use the OpenTUI results as preview backend evidence.
|
||||
|
||||
For terminal-level launch and rendering evidence, use the
|
||||
[real-terminal TUI harness](../tui-real-terminal-harness.md).
|
||||
Reference in New Issue
Block a user