Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions bin/agentic-kit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ async function main() {
allowPositionals: true,
strict: false,
});

// Experimental host-adapter bootstrap (Wave 4, adapter door) — the single
// place every command passes through. Gated on the env var BEFORE anything
// else runs so the default (flag unset) is truly zero calls, zero output,
// zero behavior change: no dynamic import, no config read, nothing.
// Refusals are warnings on stderr, never fatal — a bad external adapter
// must never block a command that doesn't use it.
if (process.env.AK_EXPERIMENTAL_HOST_ADAPTERS === '1') {
try {
const { loadKitConfig } = await import('../src/lib/config.mjs');
const { bootstrapHostAdapters } = await import('../src/lib/adapters/admission.mjs');
const { warnings } = await bootstrapHostAdapters({ cfg: loadKitConfig(), env: process.env });
for (const w of warnings) {
console.error(dim(`⚠ host adapter '${w.name}' not admitted (${w.reason}): ${w.detail ?? ''}`.trimEnd()));
}
} catch { /* experimental surface — never blocks a command */ }
}

const code = await mod.run({ flags: values, positionals, pkgRoot: PKG_ROOT });

// Drift nudge: one line, cached, never blocks (skipped in --json contexts).
Expand Down
698 changes: 698 additions & 0 deletions docs/ADAPTER-CONTRACT-DOSSIER.html

Large diffs are not rendered by default.

1,022 changes: 1,022 additions & 0 deletions docs/HOST-EXTENSIBILITY-EXPLAINER.html

Large diffs are not rendered by default.

613 changes: 613 additions & 0 deletions docs/HOST-PROVIDER-CONSISTENCY.html

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions docs/HOST-SUPPORT.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Host support: Claude Code, Codex, and OpenCode

This is the canonical compatibility reference for the three execution hosts that
agentic-kit can manage. It compares the host itself, Ruflo, agentic-qe (AQE), and
This is the canonical compatibility reference for agentic-kit's three **built-in**
execution hosts. It compares the host itself, Ruflo, agentic-qe (AQE), and
RuvNet Brain without treating those independent layers as interchangeable.

