Skip to content

Use published terraphim crates for router integration#1

Open
AlexMikhalev wants to merge 34 commits into
task/new-model-support-v2from
task/use-published-terraphim
Open

Use published terraphim crates for router integration#1
AlexMikhalev wants to merge 34 commits into
task/new-model-support-v2from
task/use-published-terraphim

Conversation

@AlexMikhalev

Copy link
Copy Markdown

Switch the terraphim-routing feature dependencies from local path deps under ../terraphim/... to crates.io =1.20.4 releases of terraphim_automata and terraphim_types.

What

  • Replace the path dependency on terraphim_router and terraphim_types with terraphim_automata = "=1.20.4" and terraphim_types = "=1.20.4" from crates.io. The remote branch still pointed at ../terraphim-ai/... which is not present on this machine, and the terraphim-ai_main checkout available here is a divergent 1.2.x fork.
  • Drop terraphim_router entirely. The rewritten pi_terraphim_router does not import anything from that crate; it builds on top of terraphim_automata (Aho-Corasick find_matches, parse_markdown_directives_dir) and terraphim_types (MarkdownDirectives, Thesaurus, NormalizedTerm, RouteDirective).
  • Narrow the terraphim-routing feature to ["dep:terraphim_automata", "dep:terraphim_types"] so it only pulls in the crates the code actually uses.
  • Pin to =1.20.4 so the lockfile is deterministic and matches the API we verified.

Why

The path-dep approach is environment-fragile. It depends on a sibling terraphim-ai/ checkout whose layout differs between contributors and forks. The previous attempt to point at terraphim-service and terraphim-ai_main worked only because the local code used a tiny surface (Capability); the rewritten module depends on MarkdownDirectives, Thesaurus, find_matches, parse_markdown_directives_dir, etc., which are only present in the published 1.20.x line.

Using crates.io also removes the dependency on a single co-located workspace and makes the branch buildable on any machine with network access.

Verification

  • cargo check --bin pi: clean (default features).
  • cargo check --bin pi --features terraphim-routing: clean. Resolves and compiles terraphim_types 1.20.4 and terraphim_automata 1.20.4.
  • cargo clippy --bin pi --features terraphim-routing -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo test --lib --features terraphim-routing pi_terraphim_router: 51 passed, 0 failed, including the embedded planning / implementation / review tier tests.
  • cargo test --lib provider_default_model_id_resolves_coding_plan_and_corrected_defaults: passed.
  • cargo run --features terraphim-routing --example terraphim_router: smoke-tested end to end. Loaded 3 routing rules from the embedded fallback taxonomy, routed the example prompts to the expected providers/tiers.
  • Release binary rebuilt: pi 0.1.20 a9a084bd, reinstalled via install.sh --from-source --force.

Out of scope

  • Full cargo test --lib run was attempted locally but a small number of unrelated long-running tests (in acp::, tools::, providers::) exhaust the local shell timeout. They do not relate to this change and should be exercised in CI.
  • The remote branch still has stray crates/terraphim_settings/default/settings.toml and .beads/.local_version, .beads/daemon-error, .beads/daemon.log files from commit 88bc4254. They are not touched by this PR; a separate small cleanup will follow.

Branches

  • Head: task/use-published-terraphim
  • Base: task/new-model-support-v2

cc any reviewers interested in the router rewrite and the dep graph.

