@tekmidian/pai 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
- package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
- package/dist/{chain-CTtHligI.mjs → chain-DhVHVnmT.mjs} +263 -791
- package/dist/chain-DhVHVnmT.mjs.map +1 -0
- package/dist/cli/index.mjs +10 -8
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +10 -8
- package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
- package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
- package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
- package/dist/config-D1G9IFpn.mjs.map +1 -0
- package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
- package/dist/config-Dl8lT4Lu.mjs +527 -0
- package/dist/config-Dl8lT4Lu.mjs.map +1 -0
- package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
- package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
- package/dist/daemon/index.mjs +12 -10
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BSL9sXxm.mjs +22 -0
- package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
- package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +127 -6
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/detector-BLVOCiJ_.mjs +5 -0
- package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
- package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
- package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
- package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
- package/dist/factory-C7ua_3ik.mjs +3 -0
- package/dist/{providers-FYkZjn_C.mjs → fallback-CdXv-Np0.mjs} +411 -51
- package/dist/fallback-CdXv-Np0.mjs.map +1 -0
- package/dist/hooks/context-compression-hook.mjs +136 -22
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/post-compact-inject.mjs +98 -7
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +75 -6
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs +75 -6
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +149 -90
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
- package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
- package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
- package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
- package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
- package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
- package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
- package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
- package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
- package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
- package/dist/main-resolver-E5WlilhM.mjs +6 -0
- package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
- package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
- package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
- package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
- package/dist/{planner-BDI7bE9B.mjs → planner-Cm3g6fWH.mjs} +5 -3
- package/dist/{planner-BDI7bE9B.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
- package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
- package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
- package/dist/{program-DXBwJV7h.mjs → program-AR0qlYRg.mjs} +334 -78
- package/dist/program-AR0qlYRg.mjs.map +1 -0
- package/dist/query-feedback-CGcEiGtY.mjs +3 -0
- package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
- package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
- package/dist/router-CAtyVQud.mjs +3 -0
- package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
- package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
- package/dist/server-BOuAOj9b.mjs +386 -0
- package/dist/server-BOuAOj9b.mjs.map +1 -0
- package/dist/skills/Worker/SKILL.md +19 -1
- package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
- package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
- package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
- package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
- package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
- package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
- package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
- package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
- package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
- package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
- package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
- package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
- package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
- package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
- package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
- package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
- package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
- package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
- package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
- package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
- package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
- package/docs/commands/README.md +5 -2
- package/docs/commands/worker.md +66 -5
- package/docs/images/provider-abstraction-live.png +0 -0
- package/docs/images/workers.png +0 -0
- package/docs/launcher-independence.md +70 -0
- package/docs/provider-abstraction.md +87 -0
- package/docs/provider-independence-details.md +330 -0
- package/docs/provider-independence.md +75 -0
- package/docs/worker.md +72 -11
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +6 -1
- package/scripts/build-skill-stubs.mjs +17 -4
- package/src/hooks/ts/lib/context-fill.test.ts +158 -4
- package/src/hooks/ts/lib/context-fill.ts +102 -35
- package/statusline-command.sh +120 -21
- package/dist/chain-CTtHligI.mjs.map +0 -1
- package/dist/config-B64vFg14.mjs.map +0 -1
- package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
- package/dist/daemon-CmHHmKpg.mjs +0 -20
- package/dist/detector-DO730Zq0.mjs +0 -5
- package/dist/factory-BD-np0Vd.mjs +0 -3
- package/dist/main-resolver-D6IImXvF.mjs +0 -4
- package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
- package/dist/program-DXBwJV7h.mjs.map +0 -1
- package/dist/providers-FYkZjn_C.mjs.map +0 -1
- package/dist/query-feedback-DSVyHtrG.mjs +0 -3
- package/dist/router-Bk77E7hj.mjs +0 -3
- package/dist/runtime-paths-rni52zHX.mjs +0 -50
- package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
- package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
- package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Launcher Independence
|
|
2
|
+
|
|
3
|
+
> Verified 2026-09-17: the `pai` session launcher starts sessions on whichever
|
|
4
|
+
> worker provider is active — the claude binary, Anthropic credentials and
|
|
5
|
+
> `~/.claude` session state are needed only where a claude-format transcript is
|
|
6
|
+
> genuinely being resumed, and nowhere else.
|
|
7
|
+
|
|
8
|
+
## The two launch paths
|
|
9
|
+
|
|
10
|
+
| Invocation | Code path | Engine decision |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `pai` (bare, tty) | `cmdPick` → `launchInDir(dir, name, { forceFresh: true, engine })` | `resolveLaunchRoute` (src/cli/lib/launch.ts) — active provider wins; `w`/`a` keys force one engine |
|
|
13
|
+
| `pai <name>` | `cmdMain` → `openMatch` / registry match → `launchSession` → **`launchInDir`** | same `resolveLaunchRoute` — fresh launch follows the provider; a resumable transcript pins claude |
|
|
14
|
+
|
|
15
|
+
`resolveLaunchRoute` mirrors the Agent-routing hook's gate: routing on = one
|
|
16
|
+
interactive `pai worker run --label <name> --cwd <dir>`; routing off, no active
|
|
17
|
+
provider, or an explicit override = `claude`. Resume stays claude by design — a
|
|
18
|
+
claude transcript has nothing to resume on another engine.
|
|
19
|
+
|
|
20
|
+
## The gap that was found and fixed
|
|
21
|
+
|
|
22
|
+
`launchSession` in `src/cli/commands/main-resolver.ts` spawned `claude` directly
|
|
23
|
+
in all three of its branches (resume, resume-fallback, fresh). Every other
|
|
24
|
+
launch path had been routed (6adddb5), but this one pinned `pai <name>`,
|
|
25
|
+
`pai <uuid>` and history-search launches to Anthropic credentials regardless of
|
|
26
|
+
configuration. It was also a third near-copy of the launch dance — the exact
|
|
27
|
+
shape that once left `probeResume` fixed in one copy and broken in two.
|
|
28
|
+
|
|
29
|
+
Fix: `launchSession` now resolves the resumable UUID, directory and display
|
|
30
|
+
name, then hands the launch to `launchInDir`. One router, one spawn site per
|
|
31
|
+
engine. Verified:
|
|
32
|
+
|
|
33
|
+
- **Before/after dry-run** (topic match over prompt history, non-resumable
|
|
34
|
+
session, `env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL pai <topic> <pick> --dry-run`):
|
|
35
|
+
before → `argv: claude --name …` (would fail to authenticate with the env
|
|
36
|
+
stripped); after → `argv: pai worker run --label … --cwd …`.
|
|
37
|
+
- **Resume path unchanged**: the same dry-run against a resumable session still
|
|
38
|
+
prints the probe + `claude --resume` plan — through `launchInDir` now.
|
|
39
|
+
- **Unit tests**: `src/cli/lib/launch.test.ts` covers every `resolveLaunchRoute`
|
|
40
|
+
outcome (provider active/off, empty provider map, override keys, resume pins
|
|
41
|
+
claude, forceFresh drops it) and that `probeResume` exists in exactly one
|
|
42
|
+
file; `main-resolver.test.ts` still passes.
|
|
43
|
+
|
|
44
|
+
## Why the provider needs nothing from Anthropic
|
|
45
|
+
|
|
46
|
+
`buildRunEnv` (src/workers/run-env.ts) builds the worker child's environment
|
|
47
|
+
from the provider config alone: token from the provider's key file,
|
|
48
|
+
`ANTHROPIC_BASE_URL` from its `baseUrl`, model overrides from its model map —
|
|
49
|
+
and it **deletes `ANTHROPIC_API_KEY`** and disables nonessential traffic. The
|
|
50
|
+
`claude` binary serves as the agent runtime, but authentication is entirely the
|
|
51
|
+
provider's.
|
|
52
|
+
|
|
53
|
+
Verified live: a headless `pai worker run` with
|
|
54
|
+
`ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`/`ANTHROPIC_BASE_URL` unset completed
|
|
55
|
+
and returned its reply — the active provider (configuration, nothing else)
|
|
56
|
+
supplied the credentials. `src/workers/daemon-llm.test.ts` asserts the same
|
|
57
|
+
rule for background spawns: the API key never reaches the child, the provider
|
|
58
|
+
token and base URL do.
|
|
59
|
+
|
|
60
|
+
## What still touches claude-only state, deliberately
|
|
61
|
+
|
|
62
|
+
- **Resume** (`claude --resume <uuid>`, `pai resume`): the transcript format and
|
|
63
|
+
the probe's filesystem layout (`~/.claude/projects/<encoded-cwd>/`) are
|
|
64
|
+
claude's; the probe itself is read-only and spawns nothing.
|
|
65
|
+
- **Explicit claude** (`a` in the picker, `engine: "claude"`).
|
|
66
|
+
- **Routing off** (`pai worker off`): everything runs claude, as before.
|
|
67
|
+
|
|
68
|
+
Switching providers (`pai worker providers use <name>`) changes every fresh
|
|
69
|
+
launch — picker, name match, UUID, history search — without touching any of
|
|
70
|
+
these files again.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Provider Abstraction
|
|
2
|
+
|
|
3
|
+
## What this layer achieves
|
|
4
|
+
|
|
5
|
+
The entire assistant stack — interactive sessions, workers, follow panes, hooks, MCP tools and the statusline — runs on **any provider that speaks the Anthropic Messages protocol**, not just Anthropic. This has been demonstrated end-to-end on a third-party `glm` endpoint serving a 1M-context model: the interactive session, every delegated worker, the daemon-side background calls (summaries, context handovers, knowledge-graph extraction) and all instrumentation run against that provider, with nothing falling back to a vendor login.
|
|
6
|
+
|
|
7
|
+
What that buys:
|
|
8
|
+
|
|
9
|
+
- **No single-vendor lock-in.** Providers are entries in a registry (`src/workers/config.ts`): protocol (`anthropic`/`openai`), engine (`claude`/`codex`), model ids, key files, cost tiers, capability tags, quota probes. Switching is `pai worker providers use <name>`; disabling routing entirely is `pai worker off`.
|
|
10
|
+
- **Plan/quota-aware instrumentation.** The statusline reads the *active provider's* real plan utilization (5-hour rolling window and weekly quota with reset times), not a hardcoded vendor endpoint.
|
|
11
|
+
- **Cost routing per task class.** Work is dispatched by class (`draft`, `spotcheck`, `complex`, …); each class maps to a provider and model, so cheap work lands on cheap models without touching code.
|
|
12
|
+
- **Provider swaps are configuration, not surgery.** Every seam that used to assume one vendor — daemon LLM spawns, compaction-history family detection, context-window defaults, tier aliases — has been patched at protocol level (see below), so a new provider is a registry entry plus a key file.
|
|
13
|
+
|
|
14
|
+
OpenAI-protocol providers are reached through the translating proxy (`src/workers/proxy/`), and the Codex engine (`src/workers/codex.ts`) normalizes its events into the same shape — both are first-class in the registry.
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
### Worker provider/model routing
|
|
19
|
+
|
|
20
|
+
`pai worker run --class <class>` picks the provider and model for a job by task class. The standard classes (`WORKER_CLASSES`, `src/workers/config.ts:126`): `draft`, `plan`, `implement`, `review`, `research`, `spotcheck`, `simple`, `complex`. `--provider` and `--model` override for a single call.
|
|
21
|
+
|
|
22
|
+
Model ids are configuration, not code. Two surfaces manage them:
|
|
23
|
+
|
|
24
|
+
- **CLI** — `pai worker model` (`src/cli/commands/worker/model.ts`): no args lists the active provider with its `default` and `fast` model ids; `model <id>` sets the default; `model fast <id>` sets the fast slot; `--provider` targets a non-active provider.
|
|
25
|
+
- **MCP** — the `worker_model` tool (`src/daemon-mcp/tools/worker-model.ts`): `action: get|set`, `slot: default|fast`, optional `provider`.
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
$ pai worker model
|
|
29
|
+
glm (active)
|
|
30
|
+
default: glm-5.3[1m]
|
|
31
|
+
fast: glm-5.3-flash
|
|
32
|
+
|
|
33
|
+
$ pai worker model glm-5.4
|
|
34
|
+
$ pai worker model fast glm-5.4-flash --provider glm
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The `fast` slot is what cheap classes resolve to; the runner also honors the platform's tier aliases by pinning `ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL` to the provider's models in the spawn environment (`src/workers/run-env.ts`).
|
|
38
|
+
|
|
39
|
+
### Provider-aware statusline
|
|
40
|
+
|
|
41
|
+
`statusline-command.sh` derives everything it can from the session's model id instead of assuming one vendor:
|
|
42
|
+
|
|
43
|
+
- **Plan utilization** — glm sessions fetch the provider's plan quota (5-hour window and weekly credit window, `nextResetTime` rendered as reset times); claude models keep the existing Anthropic OAuth path. Each source has its own 60-second cache. On fetch failure the provider windows show `?` rather than silently falling back to another plan's numbers. `PAI_ZAI_QUOTA_URL` overrides the endpoint (kill switch / testing).
|
|
44
|
+
- **Context window from the model id** — a `[1m]` variant suffix means a 1,000,000-token window (`statusline-command.sh:126`); ids with no marker keep the previous default. The same derivation lives in TypeScript as `contextWindowFromModelId()` (`src/utils/model-window.ts`), so the session hooks and the worker meter agree with the statusline.
|
|
45
|
+
- **`(N% left)`** — remaining context until auto-compact, computed against that derived window.
|
|
46
|
+
- **Worker row** — line 4 lists the workers this session launched, running ones with their current step (`pai worker status-line`, `statusline-command.sh:634`).
|
|
47
|
+
|
|
48
|
+
### Seam patches
|
|
49
|
+
|
|
50
|
+
The seams that previously hardcoded one vendor, now patched:
|
|
51
|
+
|
|
52
|
+
- **Daemon LLM spawns route through the provider registry** — `src/workers/daemon-llm.ts` builds the spawn plan for session summaries (`src/daemon/session-summary-worker.ts`), context handovers (`src/daemon/context-handover-worker.ts`) and KG extraction (`src/memory/kg-extraction.ts`) via `buildRunEnv()` (`src/workers/run-env.ts`): provider base URL and token, the provider's concrete model id instead of a tier alias, and class-appropriate timeouts. With no provider configured it falls back to the historical bare-alias behaviour minus the vendor API key.
|
|
53
|
+
- **Compaction-history family detection accepts any provider model** — `modelFamily()` (`src/hooks/ts/lib/context-fill.ts:379`) derives a stable family key from the model id itself, stripping any bracketed variant suffix (`glm-5.3[1m]` → `glm-5.3`), instead of allowlisting `claude-*` and discarding everything else as "foreign". A provider-model session keeps its measured compaction history.
|
|
54
|
+
- **Context-window defaults derived from the model id** — `src/utils/model-window.ts` is the single helper; consumers are the session hooks (`context-fill.ts`) and the provider schema default (`src/workers/config.ts`). The statusline derives its window the same way, instead of writing a single assumed size into the state file for every model it does not recognize.
|
|
55
|
+
- **Tier aliases map provider models into the cheap/standard tiers** — `modelToClass()` (`src/workers/agents.ts`) resolves an agent's model through the provider registry (`fast` → cheap tier, `default` → standard) and logs once, then falls back to the standard class, instead of silently dropping the hint for unknown ids.
|
|
56
|
+
|
|
57
|
+
### Delegation instruments
|
|
58
|
+
|
|
59
|
+
- **Agent-tool gate** — `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts` (PreToolUse, Agent matcher) denies in-process subagents whenever worker providers are configured, so no delegated work runs on the vendor login; the deny reason tells the orchestrator to use `pai worker run` instead. `ALLOW_ANTHROPIC_AGENTS=1` bypasses it for one session. Every decision is appended to the routing ledger (`pai worker log`).
|
|
60
|
+
- **Worker exemption marker** — every worker spawn carries `PAI_WORKER=1` (`src/workers/run-env.ts:47`; `src/workers/codex.ts:50` for the Codex engine). `isWorkerSession()` (`src/hooks/ts/lib/worker-session.ts`) makes all per-session hooks (autosave, stop bookkeeping, compaction state) skip disposable workers, so the gate and the bookkeeping never fight the workers themselves.
|
|
61
|
+
- **Post-compaction reinjection** — `src/hooks/ts/session-start/post-compact-inject.ts` (SessionStart, matcher `compact`) replays the state the PreCompact hook saved, so a compacted orchestrator session resumes with its context intact. The live worker list stays visible in the statusline's worker row and via `pai worker ps` / `pai worker follow <id>`.
|
|
62
|
+
|
|
63
|
+
### Session-level instruments (deployed config)
|
|
64
|
+
|
|
65
|
+
Two further instruments are deployed configuration in the PAI config dir rather than repo code, so a tree-only audit misses them:
|
|
66
|
+
|
|
67
|
+
- **Edit-delegation guard** — a PreToolUse hook (`route-edits-to-worker`) blocks `Edit`/`MultiEdit`/`Write` on files inside any git work tree for orchestrator sessions, while worker sessions are exempt via the `PAI_WORKER=1` marker set on every worker spawn. Code changes route through workers mechanically.
|
|
68
|
+
- **Compact recovery** — on `SessionStart(compact)` a hook injects the live `pai worker ps --all` output into the session context, so a session resuming from compaction immediately sees every running worker.
|
|
69
|
+
|
|
70
|
+
### Live worker panes
|
|
71
|
+
|
|
72
|
+
`pai worker follow` (`src/cli/commands/worker/index.ts:165`) attaches a chat pane to a worker, rendered by `src/workers/viewer.ts`, `pane.ts`, `render.ts` and `chatui.ts`:
|
|
73
|
+
|
|
74
|
+
- **Transcript with stamped gutter** — every event line carries a dim `HH:MM:SS │ ` stamp from the event's own timestamp, with continuation padding for wrapped text (`render.ts:135`); the pane's bar runs unbroken down its full height (`render.ts:139`).
|
|
75
|
+
- **Prompt row** — `› ` with readline line editing. Plain text is *said* to a running worker; `/resume <text>` continues a finished one; `/status` prints a one-line status (`chatui.ts`).
|
|
76
|
+
- **Status ticker** — a fixed bottom row showing elapsed time, current intent and tool (`$ bun run test` …) plus the worker's provider/model and context meter (`tickerText()`, `render.ts:183`; meter data from `src/workers/status.ts`, whose window comes from the run's init event or the provider default). A finished worker freezes the row with ✓/✗.
|
|
77
|
+
- **Resize-safe redraw** — on terminal resize the viewer re-reads the geometry, clears the stale transcript and refills it from events (`viewer.ts:735`), so the fixed prompt/ticker rows and the scroll-region transcript stay consistent at any size.
|
|
78
|
+
|
|
79
|
+
## In action
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
## Current limits
|
|
84
|
+
|
|
85
|
+
The layer is proven against one non-Anthropic provider so far (the `glm` endpoint); other Anthropic-protocol providers are expected to work but have not been exercised end-to-end, and OpenAI-protocol providers additionally depend on the translating proxy. Instrumentation still special-cases per-provider quota endpoints as providers are added. The seam audit that drove these patches lives outside this repository; new seams are patched as they surface, and each patch carries a unit test pinning the provider-neutral behaviour (`src/workers/daemon-llm.test.ts`, `src/workers/model.test.ts`, `src/workers/agents.test.ts`, `src/hooks/ts/lib/context-fill.test.ts`).
|
|
86
|
+
|
|
87
|
+
For the full worker command reference see [worker.md](worker.md) and [commands/worker.md](commands/worker.md).
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Provider Independence — Details
|
|
2
|
+
|
|
3
|
+
Everything technical about provider independence: what switches, the tools,
|
|
4
|
+
the configuration, the internals. The human-facing page is
|
|
5
|
+
[provider-independence.md](provider-independence.md).
|
|
6
|
+
|
|
7
|
+
## Switch what?
|
|
8
|
+
|
|
9
|
+
Two levels, one provider registry. Both switch by sentence or by command —
|
|
10
|
+
never by editing code:
|
|
11
|
+
|
|
12
|
+
| Level | What runs on the provider | Switch with |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| The crew | delegated workers — research, drafting, implementation, review, spotchecks | `worker_providers` (use): "Use glm for the workers from now on." |
|
|
15
|
+
| The orchestrator | the session itself — the interactive chat you are typing into | start it with `glm` (the shim for `pai worker run`), or pick a project with `pai` — the picker follows the active provider |
|
|
16
|
+
|
|
17
|
+
The main session orchestrates, reviews and merges; workers do the work —
|
|
18
|
+
unless the session itself runs on a provider too, in which case everything is
|
|
19
|
+
off the Anthropic account.
|
|
20
|
+
|
|
21
|
+
### The crew — `worker_providers` add / use
|
|
22
|
+
|
|
23
|
+
- **Add once.** "Add a worker provider named glm, base URL <url>, model
|
|
24
|
+
<model>, here is the key: <token>" → `worker_providers` (add). The key is
|
|
25
|
+
parked in `~/.config/pai/keys/<name>` (mode 0600); the config records only
|
|
26
|
+
the path. OpenAI-compatible endpoint? Say "upstream URL" — PAI routes it
|
|
27
|
+
through its local translating proxy. The first provider added also turns
|
|
28
|
+
worker routing on and seeds the nine classes (`addProvider`,
|
|
29
|
+
`src/workers/providers.ts:51`); from then a PreToolUse hook denies every
|
|
30
|
+
in-process subagent.
|
|
31
|
+
- **Use.** "Switch the fleet to glm." → `worker_providers` (use): sets
|
|
32
|
+
`workers.active` (`useProvider`, `src/workers/providers.ts:170`); classes
|
|
33
|
+
without an explicit target follow.
|
|
34
|
+
- **Test.** "Is glm alive?" → `worker_providers` (test): a one-word pong
|
|
35
|
+
probe, latency, OK/FAILED.
|
|
36
|
+
- **Off.** "Turn workers off." → `worker_toggle`: subagents run on Anthropic
|
|
37
|
+
again; the registry is kept.
|
|
38
|
+
|
|
39
|
+
### The orchestrator — the session itself
|
|
40
|
+
|
|
41
|
+
There is no in-session switch: a session runs on a provider by being
|
|
42
|
+
**started that way**. The launcher is `glm` — a shim in `~/.local/bin`
|
|
43
|
+
written by `pai worker install` (`src/workers/install.ts:119` body
|
|
44
|
+
`exec pai worker run "$@"`; installed names `glm`, `glm-run`, `glm-ps`,
|
|
45
|
+
`glm-log`, `worker-say`, `:125`).
|
|
46
|
+
|
|
47
|
+
`pai worker run` without `-p` starts an **interactive** Claude Code session:
|
|
48
|
+
it spawns `claude` with the terminal inherited (`stdio: "inherit"`,
|
|
49
|
+
`src/workers/run.ts:452`) — that process *is* the chat pane, not a subagent
|
|
50
|
+
of it (`origin: "chat"`, `run.ts:394`). No MCP restriction applies.
|
|
51
|
+
|
|
52
|
+
The `pai` project picker follows the active provider the same way: its
|
|
53
|
+
launch keys start an interactive worker run in the chosen directory
|
|
54
|
+
(`w` forces it, `a` forces plain `claude`).
|
|
55
|
+
|
|
56
|
+
What actually puts it on the provider is the spawn environment
|
|
57
|
+
(`buildRunEnv`, `src/workers/run-env.ts`):
|
|
58
|
+
|
|
59
|
+
| Env var | Value | Where |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `ANTHROPIC_API_KEY` | deleted — the Anthropic key never reaches a provider session | `run-env.ts:19` |
|
|
62
|
+
| `ANTHROPIC_BASE_URL` | the provider's base URL; the local proxy's URL for OpenAI-protocol providers | `run-env.ts:25,37` |
|
|
63
|
+
| `ANTHROPIC_AUTH_TOKEN` | the token read from the provider's key file (proxy runs: placeholder) | `run-env.ts:40` |
|
|
64
|
+
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` / `_SONNET_MODEL` / `_OPUS_MODEL` | the provider's fast/default models — every model slot maps to the provider | `run-env.ts:41-43` |
|
|
65
|
+
| `ENABLE_TOOL_SEARCH` | `true` in interactive runs; headless runs get `PAI_WORKER=1` instead | `run-env.ts:47-49` |
|
|
66
|
+
|
|
67
|
+
- **Model.** `--model <provider default>` is passed unless you name one
|
|
68
|
+
(`run.ts:438-439`). A `[1m]` suffix on the model id (`glm-5.3[1m]`) selects
|
|
69
|
+
the 1,000,000-token window variant: the suffix is read wherever a window is
|
|
70
|
+
needed (`contextWindowFromModelId()`, `src/utils/model-window.ts:33`) and
|
|
71
|
+
stripped for the family name (`stripModelVariant()`, `:19`).
|
|
72
|
+
- **Machine-wide instead of per-session.** `worker_fallback` (on) writes the
|
|
73
|
+
same env into the `env` block of `~/.claude/settings.json`
|
|
74
|
+
(`src/workers/fallback.ts:90-94`; path override `CLAUDE_SETTINGS_PATH` for
|
|
75
|
+
dry runs, `:46`), so every NEW Claude Code process — interactive,
|
|
76
|
+
task-bus, daemon summarizer — runs on the provider. `off` restores the
|
|
77
|
+
saved previous state. Running sessions keep their provider until
|
|
78
|
+
restarted; worker routing is unchanged while on.
|
|
79
|
+
- **Bypass.** `ALLOW_ANTHROPIC_AGENTS=1` in the environment re-enables the
|
|
80
|
+
Agent tool for one session.
|
|
81
|
+
|
|
82
|
+
## Tools (MCP)
|
|
83
|
+
|
|
84
|
+
Everything here happens in chat: you say the sentence, the session calls the
|
|
85
|
+
tool — you never name the tool yourself. These are the **twelve `worker_*`
|
|
86
|
+
tools** the pai MCP server registers (`src/daemon-mcp/index.ts:804-1334`);
|
|
87
|
+
there is no worker surface outside them. Parameter names are the schema's
|
|
88
|
+
own.
|
|
89
|
+
|
|
90
|
+
### At a glance
|
|
91
|
+
|
|
92
|
+
| Tool | You say | What it does |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `worker_status` | "How is the worker system doing?" | On/off, active provider, providers, today's run tally, running workers with waiting handoffs |
|
|
95
|
+
| `worker_providers` | "Switch the workers to glm." | Manage providers: list / add / update / remove / use / enable / disable / test |
|
|
96
|
+
| `worker_fallback` | "Run everything on glm." | Machine-wide: every NEW Claude Code process on a provider — or back on the Anthropic login |
|
|
97
|
+
| `worker_classes` | "Make spotchecks cheaper." | Map task classes to providers or routing constraints |
|
|
98
|
+
| `worker_model` | "Put glm on the 1M-context model." | Show or set a provider's model ids (default and fast slots) |
|
|
99
|
+
| `worker_run` | "Fix the black buttons bug." | Start a worker or chain; returns the id immediately |
|
|
100
|
+
| `worker_handoff` | (inside a worker) "Ask the parent whether …" | Send a proposal / question / blocker / result up to the parent |
|
|
101
|
+
| `worker_toggle` | "Turn workers off." | Route subagents to workers (on) or back to Anthropic (off) |
|
|
102
|
+
| `worker_ps` | "What are my workers doing?" | Running workers plus the last finished |
|
|
103
|
+
| `worker_replay` | "What did 3390 do?" | Transcript of one worker |
|
|
104
|
+
| `worker_say` | "Tell 3390 to skip the docs." | One message to a running worker, mid-run |
|
|
105
|
+
| `worker_resume` | "Have 3390 also update the docs." | Continue a finished worker, context intact |
|
|
106
|
+
|
|
107
|
+
### Tool details
|
|
108
|
+
|
|
109
|
+
#### `worker_status` — the state page
|
|
110
|
+
No parameters. Workers on/off, active provider, provider list, today's tally
|
|
111
|
+
(started / ok / failed, hook denied / allowed / rerouted), running workers
|
|
112
|
+
with `◆N` waiting handoffs. Use it before delegating to see what routing
|
|
113
|
+
will choose.
|
|
114
|
+
|
|
115
|
+
#### `worker_providers` — manage providers
|
|
116
|
+
`action` (default `list`): **list** · **add** · **update** · **remove** ·
|
|
117
|
+
**use** · **enable** · **disable** · **test**. `name` is required for every
|
|
118
|
+
action but `list`.
|
|
119
|
+
|
|
120
|
+
- **add** — needs `name`, `model`, and `key_file` or `key`. `base_url` for an
|
|
121
|
+
Anthropic-protocol endpoint, or `upstream_url` with `protocol=openai`
|
|
122
|
+
(routed through the local proxy). Optional: `engine` (`claude`|`codex`),
|
|
123
|
+
`fast_model`, `context_window` (the meter; unset hides it unless the init
|
|
124
|
+
event announces one), `env`, `note`, `quota_probe`, `cost_tier` (1 cheapest
|
|
125
|
+
… 5), `tags` (`code`, `vision`, `image-gen`, `long-context`, `fast`,
|
|
126
|
+
`reasoning`). The first provider added also turns routing on and seeds the
|
|
127
|
+
classes.
|
|
128
|
+
- **update** — `cost_tier` and/or `tags`.
|
|
129
|
+
- **use** — make it active; unset classes follow it.
|
|
130
|
+
- **enable / disable** — in or out of auto-routing.
|
|
131
|
+
- **test** — one-word pong probe: latency, OK/FAILED.
|
|
132
|
+
- **remove** — drop it, and any classes pointing at it.
|
|
133
|
+
|
|
134
|
+
#### `worker_fallback` — the machine-wide switch
|
|
135
|
+
`action` (default `status`): **on** · **off** · **status**; `on` takes
|
|
136
|
+
`provider` (default: active). See the orchestrator section above for exactly
|
|
137
|
+
what it writes and restores.
|
|
138
|
+
|
|
139
|
+
#### `worker_classes` — map classes to providers
|
|
140
|
+
`action` (default `list`): **list** · **set** · **unset**. `set` takes
|
|
141
|
+
`class` plus either `target` (`<provider>` or `<provider>/fast`) or routing
|
|
142
|
+
constraints — `max_cost_tier` (1–5) and/or `require_tags` — with which
|
|
143
|
+
auto-routing picks any qualifying provider. `unset` frees a class back to
|
|
144
|
+
the active provider.
|
|
145
|
+
|
|
146
|
+
#### `worker_model` — model slots
|
|
147
|
+
`action` (default `get`): **get** — model ids of one provider (`provider`,
|
|
148
|
+
default active) or all when omitted; **set** — needs `model`, `slot` picks
|
|
149
|
+
`default` or `fast`. `[1m]` suffix = 1M window, derived from the id.
|
|
150
|
+
|
|
151
|
+
#### `worker_run` — start work
|
|
152
|
+
`prompt` (required — the task, self-contained). Optional: `chain`
|
|
153
|
+
(`draft,implement` or `draft,implement,review`), `class` (the nine below),
|
|
154
|
+
`label` (shown in `worker_ps` and the statusline — always give one), `cwd`,
|
|
155
|
+
`allowed_tools` (comma-separated allowlist), `mcp` (servers/sets the worker
|
|
156
|
+
may load). Returns the id at once; the run is a background task — the result
|
|
157
|
+
arrives later as a task notification (`result` plus a parsed `report`:
|
|
158
|
+
changed paths, ✓/✗ checks, open items). The orchestrator ends its turn
|
|
159
|
+
instead of waiting, and reviews the diff itself — workers never merge their
|
|
160
|
+
own work.
|
|
161
|
+
|
|
162
|
+
#### `worker_handoff` — from inside a worker
|
|
163
|
+
`kind` (required): `proposal` · `question` · `blocker` · `result`; `text`
|
|
164
|
+
(required, one paragraph); optional structured `data`. Lands in the parent's
|
|
165
|
+
inbox. Only works inside a worker — no sideways or downward path. Results
|
|
166
|
+
are sent automatically when you finish; send one yourself only for mid-run
|
|
167
|
+
findings.
|
|
168
|
+
|
|
169
|
+
#### `worker_toggle` — routing switch
|
|
170
|
+
`enabled` (required). `true`: the Agent-tool hook denies in-process
|
|
171
|
+
subagents and rewrites them to workers. `false`: they run on Anthropic
|
|
172
|
+
again. Registry kept either way.
|
|
173
|
+
|
|
174
|
+
#### `worker_ps` — the list
|
|
175
|
+
Optional `all`: `true` = every session's workers, default this session's.
|
|
176
|
+
Running workers show id, provider, age, turns, current tool; the last
|
|
177
|
+
finished is included.
|
|
178
|
+
|
|
179
|
+
#### `worker_replay` — one transcript
|
|
180
|
+
`id` (required), optional `tail` (last N rendered lines, 1–2000). Plain
|
|
181
|
+
text: tool calls, short outputs, result.
|
|
182
|
+
|
|
183
|
+
#### `worker_say` — talk to a running worker
|
|
184
|
+
`id`, `text` (both required). Lands on the worker's open stdin as a user
|
|
185
|
+
message, mid-run, without breaking its stream. Fails with an explanation
|
|
186
|
+
when the worker finished — then `worker_resume`.
|
|
187
|
+
|
|
188
|
+
#### `worker_resume` — continue a finished worker
|
|
189
|
+
`id`, `text` (both required). `claude --resume` on the same provider, same
|
|
190
|
+
session, context intact; returns the new worker id. Not for running workers.
|
|
191
|
+
|
|
192
|
+
## Configure
|
|
193
|
+
|
|
194
|
+
Everything lives in the `workers` section of `~/.config/pai/config.json`
|
|
195
|
+
(`readWorkersSection` / `writeWorkersSection`, `src/workers/config.ts:619`);
|
|
196
|
+
every sentence and tool above writes it atomically — no hand-editing.
|
|
197
|
+
|
|
198
|
+
**Classes** (`WORKER_CLASSES`, `src/workers/config.ts:131`):
|
|
199
|
+
|
|
200
|
+
| Class | Work |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `draft` · `plan` | spec writing, planning |
|
|
203
|
+
| `implement` · `complex` | code (get their own git worktree on `worker/<id>`) |
|
|
204
|
+
| `review` | reads the diff |
|
|
205
|
+
| `research` | web research (give it `WebSearch,WebFetch`) |
|
|
206
|
+
| `spotcheck` · `simple` | fast verification, small jobs |
|
|
207
|
+
| `image` | image generation |
|
|
208
|
+
|
|
209
|
+
A class maps to `<provider>`, `<provider>/fast`, or to constraints
|
|
210
|
+
(`maxCostTier` 1–5, `requireTags`, per-class `order`). Unset classes follow
|
|
211
|
+
the active provider; `active: "auto"` routes by `workers.routing.order`
|
|
212
|
+
instead.
|
|
213
|
+
|
|
214
|
+
**Provider fields** (registry entry, `parseProvider`,
|
|
215
|
+
`src/workers/config.ts:264`): protocol, base URL (or upstream URL for the
|
|
216
|
+
proxy), key file path, model slots (default / fast, `[1m]` suffix for the
|
|
217
|
+
1M window), engine, `cost_tier`, `tags`, extra `env`, `context_window`.
|
|
218
|
+
|
|
219
|
+
**Defaults you inherit** (`src/workers/config.ts:209-226`):
|
|
220
|
+
|
|
221
|
+
| Area | Default |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| Routing | cooldown 30 min after a quota/rate failure, `retryOnQuota` true |
|
|
224
|
+
| Sub-worker tree | depth 2, 4 children per parent |
|
|
225
|
+
| Follow pane | font 13 pt, auto-exit 60 s |
|
|
226
|
+
| Logs | `~/.claude/logs/workers` |
|
|
227
|
+
|
|
228
|
+
**Environment variables:**
|
|
229
|
+
|
|
230
|
+
| Var | Effect |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| `PAI_WORKER=1` | carried by every headless spawn — marks the worker session, exempts it from orchestrator-only hooks (`src/workers/run-env.ts:47`) |
|
|
233
|
+
| `ALLOW_ANTHROPIC_AGENTS=1` | re-enables the Agent tool for one session |
|
|
234
|
+
| `ENABLE_TOOL_SEARCH=true` | set in interactive provider runs (`run-env.ts:49`) |
|
|
235
|
+
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` | every provider spawn (`run-env.ts:45`) |
|
|
236
|
+
| `CLAUDE_SETTINGS_PATH` | dry-run target for `worker_fallback` |
|
|
237
|
+
|
|
238
|
+
## How it works
|
|
239
|
+
|
|
240
|
+
One orchestrator, many workers, split made mechanical by four hooks:
|
|
241
|
+
|
|
242
|
+
| Hook | Enforces | Where |
|
|
243
|
+
| --- | --- | --- |
|
|
244
|
+
| PreToolUse (Agent) | every in-process subagent denied while providers are configured; the deny reason prints the `pai worker run` line to use instead | `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts`, deployed `${PAI_DIR}/Hooks/route-agents-to-worker.mjs` |
|
|
245
|
+
| PreToolUse (Edit/Write) | no code edits by the orchestrator inside a git work tree; `PAI_WORKER=1` sessions are exempt | `${PAI_DIR}/Hooks/route-edits-to-worker.mjs` (deployed config) |
|
|
246
|
+
| SessionStart (compact) | saved state replayed and the live worker list injected — a compacted orchestrator still sees every worker | `src/hooks/ts/session-start/post-compact-inject.ts`, `${PAI_DIR}/Hooks/post-compact-workers.mjs` |
|
|
247
|
+
| PreToolUse (TaskOutput) | blocking waits denied unless `block: false` — end the turn; the result arrives as a task notification | `${PAI_DIR}/Hooks/block-taskoutput-wait.mjs` |
|
|
248
|
+
|
|
249
|
+
Every routing decision lands in the routing ledger (`pai worker log`). A
|
|
250
|
+
chain `draft,implement[,review]` gives each stage its own worker — own id,
|
|
251
|
+
own follow pane, tree in the worker list; a failing stage stops the chain.
|
|
252
|
+
Writing classes work in their own git worktree on `worker/<id>`; merging or
|
|
253
|
+
discarding is a deliberate operator (or orchestrator) action.
|
|
254
|
+
|
|
255
|
+
The **statusline** carries the worker row (line 4):
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
glm ▶1 #3390 unlabeled 30s · interactive ✓74 ✗4 today
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Segments: provider badge (`glm`; `workers` when mixed) · `▶N` this
|
|
262
|
+
terminal's running workers with a live pid · short id · label · age ·
|
|
263
|
+
current step · today's finished count. Full legend (`◆N` handoffs, `ctx`
|
|
264
|
+
meter, `↳` sub-workers): the *Status line* section of
|
|
265
|
+
[worker.md](worker.md).
|
|
266
|
+
|
|
267
|
+
What else follows the active provider — statusline plan windows,
|
|
268
|
+
daemon-side summarizer calls, compaction history, the translating proxy:
|
|
269
|
+
[provider-abstraction.md](provider-abstraction.md).
|
|
270
|
+
|
|
271
|
+
### Code map
|
|
272
|
+
|
|
273
|
+
| What | Where |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| Provider registry, classes (`WORKER_CLASSES`) | `src/workers/config.ts:131` |
|
|
276
|
+
| Config read/write, `workers` section | `src/workers/config.ts:619` |
|
|
277
|
+
| Provider validation (`parseProvider`) | `src/workers/config.ts:264` |
|
|
278
|
+
| First-provider takeover (enabled + active + seeded classes) | `src/workers/providers.ts:51` (`addProvider`) |
|
|
279
|
+
| `providers use` (sets `workers.active`) | `src/workers/providers.ts:170` (`useProvider`) |
|
|
280
|
+
| `pai worker on/off` (`setWorkersEnabled`) | `src/workers/providers.ts:279` |
|
|
281
|
+
| Runner / chain / agent definitions | `src/workers/run.ts`, `src/workers/chain.ts`, `src/workers/agents.ts` |
|
|
282
|
+
| Spawn env — base URL, token, model pins | `src/workers/run-env.ts` |
|
|
283
|
+
| Machine-wide fallback (settings.json) | `src/workers/fallback.ts` |
|
|
284
|
+
| Shims (`glm`, `glm-run`, …) installer | `src/workers/install.ts` |
|
|
285
|
+
| Worktrees and merge | `src/workers/worktree.ts` |
|
|
286
|
+
| CLI surface (`run`, `ps`, `follow`, `replay`, `merge`, `discard`) | `src/cli/commands/worker/index.ts` |
|
|
287
|
+
| Providers / classes CLI | `src/cli/commands/worker/providers.ts:124,334` |
|
|
288
|
+
| Model slots CLI (`pai worker model`) | `src/cli/commands/worker/model.ts` |
|
|
289
|
+
| Statusline worker row | `src/workers/render.ts`, `statusline-command.sh` (line 4), built `${PAI_DIR}/worker-status-line.mjs` |
|
|
290
|
+
| Context window from model id (`[1m]`) | `src/utils/model-window.ts` |
|
|
291
|
+
| Agent-tool gate hook (repo) | `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts` |
|
|
292
|
+
| Post-compact reinjection (repo) | `src/hooks/ts/session-start/post-compact-inject.ts`, `src/hooks/ts/lib/context-fill.ts` |
|
|
293
|
+
| Worker-session exemption | `src/hooks/ts/lib/worker-session.ts` |
|
|
294
|
+
| Deployed hooks (config, not repo) | `${PAI_DIR}/Hooks/route-edits-to-worker.mjs`, `block-taskoutput-wait.mjs`, `post-compact-workers.mjs` — registered in `${PAI_DIR}/settings.json` |
|
|
295
|
+
| MCP tool surface — all 12 `worker_*` tools | `src/daemon-mcp/index.ts:804-1334` (`worker_model` handler: `src/daemon-mcp/tools/worker-model.ts`) |
|
|
296
|
+
| Neighbour docs | [provider-independence.md](provider-independence.md), [provider-abstraction.md](provider-abstraction.md), [worker.md](worker.md), [commands/worker.md](commands/worker.md) |
|
|
297
|
+
|
|
298
|
+
## CLI fallback
|
|
299
|
+
|
|
300
|
+
For scripts and terminals outside sessions — the twin of a tool above; flags
|
|
301
|
+
live in [commands/worker.md](commands/worker.md). (`worker_status` has no
|
|
302
|
+
single twin; `pai worker ps` + `pai worker log` show the same facts.)
|
|
303
|
+
|
|
304
|
+
| Command | Twin of |
|
|
305
|
+
| --- | --- |
|
|
306
|
+
| `pai worker run [claude-args…]` | `worker_run` — and the only place a one-call `--provider` override exists |
|
|
307
|
+
| `pai worker ps [--all]` | `worker_ps` |
|
|
308
|
+
| `pai worker replay <id> [--tail n]` | `worker_replay` |
|
|
309
|
+
| `pai worker say <id> <text>` | `worker_say` |
|
|
310
|
+
| `pai worker resume <id> <text>` | `worker_resume` |
|
|
311
|
+
| `pai worker handoff <json>` | `worker_handoff` |
|
|
312
|
+
| `pai worker providers <list\|add\|update\|remove\|use\|enable\|disable\|test>` | `worker_providers` — keys via `--key-file`, never pasted |
|
|
313
|
+
| `pai worker classes <list\|set\|unset>` | `worker_classes` |
|
|
314
|
+
| `pai worker model [what] [model]` | `worker_model` |
|
|
315
|
+
| `pai worker on` / `pai worker off` | `worker_toggle` |
|
|
316
|
+
| `pai worker fallback <on [provider]\|off\|status>` | `worker_fallback` |
|
|
317
|
+
|
|
318
|
+
Terminal-only (no tool equivalent):
|
|
319
|
+
|
|
320
|
+
| Command | What it does |
|
|
321
|
+
| --- | --- |
|
|
322
|
+
| `pai worker watch` | `ps` refreshed every 2 seconds |
|
|
323
|
+
| `pai worker follow [id]` / `pai worker pane [id]` | live transcript pane for one worker or this session's |
|
|
324
|
+
| `pai worker log [all\|tail\|<id>]` | the routing ledger, or one worker's raw event stream |
|
|
325
|
+
| `pai worker merge <id>` / `pai worker discard <id>` | land a worker's worktree branch in the original checkout, or drop it entirely |
|
|
326
|
+
| `pai worker controls <id> you\|me` | hand the desktop controls (clickr) to a worker, or take them back |
|
|
327
|
+
| `pai worker status-line [term] [cwd]` | one-line worker summary for a status bar (called by `statusline-command.sh`) |
|
|
328
|
+
| `pai worker proxy [stop]` | the local Anthropic↔OpenAI proxy |
|
|
329
|
+
| `pai worker mcp [list]` | MCP servers/sets workers may load |
|
|
330
|
+
| `pai worker install` | migration: Agent hook in settings.json, `~/.local/bin` shims (`glm`, …), old script cleanup |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Provider Independence
|
|
2
|
+
|
|
3
|
+
How to free yourself from Claude — so a maxed-out subscription stops being your problem. How to run any cloud model and any local model inside your Claude Code (or OpenCode) harness. How to run models in parallel, each doing what it is best at — all of it purely local, no need for yet another Hermes-or-whatever harness. How to have workers run by default, visibly, each with its own chat you can talk to.
|
|
4
|
+
|
|
5
|
+
My journey into PAI and AIBroker started nine months ago, when OpenClawd burned through all my credits in four hours. I decided then and there that there had to be a better way: instead of buying API tokens, use the Claude Max subscription I was already paying for.
|
|
6
|
+
|
|
7
|
+
Nine months later, I hit the next wall: maxing out Claude Max itself. The next level of independence was required. I built it — in one day. Here is what happened.
|
|
8
|
+
|
|
9
|
+
I started my Thursday at 5am, which is normal, and at about 80%, which is not.
|
|
10
|
+
|
|
11
|
+
That 80% is not motivation — although that was certainly higher. It is the weekly usage of my Claude Max 200 subscription.
|
|
12
|
+
|
|
13
|
+
With two days to go until the Saturday 8am reset, I was getting worried. Last week I had already hit 100% — despite all my efficiency gains, including inventing my own "Agentish" language.
|
|
14
|
+
|
|
15
|
+
By 9am I was above 90%. There was no way I would make it to the reset.
|
|
16
|
+
|
|
17
|
+
So I decided to solve this problem instead of waiting it out. By 10pm I was at 97% — a number that has not moved since noon, because since noon, almost nothing has run on Claude.
|
|
18
|
+
|
|
19
|
+
This is how I did it.
|
|
20
|
+
|
|
21
|
+
I made myself independent of Claude and Fable. They are great — but I hate being locked in, and I suspect most of us do. I want to decide which model I use: Claude, GLM, Grok, whatever. I need to abstract away from that choice.
|
|
22
|
+
|
|
23
|
+
Fable was — and is — great, but it burns tokens. And Opus on its own is so unbelievably dumb that I had taken to letting Fable do everything, even forcing it to not use agents — burning tokens even faster.
|
|
24
|
+
|
|
25
|
+
So the requirements were:
|
|
26
|
+
|
|
27
|
+
1. **Provider independence.** Switch to any model at any time — not only for agents, but for the main orchestrator session too.
|
|
28
|
+
2. **Cloud and local.** Any cloud model, and local models.
|
|
29
|
+
3. **Visibility.** Really see what each worker is doing — not Claude Code's click-into-a-worker-and-make-sense-of-it, but every worker in a side pane, automatically.
|
|
30
|
+
4. **Direct chat.** Talk to any worker while it runs.
|
|
31
|
+
5. **Hardening.** Each worker in its own worktree, so parallel work cannot destroy itself.
|
|
32
|
+
6. **Forced orchestration.** Not just ask the orchestrator to delegate — deterministically enforce that it spawns workers.
|
|
33
|
+
7. **Cost routing.** Automatically pick cheaper workers for simpler tasks.
|
|
34
|
+
8. **Keep the harness.** Stay on Claude Code (I could have switched to OpenCode — turns out it was not even necessary).
|
|
35
|
+
|
|
36
|
+
The whole implementation cost 7 of my last 10 percentage points of Claude Max 200. Then I switched to GLM for everything else. And what can I say — it works beautifully.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
The rest of this page is the operator's guide: switching providers, starting sessions, and the three copy-paste steps to your own setup.
|
|
41
|
+
|
|
42
|
+
> My AI budget hit 91% before lunch. So I changed one rule: the expensive
|
|
43
|
+
> assistant keeps the thinking, a cheaper crew does the building. The feature
|
|
44
|
+
> shipped anyway.
|
|
45
|
+
>
|
|
46
|
+
> Your AI bill is not a fact of nature. It is a design decision.
|
|
47
|
+
|
|
48
|
+
Your assistant runs its helper crew on whichever AI provider you choose —
|
|
49
|
+
and so can the session itself.
|
|
50
|
+
|
|
51
|
+
## Switching
|
|
52
|
+
|
|
53
|
+
- **The crew.** Once, at the start: "Add a worker provider named <provider> — here
|
|
54
|
+
is the key." Then: "Use <provider> for the workers from now on."
|
|
55
|
+
- **The session itself.** Start it with the `<provider>` command — the shim for
|
|
56
|
+
`pai worker run` — or just pick a project with `pai`: the picker follows
|
|
57
|
+
the active provider too, so the whole session, you included, runs on it.
|
|
58
|
+
|
|
59
|
+
Swapping back — or to any other provider — is one sentence for the crew, and
|
|
60
|
+
starting `claude` again for the session.
|
|
61
|
+
|
|
62
|
+
## A day with the crew
|
|
63
|
+
|
|
64
|
+
- "Fix the login timeout bug." — a worker named *fix login timeout* starts; the status line shows its name and current step, live.
|
|
65
|
+
- "What are my workers doing?" — a short list, by name.
|
|
66
|
+
- "Tell fix login timeout to also check the retry path." — lands mid-run; the worker adapts.
|
|
67
|
+
- "Show me what it changed." — the diff comes back for your review.
|
|
68
|
+
- "Stop using workers for now." — the crew stands down.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
## Further
|
|
73
|
+
|
|
74
|
+
- [How it works — full tool reference](provider-independence-details.md)
|
|
75
|
+
- [The provider layer, in depth](provider-abstraction.md)
|