@workweave/router 0.2.10 → 0.2.11
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 +15 -3
- package/cc-statusline.sh +179 -7
- package/codex-skills/force-model/SKILL.md +14 -0
- package/codex-skills/router-feedback/SKILL.md +14 -0
- package/codex-skills/unforce-model/SKILL.md +14 -0
- package/commands/models.md +46 -0
- package/commands/router-models.md +46 -0
- package/directives.tsv +12 -0
- package/install.sh +1036 -78
- package/package.json +6 -1
- package/pi-router/README.md +36 -7
- package/pi-router/skills/install-lsps/SKILL.md +75 -0
- package/pi-router/skills/lsp-guide/SKILL.md +63 -0
- package/pi-router/src/compaction.ts +46 -8
- package/pi-router/src/config.ts +34 -5
- package/pi-router/src/context-window.ts +12 -0
- package/pi-router/src/dispatch.ts +35 -2
- package/pi-router/src/index.ts +10 -1
- package/pi-router/src/lsp-broker.ts +255 -0
- package/pi-router/src/lsp-client.ts +435 -0
- package/pi-router/src/lsp-format.ts +230 -0
- package/pi-router/src/lsp-install.ts +215 -0
- package/pi-router/src/lsp-protocol.ts +128 -0
- package/pi-router/src/lsp-servers.ts +361 -0
- package/pi-router/src/lsp.ts +529 -0
- package/pi-router/src/pricing.generated.ts +6 -1
- package/pi-router/src/provider.ts +15 -2
- package/pi-router/src/routed-model.ts +17 -0
- package/registry.sh +100 -0
- package/uninstall.sh +173 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workweave/router",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "One-command installer that points Claude Code, Codex, opencode, or pi at the Weave Router. For pi it also ships the routing extension, loaded via pi.extensions.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"weave-router": "bin.js",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"pi": {
|
|
10
10
|
"extensions": [
|
|
11
11
|
"./pi-router/src/index.ts"
|
|
12
|
+
],
|
|
13
|
+
"skills": [
|
|
14
|
+
"./pi-router/skills"
|
|
12
15
|
]
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
@@ -16,6 +19,8 @@
|
|
|
16
19
|
"install.sh",
|
|
17
20
|
"uninstall.sh",
|
|
18
21
|
"cc-statusline.sh",
|
|
22
|
+
"registry.sh",
|
|
23
|
+
"directives.tsv",
|
|
19
24
|
"commands/",
|
|
20
25
|
"codex-skills/",
|
|
21
26
|
"pi-router/",
|
package/pi-router/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Installed automatically by the Weave Router installer:
|
|
|
11
11
|
```bash
|
|
12
12
|
WEAVE_ROUTER_KEY=rk_… npx --package @workweave/router -y -- weave-router --pi
|
|
13
13
|
WEAVE_ROUTER_KEY=rk_… npx --package @workweave/router -y -- weave-router --pi --local # local router
|
|
14
|
+
WEAVE_ROUTER_KEY=rk_… npx --package @workweave/router -y -- weave-router --pi --lsp go,typescript # + language servers for the lsp tool
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
That writes `~/.pi/agent/models.json` (the `weave` provider), adds
|
|
@@ -32,17 +33,37 @@ from npm on next start and loads this extension via its `pi.extensions` field.
|
|
|
32
33
|
router validates and canonicalizes the requested model.
|
|
33
34
|
- **Per-process routing bias.** Static `x-weave-routing-*` knob headers bias the
|
|
34
35
|
router: quality on the main loop and speed + cheap on subagents.
|
|
35
|
-
- **Long tool-loop compaction.** Pi
|
|
36
|
-
|
|
37
|
-
extension preserves a usable output budget for the real continuation
|
|
38
|
-
compacts once the loop settles,
|
|
39
|
-
|
|
36
|
+
- **Long tool-loop compaction.** Pi can cross its context threshold inside an
|
|
37
|
+
uninterrupted tool loop before its normal post-run compaction check. The
|
|
38
|
+
extension preserves a usable output budget for the real continuation,
|
|
39
|
+
compacts once the loop settles, and resumes that extension-owned tool loop.
|
|
40
|
+
Ordinary Pi threshold compaction remains under Pi's control.
|
|
40
41
|
- **Sticky sessions.** `metadata.user_id = "pi:<sessionId>"` pins the main loop
|
|
41
42
|
to one model for the session; subagents get their own pins.
|
|
42
43
|
- **`dispatch` tool — parallel, context-isolated subagents.** pi has none
|
|
43
44
|
natively. `dispatch` spawns child `pi` processes (read-only by default), runs
|
|
44
45
|
them concurrently, and returns only each subagent's final answer — intermediate
|
|
45
46
|
tool output stays in the child, so the main context stays small.
|
|
47
|
+
- **`lsp` tool — code intelligence through a language server.** definition,
|
|
48
|
+
references, hover, documentSymbol, and diagnostics for Go, TypeScript/
|
|
49
|
+
JavaScript, Python, and Rust (gopls, typescript-language-server, pyright,
|
|
50
|
+
rust-analyzer). Servers are spawned lazily per workspace root, shut down
|
|
51
|
+
after idling, and shared with dispatch subagents through a parent-side local
|
|
52
|
+
socket — a fan-out reuses the parent's warm servers instead of cold-starting
|
|
53
|
+
its own. A bundled `lsp-guide` skill (`/skill:lsp-guide`) carries the usage
|
|
54
|
+
recipes (per-operation cookbook, the locate-then-references flow, caveats).
|
|
55
|
+
Disable with `WEAVE_PI_NO_LSP=1`.
|
|
56
|
+
- **Opt-in language-server install — nothing installs silently.** Two ways in:
|
|
57
|
+
pass `--lsp go,typescript` to the installer, or let the assistant offer.
|
|
58
|
+
When the workspace contains a language whose server is missing, the
|
|
59
|
+
assistant offers once in conversation ("I can enable go LSP support if
|
|
60
|
+
you'd like — just say the word!"); a yes runs the install through the
|
|
61
|
+
`lsp_enable` tool (main process only — subagents can't trigger installs), a
|
|
62
|
+
"don't ask again" is persisted per language in `<agentDir>/.weave_lsp.json`
|
|
63
|
+
and respected across sessions. Installs need that language's toolchain
|
|
64
|
+
(`go`, `npm`, or `rustup`) on PATH; when the toolchain itself is missing,
|
|
65
|
+
the bundled `install-lsps` skill (`/skill:install-lsps`) walks the agent
|
|
66
|
+
through installing it — again only with explicit consent.
|
|
46
67
|
- **Persistent route + savings display.** Shows
|
|
47
68
|
`WEAVE ROUTER — <routed> ← <selected> · saved $X.XX` below pi's native footer
|
|
48
69
|
data. Savings compare the selected and routed catalog prices against the same
|
|
@@ -70,9 +91,17 @@ from npm on next start and loads this extension via its `pi.extensions` field.
|
|
|
70
91
|
| `WEAVE_ROUTING_ALPHA` / `…_SPEED_WEIGHT` / `…_OUTPUT_COST_RATIO` / `…_EXPECTED_OUTPUT_TOKENS` | role preset | Override individual routing knobs (main process only — children always use their role preset) |
|
|
71
92
|
| `WEAVE_NO_SAFETY` | unset | `1` disables the catastrophic-bash gate |
|
|
72
93
|
| `WEAVE_PI_AUTO_COMPACTION` | unset | `0` disables the routed tool-loop compaction safeguard |
|
|
94
|
+
| `WEAVE_PI_NO_LSP` | unset | `1` disables the `lsp` tool, the server pool, and the subagent broker |
|
|
95
|
+
| `WEAVE_PI_LSP_IDLE_MS` | `300000` | Idle window before an unused language server is shut down |
|
|
96
|
+
| `WEAVE_PI_LSP_REQUEST_TIMEOUT_MS` | `15000` | Per-request budget once a server is warm |
|
|
97
|
+
| `WEAVE_PI_LSP_WARMUP_TIMEOUT_MS` | `60000` | Budget for initialize + the first query (indexing) |
|
|
98
|
+
| `WEAVE_PI_LSP_DIAGNOSTICS_WAIT_MS` | `3000` | How long `diagnostics` waits for a fresh publish |
|
|
99
|
+
| `WEAVE_PI_LSP_MAX_SERVERS` | `4` | LRU cap on concurrently live language servers |
|
|
100
|
+
| `WEAVE_PI_LSP_MAX_REFERENCES` | `100` | Cap on reported reference sites |
|
|
73
101
|
|
|
74
|
-
Internal: `WEAVE_PI_SUBAGENT=1
|
|
75
|
-
on child processes; don't
|
|
102
|
+
Internal: `WEAVE_PI_SUBAGENT=1`, `WEAVE_PI_SUBAGENT_ID`, `WEAVE_PI_LSP_BROKER`,
|
|
103
|
+
and `WEAVE_PI_LSP_BROKER_TOKEN` are set by `dispatch` on child processes; don't
|
|
104
|
+
set them yourself.
|
|
76
105
|
|
|
77
106
|
## Billing
|
|
78
107
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install-lsps
|
|
3
|
+
description: Install language servers (gopls, typescript-language-server, pyright, rust-analyzer) and their prerequisite toolchains (Go, Node/npm, rustup) so the lsp tool works. Use when the user asks to enable or install LSP / language-server support, when lsp_enable reports a missing toolchain, or when the lsp tool reports no language server is installed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Install LSPs
|
|
7
|
+
|
|
8
|
+
Enable the `lsp` tool for a language by installing its language server — and,
|
|
9
|
+
when the server install needs it, the underlying toolchain.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- Everything here is consent-gated: only act on the user's explicit go-ahead,
|
|
14
|
+
and confirm each toolchain install separately — those are system-level
|
|
15
|
+
changes, not part of whatever task prompted this.
|
|
16
|
+
- If the user declines, stop. If they ask not to be offered again, call
|
|
17
|
+
`lsp_enable` with `{"language": "<language>", "action": "dismiss"}`.
|
|
18
|
+
- Prefer the `lsp_enable` tool over bash whenever the toolchain already
|
|
19
|
+
exists; reach for bash only for the prerequisites `lsp_enable` refuses to
|
|
20
|
+
install itself.
|
|
21
|
+
|
|
22
|
+
## Per language
|
|
23
|
+
|
|
24
|
+
Work the ladder for each language the user wants:
|
|
25
|
+
|
|
26
|
+
1. **Check what is missing.** Server binaries: `gopls`,
|
|
27
|
+
`typescript-language-server`, `pyright-langserver` (or
|
|
28
|
+
`basedpyright-langserver`), `rust-analyzer` — also look in `~/go/bin` and
|
|
29
|
+
`~/.cargo/bin`, which the lsp tool searches even when they are off PATH.
|
|
30
|
+
Toolchains: `go`, `npm`, `rustup`.
|
|
31
|
+
2. **Toolchain present** → call `lsp_enable` with
|
|
32
|
+
`{"language": "<go|typescript|python|rust>"}` and you are done; it runs the
|
|
33
|
+
right server install and verifies the binary resolves.
|
|
34
|
+
3. **Toolchain missing** → show the user the command you intend to run, get
|
|
35
|
+
their confirmation, run it in bash, then return to step 2.
|
|
36
|
+
4. **Verify** with a real `lsp` query (`documentSymbol` on an actual source
|
|
37
|
+
file). The first query per workspace can take up to a minute while the
|
|
38
|
+
server indexes; that is normal.
|
|
39
|
+
|
|
40
|
+
## Toolchain installs (step 3 reference)
|
|
41
|
+
|
|
42
|
+
Go (for gopls):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
brew install go # macOS
|
|
46
|
+
sudo apt-get install -y golang-go # Debian/Ubuntu; or dnf install golang
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or the official downloads at https://go.dev/dl for anything else.
|
|
50
|
+
|
|
51
|
+
Node/npm (for typescript-language-server and pyright):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
brew install node # macOS
|
|
55
|
+
sudo apt-get install -y nodejs npm # Debian/Ubuntu
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or nvm (https://github.com/nvm-sh/nvm) when the user prefers per-user installs.
|
|
59
|
+
|
|
60
|
+
rustup (for rust-analyzer):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This is the official rust installer; show the user the command before running
|
|
67
|
+
it and follow its printed PATH instructions afterwards.
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- `go install` drops gopls in `~/go/bin` and rustup drops rust-analyzer in
|
|
72
|
+
`~/.cargo/bin`. The lsp tool finds both without PATH changes; other tools
|
|
73
|
+
may still want a PATH entry.
|
|
74
|
+
- All of this can also happen at install time:
|
|
75
|
+
`npx @workweave/router --pi --lsp go,typescript,python,rust`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lsp-guide
|
|
3
|
+
description: Recipes for the lsp tool — resolving where a symbol is defined, every place it is used, type signatures and docs, file outlines, and compiler/type errors through a real language server (Go, TypeScript/JavaScript, Python, Rust). Use when navigating or explaining code by symbol, finding callers or usages, checking what broke after an edit, or deciding between lsp and text search.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Using the lsp tool
|
|
7
|
+
|
|
8
|
+
The `lsp` tool answers *semantic* questions through the language's own server:
|
|
9
|
+
it resolves through imports, types, and aliases, where grep only matches text.
|
|
10
|
+
Reach for it whenever the question is about a **symbol**; keep grep for plain
|
|
11
|
+
strings, comments, and file types no server supports.
|
|
12
|
+
|
|
13
|
+
## Operations
|
|
14
|
+
|
|
15
|
+
| Operation | Answers | Needs |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `definition` | where the symbol at a position is defined | `path`, `line`, `column` |
|
|
18
|
+
| `references` | every place the symbol at a position is used | `path`, `line`, `column` |
|
|
19
|
+
| `hover` | type signature + docs for the symbol at a position | `path`, `line`, `column` |
|
|
20
|
+
| `documentSymbol` | the outline (types, functions, methods) of one file | `path` |
|
|
21
|
+
| `diagnostics` | compiler/type errors and warnings for one file | `path` |
|
|
22
|
+
|
|
23
|
+
Positions are 1-based, and `column` must point **at the identifier** (its first
|
|
24
|
+
character works). All paths resolve against the working directory.
|
|
25
|
+
|
|
26
|
+
## Recipes
|
|
27
|
+
|
|
28
|
+
**"Where is X defined?"** — from any usage site of X:
|
|
29
|
+
`{"operation": "definition", "path": "<file>", "line": <L>, "column": <C>}`.
|
|
30
|
+
Don't know a usage site? Find one first (see next recipe's step 1).
|
|
31
|
+
|
|
32
|
+
**"Everywhere X is used"** — two steps, always:
|
|
33
|
+
|
|
34
|
+
1. Locate the declaration: `rg -n --column '\bX\b'` (the `--column` output is
|
|
35
|
+
exactly the coordinate the next call needs), or `documentSymbol` when you
|
|
36
|
+
already know the file.
|
|
37
|
+
2. `{"operation": "references", "path": "<decl file>", "line": <L>, "column": <C>}`.
|
|
38
|
+
|
|
39
|
+
A grep hit list is **not** a references answer: it includes comments and
|
|
40
|
+
strings, misses aliased imports, and can't distinguish same-named symbols.
|
|
41
|
+
Step 2 is what makes the answer authoritative.
|
|
42
|
+
|
|
43
|
+
**"What is this / what's its signature?"** — `hover` at the symbol. Prefer it
|
|
44
|
+
over reading the definition file when you only need the type or doc comment.
|
|
45
|
+
|
|
46
|
+
**"What's in this file?"** — `documentSymbol` before reading a large file; jump
|
|
47
|
+
straight to the line the outline gives you.
|
|
48
|
+
|
|
49
|
+
**"Did my edit break anything here?"** — `diagnostics` on the file after
|
|
50
|
+
editing. It reports the server's current analysis of the saved file.
|
|
51
|
+
|
|
52
|
+
## Behavior worth knowing
|
|
53
|
+
|
|
54
|
+
- **First query per workspace is slow** (the server indexes — up to a minute
|
|
55
|
+
in a big repo). Later queries on that workspace are fast; don't give up
|
|
56
|
+
after one timeout, retry once.
|
|
57
|
+
- **References are capped** (default 100, header says how many exist).
|
|
58
|
+
- **Unsupported file type or missing server** returns advice text, not an
|
|
59
|
+
error — fall back to grep for that file. A missing server can be installed
|
|
60
|
+
with the user's consent via `lsp_enable` (see the install-lsps skill for
|
|
61
|
+
toolchain prerequisites).
|
|
62
|
+
- Works inside dispatch subagents too (they share the parent's warm servers),
|
|
63
|
+
so symbol lookups are safe to fan out.
|
|
@@ -21,11 +21,15 @@ import type {
|
|
|
21
21
|
TurnEndEvent,
|
|
22
22
|
} from "@mariozechner/pi-coding-agent";
|
|
23
23
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
24
|
+
import { ROUTED_CONTEXT_WINDOW_HEADER } from "./config.js";
|
|
25
|
+
import { parseRoutedContextWindow } from "./context-window.js";
|
|
24
26
|
|
|
25
27
|
const PROBE_MAX_TOKENS = 4;
|
|
26
28
|
const CONTINUATION_MAX_TOKENS = 16_384;
|
|
27
29
|
const COMPACTION_RESERVE_TOKENS = 16_384;
|
|
28
30
|
const STATUS_KEY = "weave-compaction";
|
|
31
|
+
/** Pi does not retry threshold compaction automatically, so resume our tool loop explicitly. */
|
|
32
|
+
export const ROUTED_COMPACTION_CONTINUATION = "Continue from the compacted context. Pick up exactly where you left off.";
|
|
29
33
|
|
|
30
34
|
interface ProviderPayload {
|
|
31
35
|
max_tokens?: unknown;
|
|
@@ -86,11 +90,20 @@ export function registerCompaction(pi: ExtensionAPI, schedule: Schedule = (callb
|
|
|
86
90
|
let lastTurnTokens = 0;
|
|
87
91
|
let repairedContinuation = false;
|
|
88
92
|
let compactionScheduled = false;
|
|
93
|
+
// Smallest served context window observed on a response this run. The router
|
|
94
|
+
// can reroute any request regardless of the requested model, so the served
|
|
95
|
+
// window is authoritative for the compaction budget. Tracking the minimum
|
|
96
|
+
// (not the latest or the max) is the conservative choice: if the router ever
|
|
97
|
+
// routed to a smaller window during the run, that window could clamp again,
|
|
98
|
+
// so the budget must respect it. Fall back to the registered model window
|
|
99
|
+
// until the first routed response.
|
|
100
|
+
let servedContextWindow: number | undefined;
|
|
89
101
|
|
|
90
102
|
const resetRun = () => {
|
|
91
103
|
highWaterTokens = 0;
|
|
92
104
|
lastTurnTokens = 0;
|
|
93
105
|
repairedContinuation = false;
|
|
106
|
+
servedContextWindow = undefined;
|
|
94
107
|
};
|
|
95
108
|
|
|
96
109
|
const finishCompaction = (ctx: ExtensionContext) => {
|
|
@@ -107,6 +120,15 @@ export function registerCompaction(pi: ExtensionAPI, schedule: Schedule = (callb
|
|
|
107
120
|
if (repairClampedToolContinuation(event.payload)) repairedContinuation = true;
|
|
108
121
|
});
|
|
109
122
|
|
|
123
|
+
pi.on("after_provider_response", (event, _ctx: ExtensionContext) => {
|
|
124
|
+
if (event.status < 200 || event.status >= 300) return;
|
|
125
|
+
const contextWindow = parseRoutedContextWindow(event.headers?.[ROUTED_CONTEXT_WINDOW_HEADER]);
|
|
126
|
+
if (contextWindow === undefined) return;
|
|
127
|
+
if (servedContextWindow === undefined || contextWindow < servedContextWindow) {
|
|
128
|
+
servedContextWindow = contextWindow;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
110
132
|
pi.on("turn_end", (event: TurnEndEvent) => {
|
|
111
133
|
if (event.message.role !== "assistant") return;
|
|
112
134
|
lastTurnTokens = contextTokens(event.message as AssistantMessage);
|
|
@@ -115,14 +137,19 @@ export function registerCompaction(pi: ExtensionAPI, schedule: Schedule = (callb
|
|
|
115
137
|
|
|
116
138
|
pi.on("agent_end", (_event: AgentEndEvent, ctx: ExtensionContext) => {
|
|
117
139
|
if (process.env.WEAVE_PI_AUTO_COMPACTION === "0" || compactionScheduled) return;
|
|
118
|
-
const
|
|
140
|
+
const registeredWindow = ctx.model?.contextWindow ?? ctx.getContextUsage()?.contextWindow ?? 0;
|
|
141
|
+
const contextWindow = servedContextWindow ?? registeredWindow;
|
|
119
142
|
if (contextWindow <= COMPACTION_RESERVE_TOKENS) return;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
const servedThreshold = contextWindow - COMPACTION_RESERVE_TOKENS;
|
|
144
|
+
const registeredThreshold = registeredWindow - COMPACTION_RESERVE_TOKENS;
|
|
145
|
+
// Pi's built-in check runs immediately after this event and budgets against
|
|
146
|
+
// the REGISTERED model window. If the final turn clears Pi's registered
|
|
147
|
+
// threshold, Pi will compact asynchronously — deferring is correct, and
|
|
148
|
+
// scheduling our own would race that async compaction. Otherwise Pi will not
|
|
149
|
+
// act, so we compact ourselves when the run crossed the SERVED threshold
|
|
150
|
+
// (which can be smaller than the registered window after a reroute).
|
|
151
|
+
if (lastTurnTokens > registeredThreshold) return;
|
|
152
|
+
if (!repairedContinuation && highWaterTokens <= servedThreshold) return;
|
|
126
153
|
|
|
127
154
|
compactionScheduled = true;
|
|
128
155
|
const previousCompactionId = latestCompactionId(ctx);
|
|
@@ -139,7 +166,18 @@ export function registerCompaction(pi: ExtensionAPI, schedule: Schedule = (callb
|
|
|
139
166
|
}
|
|
140
167
|
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, "compacting routed context...");
|
|
141
168
|
ctx.compact({
|
|
142
|
-
onComplete: () =>
|
|
169
|
+
onComplete: () => {
|
|
170
|
+
finishCompaction(ctx);
|
|
171
|
+
// Pi only auto-retries an overflow compaction. This compaction is
|
|
172
|
+
// extension-owned and follows a routed tool loop, so queue the next
|
|
173
|
+
// turn after its summary is committed rather than leaving the agent idle.
|
|
174
|
+
// A user may have started another turn while the compaction ran; in
|
|
175
|
+
// that case Pi requires an explicit follow-up delivery mode.
|
|
176
|
+
pi.sendUserMessage(
|
|
177
|
+
ROUTED_COMPACTION_CONTINUATION,
|
|
178
|
+
ctx.isIdle() ? undefined : { deliverAs: "followUp" },
|
|
179
|
+
);
|
|
180
|
+
},
|
|
143
181
|
onError: (error) => {
|
|
144
182
|
compactionScheduled = false;
|
|
145
183
|
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, `compaction failed: ${error.message}`);
|
package/pi-router/src/config.ts
CHANGED
|
@@ -71,6 +71,11 @@ export function getKeyFilePath(): string {
|
|
|
71
71
|
return process.env.WEAVE_ROUTER_KEY_FILE?.trim() || path.join(getAgentDir(), ".weave_router_key");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** Persisted LSP preferences (per-language "don't offer again" dismissals). */
|
|
75
|
+
export function getLspPrefsPath(): string {
|
|
76
|
+
return path.join(getAgentDir(), ".weave_lsp.json");
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
interface WeaveProviderConfig {
|
|
75
80
|
baseUrl?: string;
|
|
76
81
|
apiKey?: string;
|
|
@@ -220,12 +225,17 @@ export function providerHeaders(role: Role, key: string): Record<string, string>
|
|
|
220
225
|
// dispatched child (and a `pi -e` smoke test with no models.json) can still
|
|
221
226
|
// resolve `weave/<model>`. The list mirrors the installer's headline models;
|
|
222
227
|
// the router re-routes every request regardless, so this is a UX/label surface.
|
|
228
|
+
// The context windows mirror the router's catalog for these models (see
|
|
229
|
+
// internal/router/catalog) so the footer and auto-compaction budget reflect
|
|
230
|
+
// what the router actually serves; per-request reroutes can still land on a
|
|
231
|
+
// different window, which the served-window header reports on the response.
|
|
223
232
|
export const WEAVE_MODELS: ProviderModelConfig[] = [
|
|
224
|
-
model("claude-opus-4-8", "Claude Opus 4.8 (via Weave Router)", 64000),
|
|
225
|
-
model("claude-opus-4-7", "Claude Opus 4.7 (via Weave Router)", 64000),
|
|
226
|
-
model("claude-sonnet-4-6", "Claude Sonnet 4.6 (via Weave Router)", 64000),
|
|
227
|
-
model("claude-haiku-4-5", "Claude Haiku 4.5 (via Weave Router)", 32000),
|
|
228
|
-
model("grok-4.6", "Grok 4.6 (via Weave Router)", 131072,
|
|
233
|
+
model("claude-opus-4-8", "Claude Opus 4.8 (via Weave Router)", 64000, 1_000_000),
|
|
234
|
+
model("claude-opus-4-7", "Claude Opus 4.7 (via Weave Router)", 64000, 1_000_000),
|
|
235
|
+
model("claude-sonnet-4-6", "Claude Sonnet 4.6 (via Weave Router)", 64000, 1_000_000),
|
|
236
|
+
model("claude-haiku-4-5", "Claude Haiku 4.5 (via Weave Router)", 32000, 200_000),
|
|
237
|
+
model("grok-4.6", "Grok 4.6 (via Weave Router)", 131072, 500_000),
|
|
238
|
+
model("qwen/qwen3.8-max", "Qwen 3.8 Max (via Weave Router)", 131072, 1_000_000),
|
|
229
239
|
];
|
|
230
240
|
|
|
231
241
|
function model(id: string, name: string, maxTokens: number, contextWindow: number = 200000): ProviderModelConfig {
|
|
@@ -246,6 +256,8 @@ function model(id: string, name: string, maxTokens: number, contextWindow: numbe
|
|
|
246
256
|
export const ROUTED_MODEL_HEADER = (process.env.WEAVE_ROUTED_MODEL_HEADER || "x-router-model").toLowerCase();
|
|
247
257
|
export const ROUTED_PROVIDER_HEADER = "x-router-provider";
|
|
248
258
|
export const ROUTER_DECISION_HEADER = "x-router-decision";
|
|
259
|
+
/** Context window (tokens) of the model that actually served the routed response. */
|
|
260
|
+
export const ROUTED_CONTEXT_WINDOW_HEADER = "x-router-context-window";
|
|
249
261
|
/** Marker a headless child prints to stderr so the parent dispatch can read its routed model. */
|
|
250
262
|
export const ROUTED_MODEL_STDERR_PREFIX = "weave-routed-model:";
|
|
251
263
|
|
|
@@ -255,6 +267,23 @@ export const MAX_SUBAGENTS = 8;
|
|
|
255
267
|
export const SUBAGENT_TIMEOUT_MS = Math.max(1000, numEnv("WEAVE_PI_SUBAGENT_TIMEOUT_MS", 600000));
|
|
256
268
|
export const DEFAULT_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
257
269
|
|
|
270
|
+
// ---------- LSP tunables ----------
|
|
271
|
+
|
|
272
|
+
/** Idle window before an unused language server is shut down. */
|
|
273
|
+
export const LSP_IDLE_MS = Math.max(10_000, numEnv("WEAVE_PI_LSP_IDLE_MS", 300_000));
|
|
274
|
+
export const LSP_REQUEST_TIMEOUT_MS = Math.max(1000, numEnv("WEAVE_PI_LSP_REQUEST_TIMEOUT_MS", 15_000));
|
|
275
|
+
/** Budget for `initialize` and the first query, which race the server's initial indexing pass. */
|
|
276
|
+
export const LSP_WARMUP_TIMEOUT_MS = Math.max(5000, numEnv("WEAVE_PI_LSP_WARMUP_TIMEOUT_MS", 60_000));
|
|
277
|
+
export const LSP_DIAGNOSTICS_WAIT_MS = Math.max(500, numEnv("WEAVE_PI_LSP_DIAGNOSTICS_WAIT_MS", 3000));
|
|
278
|
+
export const LSP_MAX_SERVERS = Math.max(1, numEnv("WEAVE_PI_LSP_MAX_SERVERS", 4));
|
|
279
|
+
export const LSP_MAX_REFERENCES = Math.max(1, numEnv("WEAVE_PI_LSP_MAX_REFERENCES", 100));
|
|
280
|
+
|
|
281
|
+
// Set by dispatch on children so a subagent reaches the parent's warm server
|
|
282
|
+
// pool over a local socket instead of cold-starting its own gopls. Internal:
|
|
283
|
+
// never set these yourself.
|
|
284
|
+
export const LSP_BROKER_ENV = "WEAVE_PI_LSP_BROKER";
|
|
285
|
+
export const LSP_BROKER_TOKEN_ENV = "WEAVE_PI_LSP_BROKER_TOKEN";
|
|
286
|
+
|
|
258
287
|
// Tools that let a subagent mutate the filesystem or run arbitrary commands.
|
|
259
288
|
// dispatch strips these from model-requested per-task `tools` (and downgrades
|
|
260
289
|
// readOnly:false's "all tools") unless WEAVE_PI_ALLOW_SUBAGENT_TOOLS=1, so a
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router-served context-window parsing shared by the model registry and
|
|
3
|
+
* compaction guard. The response header is the source of truth for the model
|
|
4
|
+
* that actually served a routed request.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Parse a router context-window header without accepting fractional or unsafe values. */
|
|
8
|
+
export function parseRoutedContextWindow(value: string | undefined): number | undefined {
|
|
9
|
+
if (!value || !/^[1-9]\d*$/.test(value)) return undefined;
|
|
10
|
+
const contextWindow = Number(value);
|
|
11
|
+
return Number.isSafeInteger(contextWindow) ? contextWindow : undefined;
|
|
12
|
+
}
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
DEFAULT_READONLY_TOOLS,
|
|
26
26
|
DISPATCH_CONCURRENCY,
|
|
27
27
|
getRouterBaseUrl,
|
|
28
|
+
LSP_BROKER_ENV,
|
|
29
|
+
LSP_BROKER_TOKEN_ENV,
|
|
28
30
|
MAX_SUBAGENTS,
|
|
29
31
|
PROVIDER_NAME,
|
|
30
32
|
resolveIdentity,
|
|
@@ -34,6 +36,15 @@ import {
|
|
|
34
36
|
SUBAGENT_TIMEOUT_MS,
|
|
35
37
|
} from "./config.js";
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The slice of the LSP subsystem dispatch depends on: hand me the env a child
|
|
41
|
+
* needs to reach a warm server pool. Declared here, by the consumer, so
|
|
42
|
+
* dispatch and lsp stay unaware of each other and index.ts does the wiring.
|
|
43
|
+
*/
|
|
44
|
+
export interface SubagentEnvProvider {
|
|
45
|
+
ensure(): Promise<Record<string, string>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
const SIGKILL_GRACE_MS = 5000;
|
|
38
49
|
|
|
39
50
|
const TaskItem = Type.Object({
|
|
@@ -123,6 +134,7 @@ function runChild(
|
|
|
123
134
|
key: string,
|
|
124
135
|
signal: AbortSignal | undefined,
|
|
125
136
|
index: number,
|
|
137
|
+
lspEnv: Record<string, string> | undefined,
|
|
126
138
|
): Promise<ChildResult> {
|
|
127
139
|
const args = ["--print", "--mode", "json", "--no-session", "-e", selfPath, "--model", `${PROVIDER_NAME}/${SUBAGENT_MODEL}`];
|
|
128
140
|
|
|
@@ -147,12 +159,17 @@ function runChild(
|
|
|
147
159
|
tools = tools.map((t) => t.trim()).filter((t) => t !== "" && !DANGEROUS_SUBAGENT_TOOLS.has(t.toLowerCase()));
|
|
148
160
|
if (tools.length === 0) tools = DEFAULT_READONLY_TOOLS;
|
|
149
161
|
}
|
|
162
|
+
// All five lsp operations are read-only, so granting it does not widen the
|
|
163
|
+
// capability gate above. Only offered when the parent actually has a broker
|
|
164
|
+
// listening; otherwise the child's tool list is exactly what it is today.
|
|
165
|
+
if (tools && lspEnv && !tools.includes("lsp")) tools = [...tools, "lsp"];
|
|
150
166
|
if (tools) args.push("--tools", tools.join(","));
|
|
151
167
|
args.push(task.prompt);
|
|
152
168
|
|
|
153
169
|
const identity = resolveIdentity();
|
|
154
170
|
const env: NodeJS.ProcessEnv = {
|
|
155
171
|
...process.env,
|
|
172
|
+
...lspEnv,
|
|
156
173
|
WEAVE_PI_SUBAGENT: "1",
|
|
157
174
|
WEAVE_PI_SUBAGENT_ID: randomUUID(),
|
|
158
175
|
WEAVE_ROUTER_KEY: key,
|
|
@@ -165,6 +182,12 @@ function runChild(
|
|
|
165
182
|
delete env.WEAVE_ROUTING_SPEED_WEIGHT;
|
|
166
183
|
delete env.WEAVE_ROUTING_OUTPUT_COST_RATIO;
|
|
167
184
|
delete env.WEAVE_ROUTING_EXPECTED_OUTPUT_TOKENS;
|
|
185
|
+
// With no broker there must be no broker env at all, so a child never tries
|
|
186
|
+
// to dial a socket that isn't there.
|
|
187
|
+
if (!lspEnv) {
|
|
188
|
+
delete env[LSP_BROKER_ENV];
|
|
189
|
+
delete env[LSP_BROKER_TOKEN_ENV];
|
|
190
|
+
}
|
|
168
191
|
if (identity.email) env.WEAVE_USER_EMAIL = identity.email;
|
|
169
192
|
if (identity.name) env.WEAVE_USER_NAME = identity.name;
|
|
170
193
|
|
|
@@ -282,7 +305,7 @@ function lastStderrLine(stderr: string): string {
|
|
|
282
305
|
return lines.length > 0 ? lines[lines.length - 1] : "";
|
|
283
306
|
}
|
|
284
307
|
|
|
285
|
-
export function registerDispatch(pi: ExtensionAPI, selfPath: string): void {
|
|
308
|
+
export function registerDispatch(pi: ExtensionAPI, selfPath: string, lspBroker?: SubagentEnvProvider): void {
|
|
286
309
|
pi.registerTool({
|
|
287
310
|
name: "dispatch",
|
|
288
311
|
label: "Dispatch",
|
|
@@ -308,9 +331,19 @@ export function registerDispatch(pi: ExtensionAPI, selfPath: string): void {
|
|
|
308
331
|
};
|
|
309
332
|
}
|
|
310
333
|
|
|
334
|
+
// Start the broker before fan-out so N children share the parent's warm
|
|
335
|
+
// language servers instead of each cold-starting its own. A broker that
|
|
336
|
+
// will not start is not fatal: children just run without the lsp tool.
|
|
337
|
+
let lspEnv: Record<string, string> | undefined;
|
|
338
|
+
try {
|
|
339
|
+
lspEnv = await lspBroker?.ensure();
|
|
340
|
+
} catch {
|
|
341
|
+
lspEnv = undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
311
344
|
const readOnly = params.readOnly ?? true;
|
|
312
345
|
const results = await mapWithConcurrencyLimit(params.tasks, DISPATCH_CONCURRENCY, (task, index) =>
|
|
313
|
-
runChild(selfPath, task, readOnly, ctx.cwd, key, signal, index),
|
|
346
|
+
runChild(selfPath, task, readOnly, ctx.cwd, key, signal, index, lspEnv),
|
|
314
347
|
);
|
|
315
348
|
|
|
316
349
|
const succeeded = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
package/pi-router/src/index.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - compaction: protect long tool loops, then compact routed context.
|
|
13
13
|
* - dispatch: parallel, context-isolated subagents — top-level process
|
|
14
14
|
* only (no grandchildren).
|
|
15
|
+
* - lsp: code intelligence (definition/references/hover/symbols/
|
|
16
|
+
* diagnostics) over lazily spawned language servers, shared
|
|
17
|
+
* with subagents through a parent-side broker socket.
|
|
15
18
|
*
|
|
16
19
|
* The same module loads in dispatched children via `-e <self>`; WEAVE_PI_SUBAGENT
|
|
17
20
|
* flips the provider knobs and suppresses the dispatch tool so fan-out doesn't recurse.
|
|
@@ -23,6 +26,7 @@ import { isSubagent } from "./config.js";
|
|
|
23
26
|
import { registerCompaction } from "./compaction.js";
|
|
24
27
|
import { registerDispatch } from "./dispatch.js";
|
|
25
28
|
import { registerForceModelCommands } from "./force-model.js";
|
|
29
|
+
import { registerLsp } from "./lsp.js";
|
|
26
30
|
import { registerMetadata } from "./metadata.js";
|
|
27
31
|
import { registerRoutedModel } from "./routed-model.js";
|
|
28
32
|
import { registerSafety } from "./safety.js";
|
|
@@ -44,8 +48,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
44
48
|
|
|
45
49
|
if (process.env.WEAVE_NO_SAFETY !== "1") registerSafety(pi);
|
|
46
50
|
|
|
51
|
+
// registerLsp picks its own role: the main process gets the pool-backed tool
|
|
52
|
+
// and returns the broker provider below; a child gets the broker-backed tool
|
|
53
|
+
// only when dispatch actually handed it a socket.
|
|
54
|
+
const lspBroker = process.env.WEAVE_PI_NO_LSP === "1" ? undefined : registerLsp(pi);
|
|
55
|
+
|
|
47
56
|
// Only the top-level process fans out. Children (WEAVE_PI_SUBAGENT=1) load
|
|
48
57
|
// this same extension but get no dispatch tool, so subagents can't spawn
|
|
49
58
|
// grandchildren.
|
|
50
|
-
if (!isSubagent()) registerDispatch(pi, SELF_PATH);
|
|
59
|
+
if (!isSubagent()) registerDispatch(pi, SELF_PATH, lspBroker);
|
|
51
60
|
}
|