Behind an experimental flag, agentic-kit can also admit **external host adapters**
that extend this set with a host not shipped in-tree — see
[External host adapters](PROVIDERS.md#external-host-adapters-experimental) and
[ADR-0029](adr/0029-host-adapter-extension-point.md). An admitted external host
picks up the same capability-driven treatment described here, but it is not one
of the three built-ins this reference compares, and it can never claim
primary-host, AQE-provider, or status-line status.

Evidence cutoff: **2026-08-04**. The comparison was checked against agentic-kit
`4.0.0-alpha.36`, Ruflo `3.34.0`, agentic-qe `3.13.x`, RuvNet Brain `4.0.7`,
Claude Code `2.1.222`, Codex CLI `0.146.0`, and OpenCode `1.18.x`. Host and
Expand Down
52 changes: 52 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,56 @@ checks every binding declared in `kit.json` and prints a warning naming any entr
unknown host, unknown provider, or unsupported transport — warnings only; nothing is changed
or removed on your behalf.

## Local OpenAI-compatible servers

Running a local model behind an OpenAI-compatible endpoint — MLX, LM Studio, `llama.cpp`, vLLM —
rather than Ollama? Declare it as a `local-openai` binding in `kit.json`:

```json
{
"integrations": {
"bindings": [
{
"id": "mlx-via-codex",
"host": "codex",
"provider": "local-openai",
"transport": "openai-compatible",
"endpoint": "http://127.0.0.1:8080/v1"
}
]
}
}
```

This gets you a named local inference target with `$0` billing and configured-grade provenance,
however the endpoint is served. `local-openai` is not an AQE provider type — `ollama` is. Loopback
`http://` is allowed; a remote endpoint requires `https://`; and the endpoint may never embed
credentials, fragments, or secret-bearing query parameters. See
[ADR-0028](adr/0028-local-openai-compatible-providers.md).

## External host adapters (experimental)

Want `ak` to manage a host CLI it doesn't ship in-tree — driving local models through something
like Hermes, say? Set `AK_EXPERIMENTAL_HOST_ADAPTERS=1` and declare it as **data**, never code:

```json
{
"hostAdapters": [
{ "name": "hermes", "source": "~/.config/ak/adapters/hermes.json", "contract": 1 }
]
}
```

An adapter is a manifest plus a handful of subprocess hooks — nothing an adapter declares ever
runs inside the `ak` process itself. Registering one asks you to confirm a content hash of the
manifest; edit the manifest afterward and that consent is invalidated until you confirm again. A
broken adapter is reported and skipped — it never takes down the hosts that already work.

An external adapter can never claim to be the primary host, an AQE provider, or the status-line
owner; those stay first-party. Nothing here installs itself: you declare the adapter, you consent
to it, and teardown remains reversible. See
[ADR-0029](adr/0029-host-adapter-extension-point.md).

---

## Level 0 — do nothing (the point)
Expand Down Expand Up @@ -340,5 +390,7 @@ just makes the good default automatic and the customization reversible.
[ADR-0006](adr/0006-primary-host-and-ambidextrous-mirroring.md).
- Capability-driven integration axes, bindings, and provenance:
[ADR-0016](adr/0016-capability-driven-integration-adapters.md).
- The generic local OpenAI-compatible provider: [ADR-0028](adr/0028-local-openai-compatible-providers.md).
- External host adapters (experimental): [ADR-0029](adr/0029-host-adapter-extension-point.md).
- Host env flags (`ENABLE_CLAUDE_CODE` / `ENABLE_CODEX`): upstream ruflo
ADR-034, "Optional MCP Backends".
8 changes: 6 additions & 2 deletions docs/TRANSCRIPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ The file is parsed with `withTurns: true` by the provider's parser
(`usage-index.mjs:1518-1545`) with the same fields the Sessions view rows
carry — `prompts`, `responses`, `exceptions`, `sidechain`, `threadSource`,
`models`, `tools`, `skill`/`plugin`, worktree — plus a `cost` priced from the
same per-model usage rows `aggregate()` uses (the header used to render a
hardcoded `$0.00`; the comment at the site records why).
same per-model usage rows `aggregate()` uses.

### 4.3 Mask, then truncate — both marked, differently

Expand Down Expand Up @@ -363,6 +362,11 @@ was wrong before, for the curious.
- **Session expander fields shipped but unrendered.** The per-session fields
§6.1's expander now renders (classification `basis` + confidence, the
token split, flags) once travelled on the wire and rendered nowhere.
- **Transcript header once showed a hardcoded `$0.00`.** `readSession`'s
assembled `meta` left `cost` undefined, and `fmtUsd(undefined)` renders the
truthy string `"$0.00"` — a fixed-looking zero on a panel whose whole
subject is cost. `meta.cost` is now priced via `sessionCost()` from the
same per-model usage rows `aggregate()` uses (`usage-index.mjs:1538-1542`).
- **Aggregate-side incidents** (the v4/v5 cache bumps, the Codex parsing
defects) are recorded in `USAGE-SCORECARD-METRICS.md` Appendix A.

Expand Down
11 changes: 9 additions & 2 deletions docs/adr/0016-capability-driven-integration-adapters.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# ADR-0016 — Capability-driven host, provider, binding, projection, and observability adapters