Jeffrey Emanuel and others added 30 commits May 24, 2026 21:02
Continues the cleanup begun in commit 8c54482 ("chore: update beads issue
tracking, remove stale conformance test fixtures") by removing 70 additional
fixture files under tests/ext_conformance/artifacts/ that are no longer
referenced by the live conformance corpus.

Removed buckets:
  - tests/ext_conformance/artifacts/claude-rules/claude-rules.ts
  - tests/ext_conformance/artifacts/npm/vaayne-agent-kit/claude-plugins/
    specs-dev/ (README, agents, commands)
  - tests/ext_conformance/artifacts/plugins-community/plugins/community/
    claude-never-forgets/ (plugin manifest, LICENSE, README, commands,
    hooks, skills/memory subtree)
  - tests/ext_conformance/artifacts/plugins-community/plugins/community/
    claude-reflect/ (plugin manifest, LICENSE, README, SKILL, commands,
    skip-reflect)

These were external snapshots of upstream plugin repos that have since
been refreshed or dropped from the curated corpus; the live corpus still
ships ~18,250 artifacts after this trim (~0.4% reduction).

No production code paths reference any of the removed fixtures; the
conformance runner discovers fixtures by glob over the artifacts/ tree so
removing files simply shrinks the test set rather than breaking any
specific test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t#90)

Add an end-to-end configurable HTTP request timeout for provider API
calls, with provider-aware defaults, fixing pi_agent_rust#90 where
`pi --provider ollama ...` failed with "Request timed out" while Ollama
was still loading a multi-GB model into memory on a cold start.

The timeout bounds connect + request-write + first-response-header
latency for each provider request. `0` disables it entirely (unbounded).

Defaults are now provider-aware (src/http/client.rs):
- Remote/cloud providers: 60s (DEFAULT_REMOTE_REQUEST_TIMEOUT_SECS) —
  generous for any healthy cloud API.
- Local providers (Ollama, LM Studio, ...): 600s / 10 min — long enough
  to absorb realistic cold-start model loads from disk into RAM/VRAM,
  which the old flat 60s could not.

Three configuration surfaces, resolved through one path:
- CLI: `--request-timeout <SECONDS>` (src/cli.rs), bound by clap to the
  `PI_HTTP_REQUEST_TIMEOUT_SECS` env var so flag and env share storage.
- Env: `PI_HTTP_REQUEST_TIMEOUT_SECS` (REQUEST_TIMEOUT_ENV constant).
- Settings file: `request_timeout_secs` with serde aliases
  `requestTimeoutSecs` / `requestTimeoutSeconds` (src/config.rs), merged
  in Config::merge at the usual `.or(base)` precedence.

Precedence wiring (src/main.rs):
- Before any provider HTTP client is built, if `cli.request_timeout` is
  Some (flag or env), call
  `pi::http::client::set_request_timeout_override(secs)`.
- After config load, only if `cli.request_timeout` is None, apply
  `config.request_timeout_secs` — so flag/env beat the settings file,
  settings file beats the built-in provider-aware default.

Error UX (src/error_hints.rs):
- `api_hints` now detects "timed out"/"timeout" messages and returns a
  dedicated hint: how to raise the timeout via flag/env/settings (and
  that 0 disables it), plus the local-provider note to pull the model
  (`ollama pull <model>`) and verify the server is reachable
  (`ollama list`). Surfaces `url` and `timeout_seconds` context fields.

src/http/client.rs also drops the now-unused
`#[cfg(not(test))] use std::sync::OnceLock;` import in favor of the new
override storage / resolution helpers (`set_request_timeout_override`
has a real impl for non-test builds and a no-op `#[cfg(test)]` variant
so tests don't mutate global timeout state).

Behavior is unchanged for anyone who never sets a timeout on a cloud
provider (still 60s); local-provider users get the 600s default instead
of spurious timeouts, and everyone can now override explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflow the doc comment on the REQUEST_TIMEOUT_ENV constant into a summary
line plus a body paragraph, and fix the slightly garbled "This is also
the env clap binds ..." sentence to "Clap binds the --request-timeout CLI
flag and the requestTimeoutSecs setting to this same env var, so the three
configuration surfaces share a single resolution path."

Documentation only — the constant value (PI_HTTP_REQUEST_TIMEOUT_SECS)
and all behavior are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TUI started the textarea cursor-blink loop in init(), which fires a
BlinkMsg every ~530ms forever. Each tick repaints the whole alternate-screen
TUI — wasted CPU and, over SSH, wasted bandwidth, even when the user is idle
and the agent is doing nothing. TextArea::update also re-arms the blink via
blink_cmd() on every cursor movement, so simply not starting it in init() is
not enough on its own.

- init(): never start the blink loop (input_cmd = None). The cursor still
  renders solid-on — a focused cursor's blink state starts "shown" and we
  never toggle it — so input remains fully usable; we just don't blink.
- update_inner(): defensively drop InitialBlinkMsg / BlinkMsg /
  BlinkCanceledMsg before they reach the textarea, so the movement-triggered
  re-arm can never sustain a tick chain. Mirrors the existing SpinnerTickMsg
  drop just above.

This removes the periodic idle repaint and is the prerequisite for the
runtime to actually stay parked between events. NOTE: the larger idle-CPU
win — removing main.rs's `.enable_parking(false)` so worker threads sleep
instead of busy-spinning — is deliberately NOT done here: it guards against a
real lost-wakeup stall in the pinned asupersync 0.3.2 (Dekker-style Relaxed
atomics in WorkerCoordinator::wake_*, fixed only in unreleased commit
8b6e44824). Tracked in bd-rek8z; safe to flip once asupersync >0.3.2 ships.

Independently reimplemented after reviewing PR Dicklesworthstone#95.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the box (Dicklesworthstone#97)

`CopilotOAuthConfig::default()` left `client_id` empty, and all four call sites
read it from `GITHUB_COPILOT_CLIENT_ID` via `unwrap_or_default()` (empty when
unset). Both the browser and device flows then hard-fail on the empty client_id,
so Copilot login was impossible without the user first registering their own
GitHub App — exactly the "same error with the new version" Dicklesworthstone#97 reports (and the
real blocker behind the Dicklesworthstone#91 routing fix).

- Add `DEFAULT_COPILOT_CLIENT_ID` = the well-known public Copilot client id that
  GitHub's own editor integrations (copilot.vim, Copilot CLI) ship and that the
  wider ecosystem reuses. It is a non-secret public app id, identical for all
  users, so embedding it is safe and standard.
- Add `resolved_copilot_client_id()` (env override → public default) and route
  all four login sites through it; make it the `Default` client_id too.
- `GITHUB_COPILOT_CLIENT_ID` still overrides for Enterprise/custom-app setups.
- Update the stale device-flow routing doc + the default-config test; add a
  resolver fallback test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Includes the Dicklesworthstone#97 fix: GitHub Copilot login now works out of the box. The default
config ships the well-known public Copilot client id (overridable via
GITHUB_COPILOT_CLIENT_ID), so both browser and device flows succeed without the
user first registering their own GitHub App.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Formatting-only change from `cargo fmt`. Reindents the closure body in
`resolve_timeout`'s `unwrap_or_else` and wraps the long `let (status,
response_headers, stream, timeout_info) = if let Some(duration) =
resolved_timeout` binding across two lines. No logic change (verified via
`git diff -w`: a single line-wrap, no token changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-export of the beads issue database to JSONL. The diff is whole-file
re-serialization churn (record reordering/normalization) rather than
substantive issue content changes. Keeps the git-tracked JSONL ledger in
step with the local beads store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sworthstone#99)

The publish job runs only on a real (non-prerelease) release tag, but when the
CARGO_REGISTRY_TOKEN secret was unset it *silently skipped* `cargo publish` and
still reported success — which is how crates.io fell behind the repo (stuck at
0.1.13 while tags advanced to 0.1.17). Add a guard step that fails the release
with a clear error when the token is missing, so a misconfiguration is visible
instead of silently dropping the publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… feature (Dicklesworthstone#98)

SDK/library consumers (e.g. Jack-NimbleTron's pi-code-gui VSCode extension,
8.5k+ OpenVSX users) embed the `pi` library but do not want the interactive
terminal stack. Previously the crate unconditionally compiled crossterm and the
full charmed_rust stack (bubbletea/lipgloss/bubbles/glamour) even for pure
library use, bloating SDK build times and the dependency surface.

This adds a `tui` Cargo feature, included in `default` (and `full`), so the CLI
build is byte-for-byte unchanged for existing users — `cargo build` still
produces an identical `pi` binary with no action required. A
`--no-default-features` build now compiles the library WITHOUT the terminal
stack, which is the SDK contract the reporter asked for.

WHY each dependency was gated vs left shared (audited via `cargo tree`):
- Gated under `tui` (used exclusively by the interactive front-end): the four
  charmed_rust crates (charmed-bubbletea/-lipgloss/-bubbles/-glamour), plus the
  TUI-layout helpers unicode-width and textwrap. With `--no-default-features`
  these crates are entirely absent from the dependency graph.
- crossterm is marked `optional`/gated as our DIRECT dependency, but note it is
  still pulled in TRANSITIVELY by rich_rust; that is expected and correct.
- rich_rust stays unconditional: it backs plain styled stdout / markdown
  rendering in non-TUI code paths (src/tui.rs and elsewhere). The investigation
  flagged pulldown-cmark / fancy-regex / unicode-width as possibly shared — they
  are: each arrives transitively via rich_rust (and swc_common), so the library
  keeps them regardless of `tui`. Only our own direct TUI edges are gated.

Source gating (mirrors the existing `wasm-host` / `sqlite-sessions` pattern):
- `src/lib.rs`: `interactive` and `session_picker` modules are `#[cfg(feature =
  "tui")]` (both reach the charmed stack and are only used from the gated
  interactive surface / `main.rs`, which itself now carries
  `required-features = ["tui"]` so the bin builds by default but is correctly
  skipped under `--no-default-features` while the lib still compiles).
- `src/theme.rs`: the `Theme` data struct stays unconditional (resource/config
  loading depend on it); only the lipgloss/glamour-backed `TuiStyles` struct and
  the `tui_styles()` / `glamour_style_config()` render helpers (and their two
  smoke tests) are gated.
- `src/keybindings.rs`: the serde keybinding catalog stays available to library
  consumers; only `KeyBinding::from_bubbletea_key` (and the bubbletea-conversion
  test block, now a gated submodule) are gated.
- `src/session.rs`: `resume_with_picker` gates only the interactive
  `session_picker::pick_session` branch; without `tui` it falls through to the
  existing non-interactive resolution path.

CI: a new `library_no_default_features` job in ci.yml runs
`cargo check --no-default-features --lib`, locking in the SDK-can-compile
contract so it can never silently regress (matches the issue's proposed design).

Verification: `cargo check --all-targets` (default) and
`cargo check --no-default-features --lib` both pass clean; `cargo fmt --check`
clean; no new clippy findings in any changed file. Independent implementation of
the design proposed by Jack-NimbleTron in Dicklesworthstone#98 (we never merge outside PRs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First crates.io-published release since 0.1.13 (0.1.14–0.1.17 were tagged but
silently skipped by the publish workflow when CARGO_REGISTRY_TOKEN was unset;
that silent-skip is now a hard failure per 6352b8c). Ships the TUI feature-gate
(Dicklesworthstone#98) so SDK/library consumers can build without the terminal stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ry branch

Follow-up to the Dicklesworthstone#99 guard: GitHub parses ::error:: workflow commands from
stdout, not stderr, so the annotation now goes to stdout (it would not have
rendered in the run UI otherwise). Also removed the Summary step's now-unreachable
"Skipped - CARGO_REGISTRY_TOKEN not configured" branch — a missing token fails the
job at the guard before Summary runs, so that branch was dead and misleading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y providers (Dicklesworthstone#100)

append_upstream_nonlegacy_models unconditionally skipped any provider already in
the legacy catalog, so snapshot and models-override.json entries for the
native-adapter providers (openai-codex, github-copilot, google-gemini-cli,
google-antigravity) were silently dropped — even though they still showed up in
autocomplete and then failed to resolve. gitlab worked only because it's
non-legacy.

The legacy skip now admits a native-adapter legacy provider when it has a usable
seed default — either a non-empty seed base_url (openai-codex, gemini-cli,
antigravity) or a self-routing adapter (github-copilot resolves its own endpoint
via GitHub token exchange despite an empty seed base_url). azure-openai and
sap-ai-core stay excluded: their empty seed base_url genuinely needs a per-user
resource URL and would fail at request time. The post-legacy dedupe means only
genuinely-new ids are appended; curated legacy entries are untouched.

Fixes github-copilot/claude-opus-4.6 etc. from the bundled snapshot immediately
and enables models-override.json for all four providers. Regression test added;
snapshot additions for the three providers absent from the snapshot are a
follow-up (needs real upstream model ids).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…thstone#101)

On macOS arm64, `pi` startup spent 10-15s at 275%+ CPU inside
`SecTrustSettingsCopyTrustSettings` parsing the system cert trust plist.

Root cause was twofold:
1. The HTTP client built its TLS connector with `with_native_roots()`,
   which on macOS loads the OS trust store via Security.framework.
2. `Client::new()` is called from many hot paths (every provider
   constructor, the version check ~4x, etc.) and rebuilt the trust store
   from scratch every time — no caching.

Fix:
1. Switch the asupersync TLS feature from `tls-native-roots` to
   `tls-webpki-roots` (Cargo.toml) and the connector build from
   `.with_native_roots()` to `.with_webpki_roots()` (src/http/client.rs).
   In asupersync 0.3.2 `with_webpki_roots(self) -> Self` is infallible
   (unlike `with_native_roots(self) -> Result<Self, TlsError>`), so the
   former `.and_then(...)?`-style error handling is replaced by a direct
   builder chain; only `.build()` remains fallible.
2. Cache the built `TlsConnector` in a process-global `OnceLock` and clone
   it (cheap) on every `Client::new()`. The connector is now built exactly
   once per process. Per-call config (user_agent, vcr) is unchanged and
   still computed per `Client`, so public API/behavior is preserved.

Cargo.lock updated as a direct consequence of the feature swap: drops
security-framework, schannel, core-foundation, openssl-probe,
rustls-native-certs; adds webpki-roots.

Trade-off: bundled webpki (Mozilla) roots drop OS/enterprise custom root
certificates. This is acceptable for a CLI that talks to public LLM API
endpoints (Anthropic/OpenAI/Google/Cohere/Azure/etc.), all of which chain
to well-known public CAs.

Verified by compiling on Linux (cargo check --all-targets: exit 0;
cargo fmt --check: clean; all 93 http::client tests + 133 http-related
tests pass). The macOS perf win cannot be reproduced on Linux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hard =0.3.2 pin (and its dev-dependency twin) was a workaround
holding back from the broken 0.3.3 release. 0.3.3 shipped a compile
regression (E0277 in src/atp/policy/scope.rs under tls) and was yanked;
0.3.4 is the fixed release, so the pin is no longer needed. Move both
asupersync entries to 0.3.4 and refresh Cargo.lock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ache (Dicklesworthstone#103)

The system prompt embedded a per-second timestamp ("...June 9, 2026,
03:04:20 PM +03:00" in app.rs; "%Y-%m-%d %H:%M:%S UTC" in acp.rs). That
string is part of the cached system-prompt prefix, so a value that
changes every second invalidates the provider's prompt/KV cache on every
request — higher latency and cost for no benefit.

Both paths now emit date granularity only, keeping the prefix stable
within a day while still giving the model the current date. test_mode
still substitutes <TIMESTAMP>, so the golden corpus is unaffected.

Closes Dicklesworthstone#103

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… clippy drift (Dicklesworthstone#102)

ACP mode hardcoded `Session::in_memory()` and never received `--session-dir`,
so ACP sessions could not be persisted/resumed. AcpOptions now carries
`session_dir` (populated from `cli.session_dir`); `handle_session_new` builds the
session via a new `new_acp_session` helper that, when `--session-dir` is set,
persists to that directory using the configured store kind and enables autosave
(`save_enabled`) so the session is resumable via `pi --session`/`--resume`.
Without the flag, ACP keeps its existing in-memory, non-persisted behavior (the
`loadSession` capability stays false — this adds save, not ACP-side load). Helper
is unit-tested for both the persisted and in-memory paths.

Also resolves pre-existing nightly-clippy-drift that had been failing CI on the
last several pushes (a rolling-nightly toolchain started flagging these):
- interactive/commands.rs: `map(..).unwrap_or(false/true)` on Result → `is_ok_and`/`map_or`
- providers/model_fetch.rs: `unwrap_or(fn())` → `unwrap_or_else`; tighten the
  cache_lookup lock-guard lifetime (clone owned data, drop guard); split two
  over-long first doc paragraphs
- auth.rs (test): `map(..).unwrap_or(true)` → `map_or`
- main.rs: box the large `run_rpc_mode` future (clippy::large_futures)
- session.rs: make `SessionStoreKind::from_config` pub(crate) for acp.rs

cargo fmt + clippy --all-targets -D warnings clean; new ACP tests pass.

Closes Dicklesworthstone#102

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dicklesworthstone#104)

Running a local OpenAI-compatible server such as llama.cpp's `llama-server`
or mistral.rs failed with:

    Error: Provider error: llamacpp: Missing API key for provider.

even though, like ollama, these are LOCAL providers that need no API key.

Root cause (two parts):

1. `llamacpp` and `mistral.rs` were not registered providers at all. Only the
   cloud `mistral` (api.mistral.ai) and Meta `llama` (api.llama.com) entries
   existed, so `--provider llamacpp` could only work via a hand-written
   models.json entry and never out-of-the-box.

2. More importantly, the OpenAI-completions and OpenAI-responses request paths
   (`OpenAIProvider::stream` / `OpenAIResponsesProvider::stream`)
   *unconditionally* required an API key whenever no `Authorization` override
   was present — regardless of whether the provider is a keyless local one.
   ollama only "works" because users follow the docs and set a throwaway
   `"apiKey": "ollama"`; a genuinely keyless local provider still hit the
   missing-key error at stream time. The provider's `auth_header == false` /
   empty `auth_env_keys` was honored by the startup readiness gate
   (`model_requires_configured_credential`) but NOT by the actual request path.

Fix:

- Register `llamacpp` (aliases `llama-cpp`/`llama.cpp`/`llama-server`,
  default `http://127.0.0.1:8080/v1`) and `mistralrs`
  (aliases `mistral.rs`/`mistral-rs`, default `http://127.0.0.1:1234/v1`) in
  `PROVIDER_METADATA` as OpenAI-compatible local providers with empty
  `auth_env_keys` and `auth_header: false`, exactly mirroring `ollama`.
