pi-minimal-dispatch 0.2.0 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 — 2026-10-26
4
+
5
+ - Status lines (`lanes` status/wait, `/lanes`) tag each lane with its model:
6
+ `[short-id think:level]` after the name — provider prefix dropped, thinking
7
+ level only when set. The tag shows the actual model from the lane's last
8
+ assistant message, falling back to the pinned value until the first answer;
9
+ `LaneStatus` carries `model`/`thinkingLevel` (full ids) for structured
10
+ consumers.
11
+ - `dispatch`'s `model` param documents pi's `id:thinking` suffix
12
+ (e.g. `zai/glm-4.6:high`), which the tag renders split off the id.
13
+
3
14
  ## 0.2.0 — 2026-10-23
4
15
 
5
16
  Requires pi ≥ 1.4.0 (the MCP reach rules the validator encodes are pi
package/README.md CHANGED
@@ -10,8 +10,8 @@ never guessed from empty files.
10
10
 
11
11
  ```
12
12
  wave 'survey':
13
- docs · running · 2 turns · tok 5703in/90out, $0.0009 · stop:toolUse · last 3.1s ago
14
- review · settled · 4 turns · tok 12021in/310out, $0.0041 · stop:stop · last 12s ago
13
+ docs [glm-4.6 think:high] · running · 2 turns · tok 5703in/90out, $0.0009 · stop:toolUse · last 3.1s ago
14
+ review [glm-5.3-flash] · settled · 4 turns · tok 12021in/310out, $0.0041 · stop:stop · last 12s ago
15
15
  ```
16
16
 
17
17
  ## Why
@@ -75,8 +75,8 @@ pi decides when to call the tools. A real session looks like:
75
75
  manifest: /abs/path/to/.pi/work/survey-manifest.json
76
76
 
77
77
  > lanes wave="survey" action="wait"
78
- docs · settled · 2 turns · tok 5703in/90out, $0.0009 · stop:stop · last 0.8s ago
79
- review · settled · 4 turns · tok 12021in/310out, $0.0041 · stop:stop · last 2.1s ago
78
+ docs [glm-5.3-flash] · settled · 2 turns · tok 5703in/90out, $0.0009 · stop:stop · last 0.8s ago
79
+ review [glm-4.6] · settled · 4 turns · tok 12021in/310out, $0.0041 · stop:stop · last 2.1s ago
80
80
 
81
81
  > lanes wave="survey" action="collect"
82
82
  collect wave 'survey':
@@ -139,8 +139,11 @@ While any tracked lane runs, the footer shows one line — e.g. `lanes: 2 runnin
139
139
  — updated at the `pollMs` cadence and cleared when nothing is in flight.
140
140
  `/lanes` (optionally `/lanes <wave>`) prints the full status table for waves
141
141
  seen this session — an untracked wave name is resolved from the default
142
- workDir — and completes wave names. Both are TUI conveniences over the same
143
- derived state the `lanes` tool reports.
142
+ workDir — and completes wave names. Each line tags the lane with its model —
143
+ short id (provider prefix dropped), thinking level appended as `think:<level>`
144
+ when set — the actual model from the event stream once answers exist, the pinned
145
+ value before that. Both are TUI conveniences over the same derived state the
146
+ `lanes` tool reports.
144
147
 
145
148
  ## State model
146
149
 
package/docs/API.md CHANGED
@@ -20,7 +20,7 @@ the leash. Events stream to `<workDir>/<wave>-<name>.events.jsonl`.
20
20
  | `lanes` | array | — | One entry per lane (min 1); lane slugs unique within the wave |
21
21
  | `lanes[].name` | string | — | Lane slug: `kebab-case`, max 64 chars |
22
22
  | `lanes[].briefPath` | string | — | Path to the lane's brief file (its `@file` prompt) |
23
- | `lanes[].model` | string? | pi default | Pin a model id; omitted: pi's **configured default model** — not the dispatcher session's active model |
23
+ | `lanes[].model` | string? | pi default | Pin a model id; omitted: pi's **configured default model** — not the dispatcher session's active model. Accepts pi's `id:thinking` suffix (e.g. `zai/glm-4.6:high`) |
24
24
  | `lanes[].deadlineSec` | number? | `deadlineSec` config (900) | Leash in seconds; holds even if the dispatcher is gone. Minimum 1 |
25
25
  | `lanes[].tools` | string? | `read,grep,find,ls` | Tool allowlist for `--tools` — plain names or `*` patterns; no `+name`/`-name` modifiers, no shell quoting, not empty. Add `bash` when a lane must run commands (bash can still invoke git). With MCP enabled, reach requires `codemode` or `tool_search` here — see [MCP and web reach](#mcp-and-web-reach) |
26
26
  | `lanes[].noExtensions` | boolean? | false | Pass `-ne` (drop extensions incl. MCP in the lane) |
@@ -98,6 +98,8 @@ Per-lane `structuredContent` entries. Note: `settled` only means the
98
98
  | `overdue` | boolean? | alive past its deadline (leash misfire) — informational |
99
99
  | `turns` | number | turn count so far |
100
100
  | `usage` | object | run totals summed over assistant messages: `{input, output, cacheRead, cacheWrite, cost}` |
101
+ | `model` | string? | model answering the last assistant message; until the first assistant message, the pinned model with pi's `:thinking` suffix split off (last colon, valid level word only) |
102
+ | `thinkingLevel` | string? | thinking level of the last assistant message (`off`–`max`) — absent for models without thinking |
101
103
  | `stopReason` | string? | last assistant stop reason (`stop` is clean) |
102
104
  | `lastEventAgeSec` | number? | seconds since the events file last grew — the stuck-lane signal |
103
105
  | `spawnError` | string? | why a `spawn`-failed lane never started |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-minimal-dispatch",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Minimal detached lane dispatch with visible state for the pi coding agent — dispatch/lanes tools over a manifest + JSON-event protocol",
5
5
  "author": "Mike Cheung",
6
6
  "keywords": [
package/src/derive.ts CHANGED
@@ -45,6 +45,10 @@ export type LaneStatus = {
45
45
  overdue?: boolean;
46
46
  turns: number;
47
47
  usage: Usage;
48
+ /** Model answering the last assistant message; before the first, the pinned model (pi's `id:thinking` suffix split off). */
49
+ model?: string;
50
+ /** Thinking level of the last assistant message; absent for models without thinking. */
51
+ thinkingLevel?: string;
48
52
  stopReason?: string;
49
53
  lastEventAgeSec: number | null;
50
54
  spawnError?: string;
@@ -113,11 +117,16 @@ function errTailFor(lane: LaneRecord): string | undefined {
113
117
 
114
118
  /** Derive a lane's status from its events file, PID liveness, and manifest facts. Never stored. */
115
119
  export function deriveLane(lane: LaneRecord, now: number): LaneStatus {
120
+ // pi's --model splits a `:thinking` suffix only on the last colon and only for a valid
121
+ // level word (model-resolver's nSuffix) — ids like `qwen/qwen3-coder:exacto` keep their colon.
122
+ const pinned = lane.model?.match(/^(.+):(off|minimal|low|medium|high|xhigh|max)$/);
116
123
  const base: LaneStatus = {
117
124
  name: lane.name,
118
125
  state: "failed",
119
126
  turns: 0,
120
127
  usage: emptyUsage(),
128
+ model: pinned?.[1] ?? lane.model,
129
+ thinkingLevel: pinned?.[2],
121
130
  lastEventAgeSec: null,
122
131
  };
123
132
 
@@ -135,6 +144,12 @@ export function deriveLane(lane: LaneRecord, now: number): LaneStatus {
135
144
  base.usage.cacheWrite += u.cacheWrite ?? 0;
136
145
  base.usage.cost += u.cost?.total ?? 0;
137
146
  if (typeof message.stopReason === "string") base.stopReason = message.stopReason;
147
+ // The last assistant message is authoritative for identity — a message without
148
+ // thinkingLevel (model switch to a non-thinking model) clears a stale level.
149
+ if (typeof message.model === "string") {
150
+ base.model = message.model;
151
+ base.thinkingLevel = typeof message.thinkingLevel === "string" ? message.thinkingLevel : undefined;
152
+ }
138
153
  }
139
154
  }
140
155
  if (fs.existsSync(lane.eventsPath)) {
package/src/events.ts CHANGED
@@ -18,6 +18,8 @@ export type ContentBlock = { type: string; text?: string; thinking?: string };
18
18
  export type StreamMessage = {
19
19
  role: string;
20
20
  stopReason?: string;
21
+ model?: string;
22
+ thinkingLevel?: string;
21
23
  usage?: UsageDelta;
22
24
  content?: ContentBlock[];
23
25
  };
package/src/format.ts CHANGED
@@ -7,7 +7,11 @@ import type { LaneStatus } from "./derive.ts";
7
7
 
8
8
  /** One status-table line per lane (two-space indent, err tail four-space indented). */
9
9
  export function formatLaneLine(s: LaneStatus): string {
10
- const bits = [s.name, s.state];
10
+ // Short form: provider prefix dropped, thinking level labeled `think:` (a bare level word
11
+ // reads like a model variant) — the full ids live in the structured status payload.
12
+ const id = s.model ? s.model.split("/").pop() : undefined;
13
+ const tag = id === undefined ? "" : ` [${id}${s.thinkingLevel !== undefined ? ` think:${s.thinkingLevel}` : ""}]`;
14
+ const bits = [`${s.name}${tag}`, s.state];
11
15
  if (s.cause) bits.push(`(${s.cause}${s.spawnError ? `: ${s.spawnError}` : ""})`);
12
16
  if (s.overdue) bits.push("OVERDUE");
13
17
  bits.push(s.turns === 1 ? "1 turn" : `${s.turns} turns`);
package/src/index.ts CHANGED
@@ -108,7 +108,7 @@ const dispatchTool = defineTool({
108
108
  Type.Object({
109
109
  name: Type.String({ description: "Lane slug (kebab-case, max 64 chars, unique within the wave)" }),
110
110
  briefPath: Type.String({ description: "Path to the lane's brief file (@file prompt)" }),
111
- model: Type.Optional(Type.String({ description: "Pin a model id for this lane; omitted: pi's configured default model — not necessarily the dispatcher session's active model" })),
111
+ model: Type.Optional(Type.String({ description: "Pin a model id for this lane; omitted: pi's configured default model — not necessarily the dispatcher session's active model. Accepts pi's `id:thinking` suffix (e.g. 'zai/glm-4.6:high')" })),
112
112
  deadlineSec: Type.Optional(Type.Number({ description: "Deadline leash in seconds (config default: 900)", minimum: 1 })),
113
113
  tools: Type.Optional(Type.String({ description: "Tool allowlist for --tools; omitted: 'read,grep,find,ls' — add 'bash' when a lane must run commands (bash can still invoke git). Plain names or mcp__/tool patterns only: +name/-name modifiers, shell quoting, and empty lists are rejected. With mcp enabled, MCP/web tools are reachable only through 'codemode' or 'tool_search' named here ('mcp__server__*' entries declare tools directly only for direct-exposure servers)" })),
114
114
  noExtensions: Type.Optional(Type.Boolean({ description: "Pass -ne (drop extensions incl. MCP)" })),
@@ -209,6 +209,7 @@ const lanesTool = defineTool({
209
209
  description:
210
210
  "Observe/control a dispatched wave. action=status (default): per-lane derived state " +
211
211
  "(starting/running/settled/failed + cause spawn|killed|deadline|crashed), turns, run-total usage, " +
212
+ "model + thinking level (actual, from the last assistant message; pinned value before the first), " +
212
213
  "stopReason, last-event age, .err tail for crashed lanes. action=wait: hold this call open, " +
213
214
  "re-deriving at the pollMs cadence, until every lane is settled/failed or waitSec (default 300, " +
214
215
  "max 3600) elapses — then return the status table; replaces blind sleep-then-status polling. " +