- **Status:** Accepted; compatibility clauses superseded by
[ADR-0020](0020-ga-stable-surfaces.md)
[ADR-0020](0020-ga-stable-surfaces.md); closed-registry clause superseded by
[ADR-0029](0029-host-adapter-extension-point.md)
- **Date:** 2026-07-28
- **Updated:** 2026-08-14
- **Updated:** 2026-08-15
- **Update note:** Added read-only Codex plugin-hook compatibility facts,
runtime-selected Ruflo project-memory store proofs, and the non-correlatable
OpenRouter account-analytics boundary; removed the pre-GA compatibility command,
Expand All @@ -18,6 +19,12 @@
warnings (F-16); and the integrations migrator derives each host's native
default provider from the provider registry's host-login entries instead of a
literal map, inferring no binding at all for hosts without one (F-13).
2026-08-15: [ADR-0029](0029-host-adapter-extension-point.md) supersedes §1's
closed-registry requirement — the registry admits an explicitly registered,
hash-pinned, subprocess-only external host adapter behind an experimental
flag; every other property that clause protected (zero-runtime-dependency,
offline-first normal operation, no in-process third-party code) remains
intact.
- **Deciders:** agentic-kit maintainers
- **Related:** [ADR-0001](0001-one-routing-policy-many-projections.md),
[ADR-0003](0003-auto-seed-dual-host-provenance.md),
Expand Down
178 changes: 178 additions & 0 deletions docs/adr/0028-local-openai-compatible-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# ADR-0028 — One generic local OpenAI-compatible provider, not a vendor enumeration

- **Status:** Accepted
- **Date:** 2026-08-11
- **Updated:** 2026-08-14
- **Update note:** Accepted with corrections after review of PR #131: the quoted Hermes
`api_mode: openai` value is annotated as invalid rather than reproduced as valid (F-30), and the
AQE-projection asymmetry between `ollama` and `local-openai` is now stated explicitly as
intentional (F-29). Implemented with in-tree projections `['ruflo', 'codex', 'opencode']`.
- **Deciders:** agentic-kit maintainers
- **Related:** [ADR-0011](0011-local-model-provenance-zero-cost-and-transcript-fidelity.md),
[ADR-0016](0016-capability-driven-integration-adapters.md),
[ADR-0021](0021-inference-provider-provenance.md)

Proposed by [@adrianco](https://github.com/adrianco) in
[PR #131](https://github.com/pacphi/agentic-kit/pull/131); accepted with the corrections recorded
below.

## Context

[ADR-0016](0016-capability-driven-integration-adapters.md) separates inference **providers** from
execution **hosts**, and [ADR-0011](0011-local-model-provenance-zero-cost-and-transcript-fidelity.md)
governs what a local provider may claim. The provider registry declared exactly **one** local
provider — `ollama` — and `BUILTIN_BINDINGS` carried exactly two local bindings,
`ollama-via-claude` and `ollama-via-codex` (`src/lib/adapters/registries.mjs`,
`src/lib/adapters/bindings.mjs`).

Ollama is not the only way a local model is served, and on real machines it is frequently not the
one in use. A local inference server is normally reached as an **OpenAI-compatible HTTP endpoint on
loopback**: MLX/`mlx_lm.server`, LM Studio, `llama.cpp`'s server, and vLLM all present that shape.
Nothing in ak could name such an endpoint as a provider, so a machine running one had its local
inference either invisible or misfiled.

This was observed on the proposer's reference machine. `~/.hermes/config.yaml` declared:

```yaml
provider: mlxlocal
providers:
mlxlocal:
api: http://127.0.0.1:8080/v1
api_mode: openai
default_model: mlx-community--Qwen3-Coder-Next-4bit
```

`api_mode: openai` is not a valid Hermes value — verified against `NousResearch/hermes-agent`
v0.20.0's `_parse_api_mode`, which silently **drops** an unrecognized value rather than raising.
The valid set is `{chat_completions, codex_responses, anthropic_messages, bedrock_converse,
codex_app_server}` (the newer key spelling is `transport`); `chat_completions` is the correct value
here, and it is also the default the parser falls back to, which is why the endpoint worked despite
the invalid setting. The quote above is reproduced verbatim because it is what the proposer's
machine observed — but `api_mode: openai` is not read as valid Hermes configuration by this ADR.

Two facts survive that correction. First, the endpoint is a plain OpenAI-compatible loopback URL —
the generic shape, not a vendor-specific protocol. Second, **the provider name is user-chosen**
(`mlxlocal`). No enumeration of vendor ids can cover that case; a registry that lists `mlx`,
`lmstudio`, `llamacpp`, and `vllm` still has no row for `mlxlocal`.

The binding machinery already accommodates this. `validateEndpoint` accepts loopback `http://`
while rejecting remote `http://`, embedded credentials, fragments, and secret-bearing query
parameters (`src/lib/adapters/config.mjs`). `http://127.0.0.1:8080/v1` was already a legal binding
endpoint; only the provider row was missing.

## Decision

### 1. Add one generic provider row: `local-openai`

A single provider represents "an OpenAI-compatible model server the user runs locally", regardless
of which program serves it:

- `billing: 'local'`, `credentials: { kind: 'none' }`, `capabilities.pricing: 'zero'` — required by
the registry's own construction invariants for a local provider (`validateRegistries`), and
correct: a loopback server bills nothing. A server that wants a placeholder token does not make
the credential *required*, so `kind: 'none'` remains accurate.
- `transports: ['openai-compatible']` — the only transport the row may claim. Anthropic-compatible
and native shells stay Ollama's, established separately.
- `capabilities.modelDiscovery: false`, `runtimeDiscovery: false`, `quota: false`,
`cacheAccounting: 'unknown'`. A generic endpoint exposes no catalogue ak may rely on. Claiming
`/v1/models` discovery would assert a uniformity across MLX, LM Studio, llama.cpp, and vLLM this
ADR has not measured.
- `observability: []`. Ollama keeps `ollama-catalog` / `ollama-runtime`; the generic row gets
neither, because it has no daemon API ak has verified.

`ollama` is unchanged. It keeps its richer transports and its two observability sources precisely
because those rest on a specific, known daemon.

### 2. The endpoint carries the identity; the provider row does not

Which program serves a `local-openai` binding is recorded as the **binding's** endpoint and model,
not as provider identity. A user running MLX on `:8080` and LM Studio on `:1234` has two bindings
against one provider — the same relation ADR-0011 already names for `ollama-via-claude` /
`ollama-via-codex`, one level more general.

Consistent with [ADR-0021](0021-inference-provider-provenance.md), such a binding establishes
**configured** provenance and nothing stronger. The endpoint is user-declared, so it may not be
displayed as observed, and it does not upgrade model, token, cache, or digest claims. The `$0`
claim is the one exception and is a property of the billing type, not of evidence about the run.

### 3. No built-in bindings for the generic provider

`BUILTIN_BINDINGS` gains nothing here. Ollama's two rows are justified by a fixed, well-known
default port; a generic local endpoint has no default ak may presume. Bindings are declared by the
user in `kit.json` and validated by the existing `assertValidBinding` path. "Local" is a billing
claim (user-run, `$0` — ADR-0011), not a topology constraint: a binding may name a user-run server
on another machine over `https`, while plain `http` remains loopback-only per `validateEndpoint`.

### 4. Replace the derived capability block with per-entry data

`providerEntries` previously derived capabilities from identity comparisons inside a `.map`
(`modelDiscovery: id === 'ollama'`, `pricing: id === 'ollama' ? 'zero' : …`). That construction does
not survive a second local provider: `local-openai` needs `pricing: 'zero'` without
`modelDiscovery`, which the `id === 'ollama'` coupling cannot express. Provider entries become
explicit records carrying their own capability block, matching how `hostEntries` is already
written.

### 5. `local-openai` projects to `['ruflo', 'codex', 'opencode']`, and is not an AQE provider type

The in-tree row declares projections `['ruflo', 'codex', 'opencode']`. Two omissions, both
deliberate:

- **No `'claude'` projection.** The row claims only the OpenAI-compatible transport; Claude's
projection expects an anthropic-compatible surface, which is Ollama's arrangement, not this
provider's.
- **No `'aqe'` projection — `local-openai` is not an AQE provider type; `ollama` is.** AQE's
provider set is upstream's own enumeration (`ollama`, `onnx` are its local types), not something
ak may extend by adding a row to its own registry. Projecting `local-openai` into AQE would
fabricate a provider identity AQE has never declared it understands. This is an intentional
asymmetry, not a bug: `ollama` gets AQE projection because AQE names it; `local-openai` does not,
because AQE does not. `ak status`'s provider surface reflects this distinction; surfacing it
clearly is a sibling work package's scope, not this ADR's.

## Consequences

- A machine serving models from MLX, LM Studio, llama.cpp, vLLM, or anything else speaking
OpenAI-compatible HTTP on loopback can be described to ak without a new ADR per vendor, and
without inventing a provider id the user did not choose.
- Every host that can be pointed at an OpenAI-compatible base URL gains a nameable local provider.
- The generic row deliberately supports **less** than `ollama`: no catalogue, no runtime probe, no
digest. Surfaces that show local-model detail for Ollama will show less for `local-openai`, and
that gap is the honest reading of the evidence, not a defect to paper over.
- `local-openai` is not an AQE provider type and is not projected as one. AQE's own local routing
(`ollama`, `onnx`) is a separate axis, defined upstream, and untouched by this ADR.

## Alternatives considered

- **Named rows per runtime (`mlx`, `lmstudio`, `llamacpp`, `vllm`).** Rejected for this revision on
two grounds. It cannot cover a user-named provider such as the observed `mlxlocal`, so the
generic row is required regardless and the named rows would be additive decoration. And each row
would assert transport and discovery facts for a server this repository has not measured —
precisely the derivation-without-measurement that
[docs/LOCAL-MODEL-VALIDATION.md](../LOCAL-MODEL-VALIDATION.md) exists to correct. Named rows
remain available later, gated on an evidence pass of the same kind, and would then be able to
claim real `/v1/models` discovery instead of guessing at it.
- **Extend `ollama` to mean "any local server".** Rejected: it would make an established provider
id lie about which daemon is answering, and `ollama-catalog` / `ollama-runtime` would be attached
to endpoints that serve neither.
- **Infer the runtime by probing the endpoint.** Rejected as a default: ak would be spawning
network probes during status collection to manufacture an identity claim that ADR-0021 would then
have to grade as inferred anyway. The user naming their own binding is cheaper and more honest.

## References

- `src/lib/adapters/registries.mjs` (`providerEntries`, `validateProviderAdapter`,
`validateRegistries` local-billing invariants), `src/lib/adapters/bindings.mjs`
(`BUILTIN_BINDINGS`, `assertValidBinding`), `src/lib/adapters/config.mjs` (`validateEndpoint`
loopback rule).
- ADR-0011 (local-model provenance, `$0`, transcript fidelity), ADR-0016 (provider/binding
separation), ADR-0021 (provenance is carried, never upgraded).
- Observed local configuration: `~/.hermes/config.yaml` on the proposer's reference machine
(`api: http://127.0.0.1:8080/v1`); the file's `api_mode: openai` is not a valid Hermes value (see
Context) and is not cited as correct usage.
- Hermes source verified for the `api_mode` correction: `NousResearch/hermes-agent` v0.20.0,
`_parse_api_mode`.
- [PR #131](https://github.com/pacphi/agentic-kit/pull/131) — original proposal, including
companion proposals for a host-adapter extension point (that PR's ADR-0029) and a Hermes reference
adapter (that PR's ADR-0030), neither adopted into this repository by this ADR.
- Tests: `tests/kit/adapter-registries.test.mjs` (deep-equality pin for the five pre-existing
providers, registry invariants for a second local provider, binding validation against a
loopback OpenAI-compatible endpoint).
Loading
Loading