- Add `provider_is_keyless_local()`, derived from canonical metadata
  (empty `auth_env_keys` AND routing `auth_header == false`) rather than a
  hardcoded list, so it stays correct as new keyless local providers are added.
- In both `OpenAIProvider::stream` and `OpenAIResponsesProvider::stream`,
  when no key and no auth override are present, proceed WITHOUT an
  Authorization header for keyless local providers instead of erroring. Cloud
  / keyed providers still raise the missing-key error as before.

With this, `pi --provider llamacpp --model <id>` (and `--provider mistralrs`)
attempts a real connection to the local server instead of failing on auth.

Tests:

- `provider_metadata::tests::local_keyless_providers_registered_without_auth`,
  `local_keyless_provider_aliases_resolve`,
  `keyless_local_predicate_matches_local_providers_only` — pin the metadata +
  predicate (ollama + llamacpp + mistralrs are keyless-local; openai/anthropic/
  ollama-cloud/lmstudio are not).
- `models::tests::local_providers_synthesize_ready_keyless_entries` — the
  synthesized ad-hoc entry for these providers is ready without credentials.
- `openai::tests::test_stream_keyless_local_provider_does_not_require_key`
  and `test_stream_unknown_provider_without_key_still_errors` — the request
  path no longer raises the missing-key error for local providers, but still
  does for others. Both point at an unroutable address so they resolve fast
  and deterministically (observe the auth decision, not a full round-trip).

Docs: document the built-in keyless local providers in docs/models.md.

Co-Authored-By: Claude <noreply@anthropic.com>
The Azure-completions and Ollama config examples set `"api": "openai"`, but
`Api::from_str` doesn't recognize "openai" — it falls through to
`Api::Custom("openai")` (provider.rs:278), which routes incorrectly. The
intended OpenAI-compatible completions API is `"openai-completions"`.

Co-Authored-By: Claude <noreply@anthropic.com>
…icklesworthstone#105)

ACP mode previously had no handlers for `session/set_model` or
`session/set_config_option`, so clients (e.g. the Zed runtime in the
issue) that tried to set the model or reasoning effort dynamically right
after `session/new` got `Method not found: session/set_model` /
`session/set_config_option` and were stuck with whatever static config
the server started with. The underlying capability already existed
(`AgentSession::set_provider_model` / the SDK's `set_thinking_level`);
only the ACP JSON-RPC transport binding was missing — and the ACP
sessions were not even wired with a model registry, so a switch could
not have resolved a target model anyway.

What this does:

- `session/set_model`: accepts either an explicit `{provider, model}`
  pair or a bare model id via `model`/`modelId`/`value` (the issue's
  client sends `config=model value=gpt-5.5` with no provider). When only
  an id is given the provider is resolved from the model registry.
  Unknown models are rejected up front with an actionable error naming
  the requested provider/model. On success the live session's
  provider/model is switched in place and the new pair is returned.

- `session/set_config_option`: accepts `{name|key, value}`. The only
  runtime-settable option today is the reasoning/thinking effort, under
  the aliases thought_level / thinking_level / thinking / reasoning /
  effort / reasoning_effort (matching the issue's `thought_level=off` and
  `effort=off`). Values may be a string (off|minimal|low|medium|high|
  xhigh) or an integer 0..=4; the level is clamped to what the active
  model supports. Any other (or restart-only) option returns a
  structured INVALID_PARAMS error that names the option and the settable
  set — never a silent success and never a panic.

Runtime-vs-restart contract (documented in src/acp.rs): only the active
model and the thinking/effort level are mutable on a live ACP session.
Everything else (tool set, cwd, system prompt, compaction limits, image
handling) is fixed at `session/new` and requires a new session; the
config-option handler says so explicitly in its error.

Wiring:

- `AcpOptions` now carries the full `ModelRegistry` (not just the ready
  `available_models`); `handle_session_new` attaches it plus the auth
  storage to the `AgentSession` so `set_provider_model` can locate the
  target model and resolve its credentials/headers. Without this the
  switch path could only no-op on the already-active model.
- Added `AgentSession::set_thinking_level` (mirrors the SDK handle's
  logic: clamp, dedupe history, persist, refresh extension host state)
  so the ACP transport — which holds an `AgentSession`, not an SDK
  handle — can apply it directly. The SDK handle now delegates to it so
  the logic lives in one place and cannot drift between the two paths.

Both handlers reject missing `sessionId`, unknown sessions, bad params,
and refuse to mutate while a prompt turn is in flight (the
`agent_session` is taken out of the state during a turn) with a clear
retryable error. All other ACP methods are unchanged.

Tests (src/acp.rs): resolve_set_model_target (explicit pair, bare value
→ provider resolved, missing model, unknown model), parse_config_option
(all thinking aliases, numeric value, unknown option → structured error,
bad value), and end-to-end apply_set_model (switches provider/model) /
apply_set_config_option (applies thinking level) / apply_set_model
unknown-model (rejected, active model unchanged).

`cargo check --all-targets` and `cargo fmt --check` pass clean. Targeted
`cargo test --lib acp::` was running on a heavily overloaded remote build
host (load ~88 local; many concurrent agent builds) and had not finished
at commit time, so test confirmation rests on the clean all-targets
check plus the unit tests compiling in the test target. The only
remaining `cargo clippy -D warnings` findings are 19 pre-existing
`unused_async` errors in unrelated files (extensions.rs,
extension_dispatcher.rs, sdk.rs:781/1048) caused by floating-nightly
clippy drift — the crate already carries `#![allow(clippy::unused_async)]`
at lib.rs:25 and explicit per-fn allows that the current nightly no
longer honors; none are in the code changed here.

Co-Authored-By: Claude <noreply@anthropic.com>
An unpinned `nightly` channel let clippy drift across toolchains: each newer
nightly strengthens lints and stops honoring the crate's existing
`#![allow(clippy::unused_async)]`, turning `clippy --all-targets -D warnings`
red in CI with no code change. Pin to nightly-2026-02-19 — the same dated
nightly meta_skill and beads_rust pin, and the toolchain already active on the
build host — so the crate's allows are honored again and CI is reproducible.

Co-Authored-By: Claude <noreply@anthropic.com>
- feat(acp): session/set_model + session/set_config_option (Dicklesworthstone#105)
- fix(providers): keyless local providers llamacpp + mistral.rs (Dicklesworthstone#104)
- feat(acp): --session-dir ACP session persistence (Dicklesworthstone#102)
- docs(models): correct api "openai-completions" example
- ci: pin nightly-2026-02-19 to stop clippy drift

Co-Authored-By: Claude <noreply@anthropic.com>
Cargo.toml already declares version 0.1.19; regenerate the lockfile entry for
the pi_agent_rust package so the lock matches the published version (was still
pinned at 0.1.18).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… hint (Dicklesworthstone#106)

Layered Winsock providers on Windows (VPN clients, antivirus, firewall
LSPs) can report an outbound TCP connect as complete — `getpeername`
succeeds — while the base provider socket has not actually finished
connecting, so the first send fails with `WSAENOTCONN` (os error 10057).
The upstream asupersync peer_addr readiness probe (0.3.2+) is not
sufficient in those environments (pi_agent_rust#106, previously Dicklesworthstone#66 /
asupersync#35).

src/http/client.rs:
- Treat a "socket not connected" failure during connect/TLS handshake as
  transient and retry the whole connect with a fresh socket. New
  `is_retryable_not_connected` matches `ErrorKind::NotConnected` or raw os
  error 10057, and walks the error `source()` chain so a layered
  transport that re-wraps the original `io::Error` is still recognized.
  During an outbound connect this is never a legitimate terminal state
  (we just created the socket), so it is retried on every platform;
  refused/reset/DNS/certificate errors are deliberately not retried here.
  `NOT_CONNECTED_RETRY_BACKOFFS` gives three attempts total (initial +
  250ms + 750ms).

src/error_hints.rs:
- Add `winsock_not_connected_hints()` — an `ErrorHint` surfaced for the
  10057 / NotConnected signature (matched via both the error kind and the
  raw os error, including uncategorized kinds) explaining that pi already
  retried automatically and pointing at OS-level remediation
  (`netsh winsock reset`, disabling interfering VPN/AV/LSP). Tests cover
  the NotConnected-kind and raw-os-error-10057 mappings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…TCONN (Dicklesworthstone#106)

`is_retryable_not_connected` only walked the generic `Error::source` chain,
but `std::io::Error`'s `Error::source` intentionally returns the *inner's*
source — not the wrapped inner `io::Error` itself — so a "socket not
connected" error wrapped as `io::Error(io::Error(ENOTCONN/WSAENOTCONN))`
was never recognized and the connect was not retried.

Inspect both wrapping shapes: walk the `io::Error` -> `io::Error` chain via
`std::io::Error::get_ref` + `downcast_ref`, and separately walk the generic
`Error::source` chain for non-`io` wrappers (downcasting each link back to
`io::Error`). Either path matching `ErrorKind::NotConnected` / raw os error
10057 now triggers the fresh-socket retry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns ~200% CPU (Dicklesworthstone#107)

`pi --acp` busy-spun two cores at idle because the multi-thread asupersync runtime
was built with `.enable_parking(false)`. That flag was a deliberate workaround for
asupersync 0.3.2's Dekker-style lost-wakeup bug in WorkerCoordinator::wake_one/
wake_many (Relaxed atomics in scheduler/three_lane.rs) — without it, parked workers
could miss a wakeup, so they were kept spinning instead.

The upstream fix landed as asupersync 8b6e44824 (AcqRel ordering, in three_lane.rs
wake_one/wake_many) and is shipped in 0.3.4, which this workspace already depends on
(verified: the v0.3.4 tag contains 8b6e44824 and Cargo.lock pins the registry 0.3.4).
The bd-rek8z precondition ("re-enable parking once asupersync ships the AcqRel wake
fix > 0.3.2") is therefore satisfied, so the workaround is removed and parking
defaults back to true — idle workers sleep instead of spin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stone#108)

The CHANGELOG had drifted: its most recent versioned entry was v0.1.15
(plus an "[Unreleased]" block that actually described shipped v0.1.16
work), while git tags and releases had advanced to v0.1.20. Issue Dicklesworthstone#108
reports "Changelog is 0.1.15, commit history has 0.1.20."

Reconstructed accurate entries for each missing version directly from
git history between the tags (no invented changes), following the file's
existing format (## [vX.Y.Z] — DATE — Release/Tag-only, with Features /
Bug Fixes / Internal subsections and issue/PR cross-links):

- v0.1.16 (Release): converted the stale "[Unreleased]" block into the
  v0.1.16 entry (coding-plan provider selection + ad-hoc model/credential
  resolution + modernized default model ids — all committed between the
  v0.1.15 and v0.1.16 tags), and enriched it with the rest of that range:
  credential-loss auto-switch (Dicklesworthstone#81), network-unreachable hints (Dicklesworthstone#88),
  asupersync 0.3.2 bump fixing the post-session 500% CPU spin (Dicklesworthstone#83), the
  large QuickJS Node/Buffer-shim conformance push, and the swarm
  operations / autopilot tooling.
- v0.1.17 (Release): provider-aware HTTP timeout (Dicklesworthstone#90), dynamic model
  fetch w/ TTL cache + static fallback, Copilot login out-of-the-box
  (Dicklesworthstone#97), idle cursor-blink churn fix, kimi fallbacks, CI size budget.
- v0.1.18 (Release): TUI feature-gate for SDK/library consumers (Dicklesworthstone#98),
  fail-loud on missing CARGO_REGISTRY_TOKEN (Dicklesworthstone#99).
- v0.1.19 (Tag-only, no GitHub Release): ACP set_model/set_config_option
  (Dicklesworthstone#105) + ACP --session-dir (Dicklesworthstone#102), date-only system prompt for KV-cache
  stability (Dicklesworthstone#103), keyless llamacpp/mistral.rs (Dicklesworthstone#104), bundled TLS roots
  + cached connector (Dicklesworthstone#101), native-adapter snapshot/override entries
  (Dicklesworthstone#100), asupersync 0.3.4.
- v0.1.20 (Tag-only, no GitHub Release): Windows WSAENOTCONN connect
  retry + get_ref source-chain detection (Dicklesworthstone#106).

Dates use the GitHub Release publish date for published releases and the
release-commit date for the tag-only versions (v0.1.19/v0.1.20 have tags
but no GitHub Release, matching the file's "Tag-only" convention). Also
added compare-link references for v0.1.16–v0.1.20 and repointed the stale
[Unreleased] compare link from v0.1.9...HEAD to v0.1.20...HEAD.

Co-Authored-By: Claude <noreply@anthropic.com>
…orthstone#110)

Prompt turns built StreamOptions with max_tokens = None, so every provider
fell back to its hardcoded per-request default (4096 for openai-compat/azure/
cohere, 8192 for anthropic/gemini). The model registry's maxTokens was parsed
into Model.max_tokens but only used for display, never applied to the request.
In --mode rpc there was no way to raise it (no equivalent of the SDK's
set_max_tokens), so turns emitting large tool-call arguments — most visibly the
`write` tool — were truncated at finish_reason "length", deserializing to null
arguments ("invalid type: null, expected struct WriteInput").

Seed StreamOptions.max_tokens from the selected model's registry maxTokens at
every session-build site (app::build_stream_options for CLI/RPC, the SDK
builder, and the ACP path) and refresh it on every runtime model switch (RPC
set_model and the interactive /model command), mirroring how api_key/headers
are already propagated. This makes the maxTokens users configure in models.json
actually take effect across all providers and modes. SDK embedders can still
override per-call via set_max_tokens (including back to None for the provider
default), so that contract is preserved.

Updated the SDK test to assert the new seeded-from-registry default while
keeping the set/override/reset coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Closes Dicklesworthstone#112: `/mcp` was reported as "Unknown command". Pi has no
  config-file MCP client — MCP servers are only registered by JS extensions
  via registerMcpServer, and standalone config files (.agents/mcp.json,
  .pi/mcp.json, ~/.pi/agent/mcp.json) are intentionally not read. Added a real
  `/mcp` slash command (enum + parse + help + autocomplete + handler) that
  lists extension-registered MCP servers and explains this, instead of the
  misleading "unknown command".
- Dicklesworthstone#111: the WSAENOTCONN (os error 10057) fresh-socket retry classifier only
  inspected the direct `TlsError::Io` variant. Hardened
  is_retryable_not_connected_tls to also walk the std::error::Error source
  chain, so a "socket not connected" io::Error surfaced through a Rustls/
  Handshake variant (or wrapped) still triggers the retry. (Does not by itself
  resolve Dicklesworthstone#111 — that failure is environment-level Winsock/VPN/AV interference;
  left open pending reporter logs.)

Verified: cargo check --all-targets clean; new /mcp parse tests (15) and the
tls_io source-chain tests (4) pass.

Co-Authored-By: Claude <noreply@anthropic.com>
- Sync upstream changes through origin/main (v0.1.20)
- Resolve merge conflicts in Cargo.lock and src/providers/model_fetch.rs
- Update terraphim_router / terraphim_types path deps to local workspace layout
- Fix clippy lints in src/autocomplete.rs and src/interactive/commands.rs
GLM-5.1 and minimax-m2.7-highspeed are now the latest defaults after
the upstream model snapshot sync, so align the test expectations.
Drop local path dependencies for terraphim_router and terraphim_types
that pointed at divergent forks under ../terraphim/ on this machine.
Pin terraphim_automata and terraphim_types to =1.20.4, the coordinated
release that exports the MarkdownDirectives / Thesaurus / RouteDirective
types and the find_matches / parse_markdown_directives_dir functions
used by the pi_terraphim_router module.

The terraphim_router crate is not imported anywhere in
src/pi_terraphim_router/** or src/main.rs, so it is removed from the
dependency list. The terraphim-routing feature is narrowed to the two
crates the code actually uses.
…rates

Bring in the remote branch's pi_terraphim_router rewrite (KgRouter pattern
using terraphim_automata::find_matches and parse_markdown_directives_dir),
the readiness-aware fallback selection, the embedded planning /
implementation / review tier taxonomy, and the corresponding unit tests.

The remote's Cargo.toml still pointed at ../terraphim-ai/ path deps for
terraphim_router / terraphim_types / terraphim_automata; that sibling
checkout does not exist on this machine and the local terraphim-ai_main
fork is a divergent 1.2.x. The previous commit in this branch already
switched to crates.io =1.20.4 for terraphim_automata and terraphim_types,
which is the coordinated release that exports the MarkdownDirectives /
Thesaurus / RouteDirective types and the find_matches /
parse_markdown_directives_dir functions the rewrite depends on. Drop the
unused terraphim_router dep entirely and narrow the terraphim-routing
feature to the two crates the code actually uses.

Drop the remote branch's stray vendored crates/terraphim_settings data
file and .beads runtime artefacts (.local_version, daemon-error,
daemon.log); they were committed by mistake on the remote and are not
part of the rewrite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants