@getpipher/armory-fleet 1.0.0 → 1.1.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/README.md +37 -4
- package/package.json +1 -1
- package/src/engine/retry-fallback.ts +14 -0
- package/src/engine/session-rejection.ts +23 -0
- package/src/engine/spawnSubagent.ts +77 -17
- package/src/index.ts +98 -5
- package/src/panel/fleet-panel.ts +8 -1
- package/src/panel/rows.ts +5 -1
- package/src/panel/runs-rows.ts +5 -1
- package/src/panel/widget-rows.ts +7 -1
- package/src/rpc/rpc-server.ts +57 -7
- package/src/runtime/async-runner.ts +7 -1
- package/src/runtime/run-log.ts +8 -0
- package/src/settings/fleet-settings.ts +103 -0
- package/src/tools/subagent.ts +8 -1
- package/src/workflows/runtime/adapters.ts +3 -0
package/README.md
CHANGED
|
@@ -254,6 +254,20 @@ Models are an ordered fallback chain (primary first; a spawn retries the next ca
|
|
|
254
254
|
|
|
255
255
|
Live cost $ and context % are tracked per run and surfaced in the Tiers view. Override per-run with `model`, or let the tier registry route based on the task class.
|
|
256
256
|
|
|
257
|
+
### Fleet settings (v1.1.0)
|
|
258
|
+
|
|
259
|
+
`~/.pi/agent/fleet/settings.json` (global) and `<project>/.pi/fleet/settings.json` (project — wins per-field) hold fleet-wide defaults that aren't env-shaped. Currently one field:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"defaultSubagentThinking": "high"
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`defaultSubagentThinking` (#78) sets the thinking level for **every subagent whose frontmatter does not pin `thinkingLevel`** — so a controller can think at `max` while the fleet runs at `high`, without replacing builtin agents (agent-frontmatter overrides still win; per-dispatch overrides are not exposed yet). Valid values: `off | minimal | low | medium | high | xhigh | max`. On flat subscriptions (z.ai/GLM coding plans, Ollama) this is the quota lever — thinking tokens on every cheap subagent call are pure waste.
|
|
268
|
+
|
|
269
|
+
Invalid values and unknown keys produce a startup warning naming the file, the field, and the bad value — never a silent no-op. Absent files are normal.
|
|
270
|
+
|
|
257
271
|
### Operational runtime
|
|
258
272
|
|
|
259
273
|
`src/runtime/` — the async/scheduling spine:
|
|
@@ -350,9 +364,20 @@ monotonic, one space per source store):
|
|
|
350
364
|
|
|
351
365
|
Emit `{ id, verb, params }`, get exactly one reply `{ id, ok, data }` or
|
|
352
366
|
`{ id, ok: false, error: { code, message } }`. Verbs: `spawn` (returns `{ runId }`
|
|
353
|
-
immediately; result arrives via `fleet:run:ended`), `
|
|
354
|
-
|
|
355
|
-
`
|
|
367
|
+
immediately; result arrives via `fleet:run:ended`), `schedule` (#83 — returns
|
|
368
|
+
`{ scheduleId, nextFire }`; schedules run lifecycles, not single delegates), `status`,
|
|
369
|
+
`observe` (replay dump — subscribe to the broadcast channels + dedupe by
|
|
370
|
+
`(channel, runId, seq)` for the live tail), `steer`, `abort`.
|
|
371
|
+
|
|
372
|
+
`spawn` params: `agent`, `task`, `background?`, `lifecycle?` (named lifecycle — with
|
|
373
|
+
`background: true` routes through the bg runner; without it runs as a detached
|
|
374
|
+
foreground-semantics lifecycle), `modelFallback?` (per-request retry-once on a retryable
|
|
375
|
+
provider failure — the retry mints a fresh runId and relinks the primary's todo), `cwd?`,
|
|
376
|
+
`isolation?`, `maxTurns?`, `model?`, `skills?`, `readOnly?`, `track?`, `todoId?`.
|
|
377
|
+
|
|
378
|
+
`schedule` params: `task`, `expression` (cron or interval), `lifecycle?`, `auto?`,
|
|
379
|
+
`isolation?`, `cwd?` — no `agent`: the scheduler runs lifecycles only. Registration emits
|
|
380
|
+
no run events; on fire, the normal `fleet:*` stream flows with `mode: "scheduled"`.
|
|
356
381
|
|
|
357
382
|
Error codes: `E-CONTROL-DISABLED`, `E-RUN-NOT-FOUND`, `E-RUN-FINISHED`, `E-BAD-VERB`,
|
|
358
383
|
`E-BAD-PARAMS`, `E-STEER-UNSUPPORTED`, `E-INTERNAL`.
|
|
@@ -381,7 +406,7 @@ export function fleetRpc(pi: { events: { emit(c: string, d: unknown): void; on(c
|
|
|
381
406
|
|
|
382
407
|
### Control gate
|
|
383
408
|
|
|
384
|
-
`spawn`/`steer`/`abort` are on by default. Set `ARMORY_FLEET_RPC_CONTROL=0` (or `false`) to
|
|
409
|
+
`spawn`/`steer`/`abort`/`schedule` are on by default. Set `ARMORY_FLEET_RPC_CONTROL=0` (or `false`) to
|
|
385
410
|
reject them with `E-CONTROL-DISABLED`; read-only `observe`/`status` stay available. Honest
|
|
386
411
|
threat model: in-process extensions already have full system access through pi itself — the
|
|
387
412
|
switch guards accidents, not adversaries.
|
|
@@ -400,6 +425,14 @@ The #62 tail of SPEC-6-5: `subagent({ cwd })` now scopes **background and schedu
|
|
|
400
425
|
- **Per-dispatch worktrees** — `isolation: 'worktree'` / `'auto'` resolve against the **dispatch cwd's repo**, not the session's; cross-cwd worktrees land in `<child-cwd>/.pi/fleet/worktrees/` and are cleaned up via the same service.
|
|
401
426
|
- **bg lifecycle parity** — the bg adapter now honors the lifecycle cwd chain (`lifecycle.cwd` → dispatch `cwd`), which also fixes a pre-existing mismatch where bg isolated runs spawned phases in the session cwd but committed in the worktree.
|
|
402
427
|
|
|
428
|
+
## RPC parity + fleet settings (v1.1.0)
|
|
429
|
+
|
|
430
|
+
Post-v1.0 batch (issues #78/#83):
|
|
431
|
+
|
|
432
|
+
- **`fleet.defaultSubagentThinking` (#78)** — a fleet-dir settings home (`~/.pi/agent/fleet/settings.json` global, `<project>/.pi/fleet/settings.json` project override) whose first field sets the thinking level for every subagent that doesn't pin its own — the quota lever for flat subscriptions. See [Fleet settings](#fleet-settings-v110).
|
|
433
|
+
- **RPC spawn parity (#83)** — `spawn` gains `lifecycle` (bg routing, or a detached foreground-semantics lifecycle run) and `modelFallback` (per-request retry-once; the retry mints a fresh runId and relinks the primary's todo).
|
|
434
|
+
- **New `schedule` verb (#83)** — `{ task, expression, lifecycle?, auto?, isolation?, cwd? }` → `{ scheduleId, nextFire }`. Schedules run lifecycles only, so `spawn`'s uniform `{ runId }` reply stays unbranched; the frozen surface needed zero renames and zero new error codes.
|
|
435
|
+
|
|
403
436
|
## Provider-agnostic tiers (v0.15.0)
|
|
404
437
|
|
|
405
438
|
Small-backlog batch (issues #57/#63/#64/#65):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,3 +31,17 @@ export function withModelFallbackRetry(spawn: SpawnFn, fallback: string | undefi
|
|
|
31
31
|
return first;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** #83 D3: the RPC foreground-semantics detached spawn's retry — mirrors the tool's direct-path
|
|
36
|
+
* contract (retry ONCE, retryable failures only, DISTINCT fallback model) but the retry runs
|
|
37
|
+
* WITHOUT the pre-minted runId: the primary already journaled run:started/ended under it, so
|
|
38
|
+
* the retry must mint a fresh id (a reused id would double-emit run:started). `todoId` links
|
|
39
|
+
* the retry to the primary's armory-todo task (same relink contract as the tool). */
|
|
40
|
+
export async function retryForegroundOnce(
|
|
41
|
+
primary: SpawnResult,
|
|
42
|
+
fallback: string | undefined,
|
|
43
|
+
spawn: (o: { model: string; todoId?: string }) => Promise<SpawnResult>,
|
|
44
|
+
): Promise<SpawnResult> {
|
|
45
|
+
if (!fallback || primary.status !== "failed" || !primary.retryable || fallback === primary.model) return primary;
|
|
46
|
+
return spawn({ model: fallback, todoId: primary.todoId ?? undefined });
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/engine/session-rejection.ts
|
|
2
|
+
// #84: typed rejections for the live-session control path (steer/abort). The RPC verbs
|
|
3
|
+
// previously classified failures by fragile message-substring matching; the handles now
|
|
4
|
+
// throw this class and the verbs match on `reason` first. Message TEXT is unchanged at
|
|
5
|
+
// the existing throw sites — string matching survives as a back-compat fallback for
|
|
6
|
+
// third-party ChildSession implementations that bubble bare Errors.
|
|
7
|
+
export type SessionRejectionReason =
|
|
8
|
+
| "steer-unsupported" // the backend has no steer (e.g. claude children)
|
|
9
|
+
| "already-processing" // the session is mid-turn and cannot take the steer now
|
|
10
|
+
| "already-aborted"; // the session was already stopped
|
|
11
|
+
|
|
12
|
+
export class SessionRejectionError extends Error {
|
|
13
|
+
readonly reason: SessionRejectionReason;
|
|
14
|
+
constructor(reason: SessionRejectionReason, message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "SessionRejectionError";
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isSessionRejection(e: unknown): e is SessionRejectionError {
|
|
22
|
+
return e instanceof SessionRejectionError;
|
|
23
|
+
}
|
|
@@ -7,6 +7,7 @@ import type { BackendRegistry } from "../backend/port.ts";
|
|
|
7
7
|
import { genRunId, RunRegistry } from "./run-registry.ts";
|
|
8
8
|
import type { RunRecord } from "./run-registry.ts";
|
|
9
9
|
import { createTurnBudget, DEFAULT_MAX_TURNS } from "./turn-budget.ts";
|
|
10
|
+
import { SessionRejectionError } from "./session-rejection.ts";
|
|
10
11
|
import type { ForegroundLock } from "./concurrency-lock.ts";
|
|
11
12
|
import type { RunLog } from "../runtime/run-log.ts";
|
|
12
13
|
import { buildToolEvent } from "../runtime/run-log.ts";
|
|
@@ -83,7 +84,7 @@ export interface LiveSessionHandle {
|
|
|
83
84
|
* `supportsSteer` is derived from whether the backend implemented the optional `steer`. */
|
|
84
85
|
export function toLiveHandle(session: ChildSession): LiveSessionHandle {
|
|
85
86
|
return {
|
|
86
|
-
steer: (text) => session.steer ? session.steer(text) : Promise.reject(new
|
|
87
|
+
steer: (text) => session.steer ? session.steer(text) : Promise.reject(new SessionRejectionError("steer-unsupported", "steer not supported on this backend")),
|
|
87
88
|
abort: () => session.abort(),
|
|
88
89
|
subscribe: (h) => session.subscribe(h),
|
|
89
90
|
get isStreaming() { return session.isStreaming ?? false; },
|
|
@@ -158,6 +159,10 @@ export interface SpawnOptions {
|
|
|
158
159
|
modelRegistry?: ModelRegistryLike;
|
|
159
160
|
/** SPEC-6-3: workflow adapter tier override — replaces agent.tier before model resolution. */
|
|
160
161
|
tierOverride?: string;
|
|
162
|
+
/** #78: fleet-wide default thinking level (`fleet.defaultSubagentThinking` settings field).
|
|
163
|
+
* Applied only when the agent frontmatter does NOT pin `thinkingLevel` — precedence:
|
|
164
|
+
* agent.thinkingLevel > opts.defaultThinkingLevel > backend/session default. */
|
|
165
|
+
defaultThinkingLevel?: ThinkingLevel;
|
|
161
166
|
/** #31: when true, the caller asserts this dispatch will NOT mutate the working directory
|
|
162
167
|
* (review/audit/research). Read-only dispatches bypass the foreground single-slot lock so
|
|
163
168
|
* multiple readOnly dispatches — and/or a readOnly dispatch alongside a write dispatch — can
|
|
@@ -201,7 +206,7 @@ export interface SpawnResult {
|
|
|
201
206
|
/** #49: extract file paths a tool event touched, for the structured partial-result report.
|
|
202
207
|
* edit/write carry a `path` arg reliably; bash is best-effort (redirections `>`/`>>` + `tee` to a
|
|
203
208
|
* path-like token). Reads are NOT mutations and are excluded. */
|
|
204
|
-
function extractTouchedFiles(toolName: string, args: unknown): string[] {
|
|
209
|
+
export function extractTouchedFiles(toolName: string, args: unknown): string[] {
|
|
205
210
|
if (!args || typeof args !== "object") return [];
|
|
206
211
|
const a = args as Record<string, unknown>;
|
|
207
212
|
// Case-insensitive: pi tools are lowercase ("edit"), claude's are capitalized ("Edit").
|
|
@@ -219,19 +224,41 @@ function extractTouchedFiles(toolName: string, args: unknown): string[] {
|
|
|
219
224
|
const redir = />>?\s+([^\s|;&<>]+)/g;
|
|
220
225
|
let m: RegExpExecArray | null;
|
|
221
226
|
while ((m = redir.exec(cmd)) !== null) {
|
|
222
|
-
const tok = m[1];
|
|
223
|
-
if (tok
|
|
227
|
+
const tok = m[1] ? shapeToken(m[1]) : undefined;
|
|
228
|
+
if (tok) out.push(tok);
|
|
224
229
|
}
|
|
225
230
|
const tee = /\btee\s+(?:-a\s+)?([^\s|;&<>]+)/g;
|
|
226
231
|
while ((m = tee.exec(cmd)) !== null) {
|
|
227
|
-
const tok = m[1];
|
|
228
|
-
if (tok
|
|
232
|
+
const tok = m[1] ? shapeToken(m[1]) : undefined;
|
|
233
|
+
if (tok) out.push(tok);
|
|
229
234
|
}
|
|
230
235
|
return out;
|
|
231
236
|
}
|
|
232
237
|
return [];
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
/** #87: decide whether a redirect/tee capture is plausibly a file path. The bare `/[/.]/`
|
|
241
|
+
* test false-positived on `>` characters inside quoted strings/heredocs/code text
|
|
242
|
+
* ("cache.load(name,,", "[...active],,") and swallowed trailing punctuation from code-y
|
|
243
|
+
* commands ("/tmp/out.txt),"). Now: strip wrapping quotes, trim trailing punctuation,
|
|
244
|
+
* then require a real path shape (contains `/`, or a dotted filename, or a leading-dot
|
|
245
|
+
* file like .gitignore). */
|
|
246
|
+
function shapeToken(raw: string): string | undefined {
|
|
247
|
+
let tok = raw.trim().replace(/^["']+/, "");
|
|
248
|
+
// trailing junk can interleave ("'/tmp/x.txt',") — strip quotes+punct until stable
|
|
249
|
+
let prev: string;
|
|
250
|
+
do {
|
|
251
|
+
prev = tok;
|
|
252
|
+
tok = tok.replace(/["',;)\]}]+$/g, "");
|
|
253
|
+
} while (tok !== prev);
|
|
254
|
+
if (!tok) return undefined;
|
|
255
|
+
if (/^\/dev\/(null|stdout|stderr|stdin|tty|zero)$/.test(tok)) return undefined; // pseudo-devices, not touched files
|
|
256
|
+
if (tok.includes("/")) return tok;
|
|
257
|
+
if (/^\.[A-Za-z0-9._-]+$/.test(tok)) return tok; // .gitignore and friends
|
|
258
|
+
if (/^[A-Za-z0-9._-]+\.[A-Za-z0-9]+$/.test(tok)) return tok; // name.ext
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
235
262
|
export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
236
263
|
const track = opts.track ?? true;
|
|
237
264
|
const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
@@ -324,7 +351,10 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
324
351
|
priorStatus = link.priorStatus;
|
|
325
352
|
opts.runRegistry.update(runId, { todoId });
|
|
326
353
|
} catch (e) {
|
|
327
|
-
return await finishRun(
|
|
354
|
+
return await finishRun({
|
|
355
|
+
opts, runId, startedAt, status: "failed", finalText: "", todoId, priorStatus,
|
|
356
|
+
error: (e as Error).message, agentName: agentDef.name, model,
|
|
357
|
+
});
|
|
328
358
|
}
|
|
329
359
|
|
|
330
360
|
// SPEC-6-1: fallback retry loop — try candidates[0], on rejection retry candidates[1], etc.
|
|
@@ -335,7 +365,8 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
335
365
|
const result = await backend.factory.create({
|
|
336
366
|
cwd: childCwd,
|
|
337
367
|
model: cand,
|
|
338
|
-
|
|
368
|
+
// #78: agent frontmatter wins; the fleet-wide default fills the gap.
|
|
369
|
+
thinkingLevel: childAgent.thinkingLevel ?? opts.defaultThinkingLevel,
|
|
339
370
|
tools,
|
|
340
371
|
rolePrompt: childAgent.rolePrompt,
|
|
341
372
|
skills: childAgent.skills ?? [],
|
|
@@ -349,7 +380,10 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
349
380
|
} catch (e) { lastErr = e as Error; }
|
|
350
381
|
}
|
|
351
382
|
if (!session) {
|
|
352
|
-
return await finishRun(
|
|
383
|
+
return await finishRun({
|
|
384
|
+
opts, runId, startedAt, status: "failed", finalText: "", todoId, priorStatus,
|
|
385
|
+
error: `backend create failed: ${lastErr?.message ?? "unknown"}`, agentName: agentDef.name, model,
|
|
386
|
+
});
|
|
353
387
|
}
|
|
354
388
|
|
|
355
389
|
// SPEC-5b-4: retain a narrow live-session handle on the run record so the panel can
|
|
@@ -554,7 +588,12 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
554
588
|
status = "completed";
|
|
555
589
|
}
|
|
556
590
|
|
|
557
|
-
return await finishRun(
|
|
591
|
+
return await finishRun({
|
|
592
|
+
opts, runId, startedAt, status, finalText, todoId, priorStatus, error,
|
|
593
|
+
agentName: agentDef.name, model, tokenTotal, costTotal, contextTokens,
|
|
594
|
+
retryable: modelError ? true : undefined, filesTouched: filesTouchedList,
|
|
595
|
+
reachedSummary, toolCallCount,
|
|
596
|
+
});
|
|
558
597
|
} finally {
|
|
559
598
|
// #31: a readOnly dispatch never acquired the lock — don't release what it didn't take
|
|
560
599
|
// (releasing a lock held by another concurrent write dispatch would corrupt serialization).
|
|
@@ -572,13 +611,34 @@ function fail(runId: string, startedAt: number, message: string, agent: string):
|
|
|
572
611
|
/** SPEC-6-2: guard against double-finishRun (abort-then-complete). */
|
|
573
612
|
const finalizedRunIds = new Set<string>();
|
|
574
613
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
614
|
+
/** #NIT: options object — was 17 positional params (3 call sites, error-prone at the tail). */
|
|
615
|
+
interface FinishRunArgs {
|
|
616
|
+
opts: SpawnOptions;
|
|
617
|
+
runId: string;
|
|
618
|
+
startedAt: number;
|
|
619
|
+
status: FleetRunStatus;
|
|
620
|
+
finalText: string;
|
|
621
|
+
todoId: string | null;
|
|
622
|
+
priorStatus?: string;
|
|
623
|
+
error?: string;
|
|
624
|
+
agentName: string;
|
|
625
|
+
model: string;
|
|
626
|
+
tokenTotal?: number;
|
|
627
|
+
costTotal?: number;
|
|
628
|
+
contextTokens?: number;
|
|
629
|
+
retryable?: boolean;
|
|
630
|
+
filesTouched?: string[];
|
|
631
|
+
reachedSummary?: boolean;
|
|
632
|
+
toolCallCount?: number;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function finishRun(a: FinishRunArgs): Promise<SpawnResult> {
|
|
636
|
+
const { opts, runId, startedAt, status, finalText, todoId, priorStatus, error, agentName, model } = a;
|
|
637
|
+
const tokenTotal = a.tokenTotal ?? 0;
|
|
638
|
+
const costTotal = a.costTotal ?? 0;
|
|
639
|
+
const contextTokens = a.contextTokens ?? 0;
|
|
640
|
+
const toolCallCount = a.toolCallCount ?? 0;
|
|
641
|
+
const { retryable, filesTouched, reachedSummary } = a;
|
|
582
642
|
if (finalizedRunIds.has(runId)) {
|
|
583
643
|
// Already finalized — return the existing registry record's result without re-appending.
|
|
584
644
|
const existing = opts.runRegistry.get(runId);
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
getAgentDir,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
|
-
import { createSubagentTool, resolveDispatchCwd, type SubagentToolDeps } from "./tools/subagent.ts";
|
|
11
|
+
import { createSubagentTool, mergeLifecycleSkills, resolveDispatchCwd, type SubagentToolDeps } from "./tools/subagent.ts";
|
|
12
12
|
// SPEC-6-3: /fleet uses openWorkflowPanelLoop (Task 12) instead of the raw openFleetPanel factory.
|
|
13
13
|
import { discoverAgents } from "./registry/discovery.ts";
|
|
14
14
|
import { RunRegistry } from "./engine/run-registry.ts";
|
|
15
|
+
import { SessionRejectionError } from "./engine/session-rejection.ts";
|
|
15
16
|
import { createSingleSlotLock, createForegroundLock } from "./engine/concurrency-lock.ts";
|
|
16
17
|
import { ArmoryTodoAdapter } from "./todo-sync/adapter.ts";
|
|
17
18
|
import { ArmoryMemoryAdapter } from "./memory-hydrate/adapter.ts";
|
|
@@ -52,6 +53,7 @@ import { FleetWidgetController } from "./panel/fleet-widget.ts";
|
|
|
52
53
|
import { TierRegistry, mergeTiers } from "./tiers/tier-registry.ts";
|
|
53
54
|
import { BUILTIN_TIERS } from "./tiers/builtin.ts";
|
|
54
55
|
import { TierStore } from "./tiers/tier-store.ts";
|
|
56
|
+
import { FleetSettingsStore } from "./settings/fleet-settings.ts";
|
|
55
57
|
import { splitModel } from "./tiers/resolve.ts";
|
|
56
58
|
import { WorkflowJournal } from "./workflows/journal.ts";
|
|
57
59
|
import { discoverWorkflows, WorkflowRegistry, type WorkflowDef } from "./workflows/registry.ts";
|
|
@@ -82,7 +84,8 @@ function wrapPiSession(inner: ChildSession, backendSessionId: string): ChildSess
|
|
|
82
84
|
return inner.subscribe(handler);
|
|
83
85
|
},
|
|
84
86
|
// SPEC-5b-4: forward the native SDK steer + isStreaming to the real pi session.
|
|
85
|
-
|
|
87
|
+
// #84: typed rejection (message text unchanged — back-compat with string matching).
|
|
88
|
+
steer: (t) => inner.steer ? inner.steer(t) : Promise.reject(new SessionRejectionError("steer-unsupported", "pi session has no steer")),
|
|
86
89
|
get isStreaming() { return inner.isStreaming ?? false; },
|
|
87
90
|
};
|
|
88
91
|
}
|
|
@@ -304,7 +307,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
304
307
|
// in-place runs so the cross-cwd surfacing (↗ glyph + sessionCwd audit) still fires.
|
|
305
308
|
cwd: isolated ? opts.worktreePath : o.cwd,
|
|
306
309
|
runLog: deps.runLog, tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
307
|
-
|
|
310
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
311
|
+
}), opts.modelFallback ?? deps.defaultModelFallback),
|
|
308
312
|
};
|
|
309
313
|
const res = await runLifecycle(task, lifecycleName, { deps: lifecycleFullDeps, mode: opts.mode, worktreePath: opts.worktreePath, baseRef: "HEAD", entryCwd: opts.entryCwd, onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" } });
|
|
310
314
|
return res as unknown as import("./runtime/async-runner.ts").FakeLifecycleResult;
|
|
@@ -429,33 +433,84 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
429
433
|
journal: fleetJournal,
|
|
430
434
|
parentCwd: ctx.cwd,
|
|
431
435
|
hasAsyncRunner: true,
|
|
436
|
+
// #83 D4: schedule registration — scheduler.register + nextFire lookup. Throws (invalid
|
|
437
|
+
// expression) propagate to the verb, which maps them to E-BAD-PARAMS.
|
|
438
|
+
schedule: (spec) => {
|
|
439
|
+
if (!deps.scheduler) throw new Error("scheduling not configured in this session (scheduler missing)");
|
|
440
|
+
const id = deps.scheduler.register({
|
|
441
|
+
task: String(spec.task),
|
|
442
|
+
expression: String(spec.expression),
|
|
443
|
+
...(typeof spec.lifecycle === "string" ? { lifecycle: spec.lifecycle } : {}),
|
|
444
|
+
...(typeof spec.auto === "boolean" ? { auto: spec.auto } : {}),
|
|
445
|
+
...(spec.isolation === "worktree" || spec.isolation === "none" || spec.isolation === "auto" ? { isolation: spec.isolation } : {}),
|
|
446
|
+
...(typeof spec.cwd === "string" ? { cwd: spec.cwd } : {}),
|
|
447
|
+
});
|
|
448
|
+
const entry = deps.scheduler.list().find((s) => s.id === id);
|
|
449
|
+
return { scheduleId: id, nextFire: entry?.nextFire?.toISOString() ?? null };
|
|
450
|
+
},
|
|
432
451
|
spawn: (params: Record<string, unknown>, runId: string) => {
|
|
433
452
|
// Detached fire-and-forget: NEVER throws — spawnSubagent journals its own fail path
|
|
434
453
|
// (run:ended + todo revert), so the caller's pre-minted runId always resolves to events.
|
|
435
454
|
void (async () => {
|
|
436
455
|
const requestedCwd = typeof params.cwd === "string" ? params.cwd : undefined;
|
|
437
456
|
const { cwd: resolvedCwd } = resolveDispatchCwd(requestedCwd, ctx.cwd);
|
|
457
|
+
// #83: lifecycle + per-request modelFallback params (validated by the verb; re-narrowed here).
|
|
458
|
+
const rpcLifecycle = typeof params.lifecycle === "string" && params.lifecycle ? params.lifecycle : "default";
|
|
459
|
+
const rpcFallback = typeof params.modelFallback === "string" && params.modelFallback ? params.modelFallback : undefined;
|
|
438
460
|
if (params.background === true) {
|
|
439
461
|
// RPC background: routed through the async runner (pool slot + isolation + origin),
|
|
440
462
|
// identical to the tool's runBackground path (spec §3.2). Ghost-runId prevention:
|
|
441
463
|
// a synchronous pre-flight failure journals run:ended failed under the caller's id.
|
|
442
464
|
const handle = runBackground(String(params.task), {
|
|
443
465
|
deps: deps.asyncRunner!,
|
|
444
|
-
lifecycle:
|
|
466
|
+
lifecycle: rpcLifecycle,
|
|
445
467
|
mode: "auto",
|
|
446
468
|
isolation: params.isolation as "worktree" | "none" | "auto" | undefined,
|
|
447
469
|
cwd: resolvedCwd ?? ctx.cwd,
|
|
448
470
|
origin: "background",
|
|
449
471
|
runId,
|
|
472
|
+
...(rpcFallback ? { modelFallback: rpcFallback } : {}),
|
|
450
473
|
});
|
|
451
474
|
if (handle.status === "failed") {
|
|
452
475
|
deps.runLog?.append(runId, { type: "run:ended", runId, status: "failed", endedAt: Date.now(), tokenTotal: 0, error: handle.error });
|
|
453
476
|
}
|
|
454
477
|
return;
|
|
455
478
|
}
|
|
479
|
+
if (params.lifecycle !== undefined) {
|
|
480
|
+
// #83 D2: lifecycle WITHOUT background = detached foreground-semantics lifecycle run
|
|
481
|
+
// (tool parity: mode "auto", failed-phase checkpoint aborts, session lock on the phase
|
|
482
|
+
// spawn, pre-minted runId via the genRunId override — same pattern as asyncRunLifecycle).
|
|
483
|
+
const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts");
|
|
484
|
+
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
485
|
+
const { withModelFallbackRetry } = await import("./engine/retry-fallback.ts");
|
|
486
|
+
const lifecycleFullDeps = {
|
|
487
|
+
...deps.lifecycleDeps,
|
|
488
|
+
genRunId: () => runId,
|
|
489
|
+
spawn: withModelFallbackRetry(async (o) => spawnSubagent({
|
|
490
|
+
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
491
|
+
skillsOverride: mergeLifecycleSkills(o.skills, Array.isArray(params.skills) ? params.skills as string[] : undefined),
|
|
492
|
+
backendOverride: o.backend,
|
|
493
|
+
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
494
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: ctx.cwd,
|
|
495
|
+
runLog: deps.runLog, signal: undefined,
|
|
496
|
+
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
497
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
498
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
499
|
+
readOnly: params.readOnly === true,
|
|
500
|
+
cwd: o.cwd ?? resolvedCwd,
|
|
501
|
+
}), rpcFallback ?? deps.defaultModelFallback),
|
|
502
|
+
};
|
|
503
|
+
const res = await runLifecycle(String(params.task), rpcLifecycle, {
|
|
504
|
+
deps: lifecycleFullDeps, mode: "auto", entryCwd: resolvedCwd,
|
|
505
|
+
onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" },
|
|
506
|
+
});
|
|
507
|
+
deps.lifecycleRuns.set(res.runId, res); // panel Lifecycle-view visibility (workflow-lambda precedent)
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
456
510
|
// Foreground-semantics detached spawn (no mode — "foreground" is the truth).
|
|
457
511
|
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
458
|
-
await
|
|
512
|
+
const { retryForegroundOnce } = await import("./engine/retry-fallback.ts");
|
|
513
|
+
const primary = await spawnSubagent({
|
|
459
514
|
agent: params.agent as string,
|
|
460
515
|
task: params.task as string,
|
|
461
516
|
todoId: typeof params.todoId === "string" ? params.todoId : undefined,
|
|
@@ -476,7 +531,33 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
476
531
|
modelRegistry: deps.modelRegistry,
|
|
477
532
|
cwd: resolvedCwd ?? ctx.cwd,
|
|
478
533
|
runId,
|
|
534
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
479
535
|
});
|
|
536
|
+
// #83 D3: per-request fallback retry — the retry mints a FRESH runId (the primary's
|
|
537
|
+
// pre-minted id stays retired; a reuse would double-emit run:started) and relinks the
|
|
538
|
+
// primary's todo, exactly like the tool's direct path.
|
|
539
|
+
await retryForegroundOnce(primary, rpcFallback, (o) => spawnSubagent({
|
|
540
|
+
agent: params.agent as string,
|
|
541
|
+
task: params.task as string,
|
|
542
|
+
todoId: o.todoId,
|
|
543
|
+
track: params.track === true,
|
|
544
|
+
model: o.model,
|
|
545
|
+
readOnly: params.readOnly === true,
|
|
546
|
+
skillsOverride: Array.isArray(params.skills) ? params.skills as string[] : undefined,
|
|
547
|
+
registry: deps.registry,
|
|
548
|
+
todoSync: deps.todoSync,
|
|
549
|
+
runRegistry: deps.runRegistry,
|
|
550
|
+
lock: deps.lock,
|
|
551
|
+
backendRegistry: deps.backendRegistry,
|
|
552
|
+
parentModel: deps.parentModel,
|
|
553
|
+
parentCwd: ctx.cwd,
|
|
554
|
+
runLog: deps.runLog,
|
|
555
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
556
|
+
tierRegistry: deps.tierRegistry,
|
|
557
|
+
modelRegistry: deps.modelRegistry,
|
|
558
|
+
cwd: resolvedCwd ?? ctx.cwd,
|
|
559
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
560
|
+
}));
|
|
480
561
|
})().catch(() => {});
|
|
481
562
|
},
|
|
482
563
|
});
|
|
@@ -521,6 +602,16 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
521
602
|
deps.reloadTiers = reloadTiers;
|
|
522
603
|
reloadTiers(); // build the real merged registry (replaces the builtin-only placeholder)
|
|
523
604
|
|
|
605
|
+
// #78: fleet-dir settings.json (global + project, project wins) — currently carries
|
|
606
|
+
// `defaultSubagentThinking`. Warnings are surfaced, never swallowed silently.
|
|
607
|
+
const fleetSettingsStore = new FleetSettingsStore({
|
|
608
|
+
projectPath: join(dir, "settings.json"),
|
|
609
|
+
globalPath: join(process.env.HOME ?? "", ".pi", "agent", "fleet", "settings.json"),
|
|
610
|
+
});
|
|
611
|
+
const fleetSettings = fleetSettingsStore.load();
|
|
612
|
+
for (const w of fleetSettings.warnings) ctx.ui.notify(w, "warning");
|
|
613
|
+
deps.defaultSubagentThinking = fleetSettings.settings.defaultSubagentThinking;
|
|
614
|
+
|
|
524
615
|
// SPEC-6-3: workflow journal + registry + runner + fleet tool wiring.
|
|
525
616
|
const workflowJournal = new WorkflowJournal(join(dir, "workflows"));
|
|
526
617
|
wfRegistry = new WorkflowRegistry(discoverWorkflows({
|
|
@@ -543,6 +634,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
543
634
|
runLog: deps.runLog,
|
|
544
635
|
tierRegistry: deps.tierRegistry,
|
|
545
636
|
modelRegistry: deps.modelRegistry,
|
|
637
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
546
638
|
lifecycleDeps: deps.lifecycleDeps,
|
|
547
639
|
spawnSubagentFn: async (opts) => { const { spawnSubagent } = await import("./engine/spawnSubagent.ts"); return spawnSubagent(opts); },
|
|
548
640
|
runLifecycleFn: async (task, name, lcOpts) => { const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts"); return runLifecycle(task, name, lcOpts); },
|
|
@@ -677,6 +769,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
677
769
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
678
770
|
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd,
|
|
679
771
|
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry, // SPEC-6-1
|
|
772
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
680
773
|
}), deps.defaultModelFallback),
|
|
681
774
|
};
|
|
682
775
|
const res = await runLifecycle(parsed.task, lcName, { deps: lifecycleFullDeps, mode: parsed.auto ? "auto" : "checkpointed", onCheckpoint });
|
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
matchesKey,
|
|
10
10
|
type SelectItem,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
12
|
+
import type { AgentDef, ThinkingLevel } from "../registry/frontmatter.ts";
|
|
13
13
|
import { agentsRow, agentInfo, backendsRow, backendInfo, lifecycleRow, lifecyclePhaseTimeline, scheduleRow } from "./rows.ts";
|
|
14
14
|
import { buildFleetItems } from "./fleet-items.ts";
|
|
15
15
|
import { runsRow, runTimelineRow } from "./runs-rows.ts";
|
|
@@ -46,6 +46,9 @@ export interface FleetPanelDeps {
|
|
|
46
46
|
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
47
47
|
parentModel: { provider: string; id: string };
|
|
48
48
|
parentCwd: string;
|
|
49
|
+
/** #78: fleet-wide default thinking level (settings.json `defaultSubagentThinking`).
|
|
50
|
+
* Threads to every panel-driven spawn (Run/Resume/Fork + lifecycle-view phases). */
|
|
51
|
+
defaultSubagentThinking?: ThinkingLevel;
|
|
49
52
|
/** SPEC-4: lifecycle registry + active/recent run records + deps to drive checkpoints. */
|
|
50
53
|
lifecycleRegistry: Map<string, LifecycleDef>;
|
|
51
54
|
lifecycleRuns: Map<string, LifecycleRunRecord>;
|
|
@@ -483,6 +486,7 @@ export class FleetPanel extends Container {
|
|
|
483
486
|
backendRegistry: this.deps.backendRegistry,
|
|
484
487
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
485
488
|
runLog: this.deps.runLog,
|
|
489
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
486
490
|
// live Fleet row during the run (SPEC-1 §4c) — re-render on each turn_end
|
|
487
491
|
onEvent: (e) => {
|
|
488
492
|
if (e.type === "turn_end") {
|
|
@@ -1117,6 +1121,7 @@ export class FleetPanel extends Container {
|
|
|
1117
1121
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry,
|
|
1118
1122
|
lock: this.deps.lock, backendRegistry: this.deps.backendRegistry,
|
|
1119
1123
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
1124
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
1120
1125
|
onEvent: (e) => { if (e.type === "turn_end") { this.list = this.buildList(); this.renderShell(); } },
|
|
1121
1126
|
});
|
|
1122
1127
|
this.list = this.buildList();
|
|
@@ -1156,6 +1161,7 @@ export class FleetPanel extends Container {
|
|
|
1156
1161
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry,
|
|
1157
1162
|
lock: this.deps.lock, backendRegistry: this.deps.backendRegistry,
|
|
1158
1163
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
1164
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
1159
1165
|
onEvent: (e) => { if (e.type === "turn_end") { this.list = this.buildList(); this.renderShell(); } },
|
|
1160
1166
|
});
|
|
1161
1167
|
this.list = this.buildList();
|
|
@@ -1228,6 +1234,7 @@ export class FleetPanel extends Container {
|
|
|
1228
1234
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
1229
1235
|
backendRegistry: this.deps.backendRegistry, parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
1230
1236
|
runLog: this.deps.runLog,
|
|
1237
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
1231
1238
|
cwd: o.cwd,
|
|
1232
1239
|
});
|
|
1233
1240
|
},
|
package/src/panel/rows.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/panel/rows.ts
|
|
2
|
+
import { basename } from "node:path";
|
|
2
3
|
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
3
4
|
import type { FleetRunStatus } from "../todo-sync/port.ts";
|
|
4
5
|
import type { RunRecord } from "../engine/run-registry.ts";
|
|
@@ -140,6 +141,8 @@ export interface ScheduleRow {
|
|
|
140
141
|
task: string;
|
|
141
142
|
nextFire: Date | null;
|
|
142
143
|
paused: boolean;
|
|
144
|
+
/** #62: pinned dispatch cwd — rendered as a ↗ basename so cross-cwd schedules are visible. */
|
|
145
|
+
cwd?: string;
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
export function scheduleRow(s: ScheduleRow): string {
|
|
@@ -147,7 +150,8 @@ export function scheduleRow(s: ScheduleRow): string {
|
|
|
147
150
|
const next = s.nextFire ? `next: ${s.nextFire.toLocaleString()}` : "paused";
|
|
148
151
|
const task = s.task.length > 24 ? s.task.slice(0, 23) + "…" : s.task;
|
|
149
152
|
const lc = s.lifecycle ?? "default";
|
|
150
|
-
|
|
153
|
+
const cwd = s.cwd ? ` ↗${basename(s.cwd)}` : "";
|
|
154
|
+
return `${icon} ${s.expression} ${lc} "${task}"${cwd} ${next} ${s.id}`;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
const LC_GLYPH: Record<LifecycleStatus, string> = {
|
package/src/panel/runs-rows.ts
CHANGED
|
@@ -16,9 +16,13 @@ export function runsRow(r: RunMeta, getModelContextWindow?: (model: string) => n
|
|
|
16
16
|
const maxCtx = getModelContextWindow?.(r.model);
|
|
17
17
|
const ctx = (r.contextTokens != null && maxCtx != null && maxCtx > 0) ? ` ${Math.round(r.contextTokens / maxCtx * 100)}%` : "";
|
|
18
18
|
const cost = r.costTotal ? ` $${r.costTotal.toFixed(4)}` : "";
|
|
19
|
+
// #59/#60/#61 NIT: the journal fields v0.14.0 added to run:ended, now visible in the list.
|
|
20
|
+
const err = r.error ? ` ✗"${r.error.length > 60 ? r.error.slice(0, 59) + "…" : r.error}"` : "";
|
|
21
|
+
const tools = r.toolCallCount != null ? ` ·${r.toolCallCount}t` : "";
|
|
22
|
+
const files = r.filesTouched?.length ? ` ✎${r.filesTouched.length}` : "";
|
|
19
23
|
const summary = r.resultSummary ? ` "${r.resultSummary}"` : "";
|
|
20
24
|
const prov = r.resumedFrom ? ` ← resumed:${r.resumedFrom}` : r.forkedFrom ? ` ← forked:${r.forkedFrom}` : "";
|
|
21
|
-
return `${STATUS_GLYPH[r.status]} ${r.runId} ${r.agent} ${r.status} ${dur}${tok}${ctx}${cost}${summary}${prov}`;
|
|
25
|
+
return `${STATUS_GLYPH[r.status]} ${r.runId} ${r.agent} ${r.status} ${dur}${tok}${ctx}${cost}${tools}${files}${err}${summary}${prov}`;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function runTimelineRow(e: MessageEvent | ToolEvent): string {
|
package/src/panel/widget-rows.ts
CHANGED
|
@@ -119,7 +119,13 @@ function widgetLine(r: WidgetRun, now: number): string {
|
|
|
119
119
|
const label = r.task ? `"${r.task.slice(0, 40)}"` : r.runId;
|
|
120
120
|
// SPEC-6-5: cross-cwd glyph — when the run's cwd differs from the session cwd, mark it so the
|
|
121
121
|
// operator sees "this run is scoped to a different project" at a glance. Same-cwd → no glyph.
|
|
122
|
-
|
|
122
|
+
// #62 NIT: isolated runs pin cwd to <child-cwd>/.pi/fleet/worktrees/<runId> — the basename
|
|
123
|
+
// was the cryptic run-id. Strip the worktrees suffix so the glyph names the TARGET repo dir.
|
|
124
|
+
const displayCwd = (cwd: string): string => {
|
|
125
|
+
const wt = cwd.indexOf("/.pi/fleet/worktrees/");
|
|
126
|
+
return wt > 0 ? basename(cwd.slice(0, wt)) : basename(cwd);
|
|
127
|
+
};
|
|
128
|
+
const crossCwd = (r.cwd && r.sessionCwd && r.cwd !== r.sessionCwd) ? ` ↗${displayCwd(r.cwd)}` : "";
|
|
123
129
|
const agentSeg = r.agent && r.agent !== "general-purpose" ? ` · ${r.agent}` : "";
|
|
124
130
|
// #23: liveness — only after LIVENESS_THRESHOLD_MS, to keep short runs concise (per acceptance).
|
|
125
131
|
// turn N/max + last-event class (no prompt content, no args/results — only the tool name)
|
package/src/rpc/rpc-server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { RunRegistry, RunRecord } from "../engine/run-registry.ts";
|
|
|
7
7
|
import type { RunLog } from "../runtime/run-log.ts";
|
|
8
8
|
import type { RunJournal } from "../runtime/run-journal.ts";
|
|
9
9
|
import { resolveDispatchCwd } from "../tools/subagent.ts";
|
|
10
|
+
import { isSessionRejection } from "../engine/session-rejection.ts";
|
|
10
11
|
|
|
11
12
|
export type RpcErrorCode =
|
|
12
13
|
| "E-CONTROL-DISABLED" | "E-RUN-NOT-FOUND" | "E-RUN-FINISHED" | "E-BAD-VERB"
|
|
@@ -40,6 +41,9 @@ export interface RpcServerDeps {
|
|
|
40
41
|
* Never throws — runtime failures land via the registry + RunLog journal (spawnSubagent's own
|
|
41
42
|
* fail path journals run:ended), so the caller's { runId } always resolves to a real run. */
|
|
42
43
|
spawn: (params: Record<string, unknown>, runId: string) => void;
|
|
44
|
+
/** #83: schedule registration (scheduler.register under the hood). Throws on an invalid
|
|
45
|
+
* expression (surfaced as E-BAD-PARAMS); absent = scheduling not configured in this session. */
|
|
46
|
+
schedule?: (spec: Record<string, unknown>) => { scheduleId: string; nextFire: string | null };
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const LIST_CAP = 25;
|
|
@@ -84,14 +88,15 @@ export class RpcServer {
|
|
|
84
88
|
case "spawn": return gated ? this.spawnVerb(id, params) : this.controlDisabled(id);
|
|
85
89
|
case "steer": return gated ? this.steerVerb(id, params) : this.controlDisabled(id);
|
|
86
90
|
case "abort": return gated ? this.abortVerb(id, params) : this.controlDisabled(id);
|
|
91
|
+
case "schedule": return gated ? this.scheduleVerb(id, params) : this.controlDisabled(id);
|
|
87
92
|
case "observe": return this.observeVerb(id, params);
|
|
88
93
|
case "status": return this.statusVerb(id, params);
|
|
89
|
-
default: return this.err(id, "E-BAD-VERB", `unknown verb '${verb}' (known: spawn, steer, observe, abort, status)`);
|
|
94
|
+
default: return this.err(id, "E-BAD-VERB", `unknown verb '${verb}' (known: spawn, steer, observe, abort, status, schedule)`);
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
private controlDisabled(id: string): RpcReply {
|
|
94
|
-
return this.err(id, "E-CONTROL-DISABLED", "fleet rpc control is disabled (ARMORY_FLEET_RPC_CONTROL is set to off; remove it or set it to 1 to enable spawn/steer/abort)");
|
|
99
|
+
return this.err(id, "E-CONTROL-DISABLED", "fleet rpc control is disabled (ARMORY_FLEET_RPC_CONTROL is set to off; remove it or set it to 1 to enable spawn/steer/abort/schedule)");
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
private err(id: string, code: RpcErrorCode, message: string): RpcReply {
|
|
@@ -107,9 +112,9 @@ export class RpcServer {
|
|
|
107
112
|
if (!p) return this.err(id, "E-BAD-PARAMS", "spawn requires params: { agent, task, ... }");
|
|
108
113
|
if (typeof p.agent !== "string" || !p.agent) return this.err(id, "E-BAD-PARAMS", "params.agent must be a non-empty string");
|
|
109
114
|
if (typeof p.task !== "string" || !p.task) return this.err(id, "E-BAD-PARAMS", "params.task must be a non-empty string");
|
|
110
|
-
if (p.lifecycle !== undefined) return this.err(id, "E-BAD-PARAMS", "params.lifecycle
|
|
111
|
-
if (p.schedule !== undefined) return this.err(id, "E-BAD-PARAMS", "params.schedule is not
|
|
112
|
-
if (p.modelFallback !== undefined) return this.err(id, "E-BAD-PARAMS", "params.modelFallback
|
|
115
|
+
if (p.lifecycle !== undefined && (typeof p.lifecycle !== "string" || !p.lifecycle)) return this.err(id, "E-BAD-PARAMS", "params.lifecycle must be a non-empty string when set (#83)");
|
|
116
|
+
if (p.schedule !== undefined) return this.err(id, "E-BAD-PARAMS", "params.schedule is not a spawn param — schedules run lifecycles, not single delegates; use the 'schedule' verb (#83)");
|
|
117
|
+
if (p.modelFallback !== undefined && (typeof p.modelFallback !== "string" || !p.modelFallback)) return this.err(id, "E-BAD-PARAMS", "params.modelFallback must be a non-empty string when set (#83)");
|
|
113
118
|
if (p.cwd !== undefined && (typeof p.cwd !== "string" || p.cwd === "")) return this.err(id, "E-BAD-PARAMS", "params.cwd must be a non-empty string when set");
|
|
114
119
|
if (p.cwd !== undefined) {
|
|
115
120
|
const { error } = resolveDispatchCwd(p.cwd, this.deps.parentCwd);
|
|
@@ -135,6 +140,42 @@ export class RpcServer {
|
|
|
135
140
|
return { id, ok: true, data: { runId } };
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/** #83 D4: register a recurring lifecycle run. Reply shape { scheduleId, nextFire } — schedules
|
|
144
|
+
* are NOT runs, so no runId (spawn's uniform { runId } contract stays unbranched). */
|
|
145
|
+
private scheduleVerb(id: string, params: unknown): RpcReply {
|
|
146
|
+
const p = this.obj(params);
|
|
147
|
+
if (!p) return this.err(id, "E-BAD-PARAMS", "schedule requires params: { task, expression, ... }");
|
|
148
|
+
if (typeof p.task !== "string" || !p.task) return this.err(id, "E-BAD-PARAMS", "params.task must be a non-empty string");
|
|
149
|
+
if (typeof p.expression !== "string" || !p.expression) return this.err(id, "E-BAD-PARAMS", "params.expression must be a non-empty string (cron or interval, e.g. '*/5 * * * *' or '30m')");
|
|
150
|
+
if (p.lifecycle !== undefined && (typeof p.lifecycle !== "string" || !p.lifecycle)) return this.err(id, "E-BAD-PARAMS", "params.lifecycle must be a non-empty string when set");
|
|
151
|
+
if (p.auto !== undefined && typeof p.auto !== "boolean") return this.err(id, "E-BAD-PARAMS", "params.auto must be a boolean");
|
|
152
|
+
if (p.isolation !== undefined && p.isolation !== "worktree" && p.isolation !== "none" && p.isolation !== "auto") {
|
|
153
|
+
return this.err(id, "E-BAD-PARAMS", "params.isolation must be 'worktree' | 'none' | 'auto'");
|
|
154
|
+
}
|
|
155
|
+
if (p.cwd !== undefined && (typeof p.cwd !== "string" || p.cwd === "")) return this.err(id, "E-BAD-PARAMS", "params.cwd must be a non-empty string when set");
|
|
156
|
+
let cwd: string | undefined;
|
|
157
|
+
if (p.cwd !== undefined) {
|
|
158
|
+
const resolved = resolveDispatchCwd(p.cwd, this.deps.parentCwd);
|
|
159
|
+
if (resolved.error) return this.err(id, "E-BAD-PARAMS", resolved.error);
|
|
160
|
+
cwd = resolved.cwd;
|
|
161
|
+
}
|
|
162
|
+
if (!this.deps.schedule) {
|
|
163
|
+
return this.err(id, "E-BAD-PARAMS", "scheduling not configured in this session (scheduler missing)");
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const out = this.deps.schedule({
|
|
167
|
+
task: p.task, expression: p.expression,
|
|
168
|
+
...(p.lifecycle !== undefined ? { lifecycle: p.lifecycle } : {}),
|
|
169
|
+
...(p.auto !== undefined ? { auto: p.auto } : {}),
|
|
170
|
+
...(p.isolation !== undefined ? { isolation: p.isolation } : {}),
|
|
171
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
172
|
+
});
|
|
173
|
+
return { id, ok: true, data: { scheduleId: out.scheduleId, nextFire: out.nextFire } };
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return this.err(id, "E-BAD-PARAMS", (e as Error).message || "schedule registration failed");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
138
179
|
private statusVerb(id: string, params: unknown): RpcReply {
|
|
139
180
|
const p = this.obj(params) ?? {};
|
|
140
181
|
if (p.runId !== undefined) {
|
|
@@ -143,8 +184,12 @@ export class RpcServer {
|
|
|
143
184
|
if (!rec) return this.err(id, "E-RUN-NOT-FOUND", `no live run '${p.runId}' in the registry (finished runs older than the session are not listed)`);
|
|
144
185
|
return { id, ok: true, data: { runs: [summarize(rec)] } };
|
|
145
186
|
}
|
|
146
|
-
const runs = this.deps.runRegistry.list()
|
|
147
|
-
|
|
187
|
+
const runs = this.deps.runRegistry.list();
|
|
188
|
+
const capped = runs.slice(0, LIST_CAP).map(summarize);
|
|
189
|
+
// #84: surface the omitted count so RPC consumers know the list is partial. Absent
|
|
190
|
+
// when everything fit (additive field — consumers check presence, not falseness).
|
|
191
|
+
const truncated = runs.length - capped.length;
|
|
192
|
+
return { id, ok: true, data: truncated > 0 ? { runs: capped, truncated } : { runs: capped } };
|
|
148
193
|
}
|
|
149
194
|
|
|
150
195
|
private observeVerb(id: string, params: unknown): RpcReply {
|
|
@@ -211,6 +256,9 @@ export class RpcServer {
|
|
|
211
256
|
try {
|
|
212
257
|
await session.steer(p.message);
|
|
213
258
|
} catch (e) {
|
|
259
|
+
// #84: typed rejections match by reason; string matching survives as a back-compat
|
|
260
|
+
// fallback for third-party ChildSession implementations that bubble bare Errors.
|
|
261
|
+
if (isSessionRejection(e) && e.reason === "steer-unsupported") return this.err(id, "E-STEER-UNSUPPORTED", e.message);
|
|
214
262
|
const msg = (e as Error).message ?? "steer failed";
|
|
215
263
|
if (msg.includes("not supported")) return this.err(id, "E-STEER-UNSUPPORTED", msg);
|
|
216
264
|
return this.err(id, "E-INTERNAL", `steer failed: ${msg}`);
|
|
@@ -229,6 +277,8 @@ export class RpcServer {
|
|
|
229
277
|
try {
|
|
230
278
|
await session.abort();
|
|
231
279
|
} catch (e) {
|
|
280
|
+
// #84: typed first (reason-based), string fallback for bare-Error handles.
|
|
281
|
+
if (isSessionRejection(e) && (e.reason === "already-aborted" || e.reason === "already-processing")) return this.err(id, "E-RUN-FINISHED", e.message);
|
|
232
282
|
const msg = (e as Error).message ?? "abort failed";
|
|
233
283
|
if (msg.includes("already")) return this.err(id, "E-RUN-FINISHED", msg);
|
|
234
284
|
return this.err(id, "E-INTERNAL", `abort failed: ${msg}`);
|
|
@@ -31,6 +31,9 @@ export interface RunLifecycleOpts {
|
|
|
31
31
|
/** #62: the dispatch target cwd for in-place runs (isolated runs pass the worktree path).
|
|
32
32
|
* Flows into runLifecycle's SPEC-6-5 cwd resolution (lifecycle.cwd ?? entryCwd). */
|
|
33
33
|
entryCwd?: string;
|
|
34
|
+
/** #83: per-run fallback model for the phase-spawn retry wrapper (wins over the host's
|
|
35
|
+
* global default). Undefined = use the host default (back-compat). */
|
|
36
|
+
modelFallback?: string;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export type RunLifecycleFn = (task: string, lifecycleName: string, opts: RunLifecycleOpts) => Promise<FakeLifecycleResult>;
|
|
@@ -66,6 +69,9 @@ export interface RunBackgroundOpts {
|
|
|
66
69
|
runId?: string;
|
|
67
70
|
/** v0.11.1: edit isolation for background runs. Default "auto" (worktree when cwd is a git repo, in-place otherwise). */
|
|
68
71
|
isolation?: Isolation;
|
|
72
|
+
/** #83: per-run fallback model — forwarded to the runLifecycle adapter so its phase-spawn
|
|
73
|
+
* retry wrapper prefers this over the host's global default. Undefined = host default. */
|
|
74
|
+
modelFallback?: string;
|
|
69
75
|
/** #62: the dispatch target cwd (undefined = session cwd, back-compat). Scopes the run:
|
|
70
76
|
* isolation routing + worktree creation resolve against THIS cwd (via deps.worktreeFor),
|
|
71
77
|
* and in-place runs pass it as the lifecycle entryCwd. */
|
|
@@ -120,7 +126,7 @@ function runBackgroundInPlace(runId: string, task: string, opts: RunBackgroundOp
|
|
|
120
126
|
deps.journal.append(runId, ev0);
|
|
121
127
|
emitProgress(deps, runId, { status: "running", phase: "", phaseIndex: 0, phaseTotal: 0, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
122
128
|
|
|
123
|
-
const res = await deps.runLifecycle(task, opts.lifecycle, { runId, worktreePath: isolated?.worktreePath, branch: isolated?.branch, mode: opts.mode, entryCwd: isolated ? isolated.worktreePath : opts.cwd, fleetMode: opts.origin ?? "background" });
|
|
129
|
+
const res = await deps.runLifecycle(task, opts.lifecycle, { runId, worktreePath: isolated?.worktreePath, branch: isolated?.branch, mode: opts.mode, entryCwd: isolated ? isolated.worktreePath : opts.cwd, fleetMode: opts.origin ?? "background", ...(opts.modelFallback ? { modelFallback: opts.modelFallback } : {}) });
|
|
124
130
|
|
|
125
131
|
if (res.status === "completed") {
|
|
126
132
|
if (isolated) {
|
package/src/runtime/run-log.ts
CHANGED
|
@@ -61,6 +61,12 @@ export interface RunMeta {
|
|
|
61
61
|
cwd?: string;
|
|
62
62
|
/** SPEC-6-5: the session cwd the dispatch originated from (= parentCwd). */
|
|
63
63
|
sessionCwd?: string;
|
|
64
|
+
/** #59 NIT: the failure reason on failed runs (from run:ended). */
|
|
65
|
+
error?: string;
|
|
66
|
+
/** #61 NIT: executed-tool count (the zero-work signal). */
|
|
67
|
+
toolCallCount?: number;
|
|
68
|
+
/** #60 NIT: file paths the run mutated. */
|
|
69
|
+
filesTouched?: string[];
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const ARGS_LIMIT = 200;
|
|
@@ -145,6 +151,8 @@ export class RunLog {
|
|
|
145
151
|
meta.resultSummary = ended.resultSummary; meta.tokenTotal = ended.tokenTotal;
|
|
146
152
|
meta.resumedFrom = ended.resumedFrom; meta.forkedFrom = ended.forkedFrom;
|
|
147
153
|
meta.costTotal = ended.costTotal; meta.contextTokens = ended.contextTokens;
|
|
154
|
+
// #59/#60/#61 NIT: surface the newer journal fields to the Runs tab too.
|
|
155
|
+
meta.error = ended.error; meta.toolCallCount = ended.toolCallCount; meta.filesTouched = ended.filesTouched;
|
|
148
156
|
}
|
|
149
157
|
out.push(meta);
|
|
150
158
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/settings/fleet-settings.ts
|
|
2
|
+
// #78: fleet-dir settings.json — the settings home for fleet-wide defaults that
|
|
3
|
+
// aren't env-shaped (sibling of tiers.json in the same global+project scheme).
|
|
4
|
+
//
|
|
5
|
+
// Locations:
|
|
6
|
+
// global: ~/.pi/agent/fleet/settings.json
|
|
7
|
+
// project: <cwd>/.pi/fleet/settings.json (project wins per-field)
|
|
8
|
+
//
|
|
9
|
+
// Design rules:
|
|
10
|
+
// - Absent files are normal (empty settings, no warning).
|
|
11
|
+
// - Present-but-invalid content produces ACTIONABLE warnings (file + field + bad
|
|
12
|
+
// value) and drops the field — never a silent swallow (the TierStore ENOENT
|
|
13
|
+
// lesson: silent loaders make wrong docs doubly dangerous).
|
|
14
|
+
// - The schema is intentionally small and additive; unknown keys warn so typos
|
|
15
|
+
// surface instead of no-oping.
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import type { ThinkingLevel } from "../registry/frontmatter.ts";
|
|
18
|
+
|
|
19
|
+
const THINKING_LEVELS: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
20
|
+
|
|
21
|
+
export function isThinkingLevel(v: unknown): v is ThinkingLevel {
|
|
22
|
+
return typeof v === "string" && (THINKING_LEVELS as readonly string[]).includes(v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fleet-wide defaults. Intentionally additive — new fields land here. */
|
|
26
|
+
export interface FleetSettings {
|
|
27
|
+
/** #78: applied to every subagent whose frontmatter does NOT pin `thinkingLevel`.
|
|
28
|
+
* Precedence: agent.thinkingLevel > this > the backend/session default. */
|
|
29
|
+
defaultSubagentThinking?: ThinkingLevel;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FleetSettingsResult {
|
|
33
|
+
settings: FleetSettings;
|
|
34
|
+
/** Actionable, source-labeled warnings (never empty-string; always name the file). */
|
|
35
|
+
warnings: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Parse one settings file's content. `label` names the file in warnings. */
|
|
39
|
+
export function parseFleetSettings(json: string, label = "settings.json"): FleetSettingsResult {
|
|
40
|
+
const warnings: string[] = [];
|
|
41
|
+
let raw: unknown;
|
|
42
|
+
try {
|
|
43
|
+
raw = JSON.parse(json);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { settings: {}, warnings: [`${label}: invalid JSON (${(e as Error).message}) — fleet settings from this file ignored`] };
|
|
46
|
+
}
|
|
47
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
48
|
+
return { settings: {}, warnings: [`${label}: expected a JSON object, got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw} — fleet settings from this file ignored`] };
|
|
49
|
+
}
|
|
50
|
+
const obj = raw as Record<string, unknown>;
|
|
51
|
+
const settings: FleetSettings = {};
|
|
52
|
+
|
|
53
|
+
const thinking = obj["defaultSubagentThinking"];
|
|
54
|
+
if (thinking !== undefined) {
|
|
55
|
+
if (isThinkingLevel(thinking)) {
|
|
56
|
+
settings.defaultSubagentThinking = thinking;
|
|
57
|
+
} else {
|
|
58
|
+
warnings.push(`${label}: defaultSubagentThinking must be one of ${THINKING_LEVELS.join("|")}, got ${JSON.stringify(thinking)} — ignored`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const known = new Set(["defaultSubagentThinking"]);
|
|
63
|
+
const unknownKeys = Object.keys(obj).filter((k) => !known.has(k));
|
|
64
|
+
if (unknownKeys.length > 0) {
|
|
65
|
+
warnings.push(`${label}: unknown setting${unknownKeys.length > 1 ? "s" : ""} ${unknownKeys.map((k) => `"${k}"`).join(", ")} — ignored (valid: ${[...known].join(", ")})`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { settings, warnings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FleetSettingsStoreOpts {
|
|
72
|
+
projectPath: string;
|
|
73
|
+
globalPath: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Global+project fleet settings (project wins per-field), mirroring TierStore's scheme. */
|
|
77
|
+
export class FleetSettingsStore {
|
|
78
|
+
constructor(private readonly opts: FleetSettingsStoreOpts) {}
|
|
79
|
+
|
|
80
|
+
private readOne(path: string, label: string): FleetSettingsResult {
|
|
81
|
+
let content: string;
|
|
82
|
+
try {
|
|
83
|
+
content = readFileSync(path, "utf8");
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// ENOENT = the normal absent-file state, never a warning. Any OTHER read failure
|
|
86
|
+
// (EACCES, EISDIR, …) on a path that was expected to be readable must surface —
|
|
87
|
+
// silent swallows make misconfiguration invisible (the TierStore lesson).
|
|
88
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
89
|
+
if (code === "ENOENT") return { settings: {}, warnings: [] };
|
|
90
|
+
return { settings: {}, warnings: [`${label}: unreadable (${code ?? (e as Error).message}) — fleet settings from this file ignored`] };
|
|
91
|
+
}
|
|
92
|
+
return parseFleetSettings(content, label);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
load(): FleetSettingsResult {
|
|
96
|
+
const global = this.readOne(this.opts.globalPath, this.opts.globalPath);
|
|
97
|
+
const project = this.readOne(this.opts.projectPath, this.opts.projectPath);
|
|
98
|
+
return {
|
|
99
|
+
settings: { ...global.settings, ...project.settings },
|
|
100
|
+
warnings: [...global.warnings, ...project.warnings],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/tools/subagent.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { statSync } from "node:fs";
|
|
4
4
|
import { Type, type Static } from "typebox";
|
|
5
|
-
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
5
|
+
import type { AgentDef, ThinkingLevel } from "../registry/frontmatter.ts";
|
|
6
6
|
import type { TodoSyncPort } from "../todo-sync/port.ts";
|
|
7
7
|
import type { RunRegistry } from "../engine/run-registry.ts";
|
|
8
8
|
import type { ForegroundLock } from "../engine/concurrency-lock.ts";
|
|
@@ -96,6 +96,10 @@ export interface SubagentToolDeps {
|
|
|
96
96
|
* Per-dispatch `modelFallback` (when passed) takes precedence. Applies to the direct foreground
|
|
97
97
|
* path, the foreground lifecycle spawn, and the background/scheduled spawn. */
|
|
98
98
|
defaultModelFallback?: string;
|
|
99
|
+
/** #78: fleet-wide default thinking level for subagents (settings.json `defaultSubagentThinking`).
|
|
100
|
+
* Applied when the agent frontmatter does NOT pin `thinkingLevel`. Threads to every spawn
|
|
101
|
+
* path (direct, lifecycle phase, fallback retry) exactly like `defaultModelFallback`. */
|
|
102
|
+
defaultSubagentThinking?: ThinkingLevel;
|
|
99
103
|
/** SPEC-6-5: notify hook for cross-cwd dispatch surfacing. Wired from ctx.ui.notify in index.ts. */
|
|
100
104
|
onNotify?: (message: string, kind?: "info" | "warning" | "error") => void;
|
|
101
105
|
}
|
|
@@ -155,6 +159,7 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
155
159
|
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd, runLog: deps.runLog, signal,
|
|
156
160
|
maxTurns: params.maxTurns,
|
|
157
161
|
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
162
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
158
163
|
readOnly: params.readOnly,
|
|
159
164
|
cwd: o.cwd,
|
|
160
165
|
}), params.modelFallback ?? deps.defaultModelFallback, signal),
|
|
@@ -192,6 +197,7 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
192
197
|
signal,
|
|
193
198
|
maxTurns: params.maxTurns,
|
|
194
199
|
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
200
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
195
201
|
cwd: resolvedCwd,
|
|
196
202
|
});
|
|
197
203
|
// #39: auto-retry on a retryable provider rate-limit / auth failure (stopReason "error").
|
|
@@ -227,6 +233,7 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
227
233
|
signal,
|
|
228
234
|
maxTurns: params.maxTurns,
|
|
229
235
|
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
236
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
230
237
|
cwd: resolvedCwd,
|
|
231
238
|
});
|
|
232
239
|
retriedWithModel = fallback;
|
|
@@ -22,6 +22,8 @@ export interface WorkflowAdapterBase {
|
|
|
22
22
|
runLog?: unknown
|
|
23
23
|
tierRegistry?: unknown
|
|
24
24
|
modelRegistry?: unknown
|
|
25
|
+
/** #78: fleet-wide default thinking level, threaded into every workflow child spawn. */
|
|
26
|
+
defaultThinkingLevel?: SpawnOptions["defaultThinkingLevel"]
|
|
25
27
|
lifecycleDeps: unknown
|
|
26
28
|
spawnSubagentFn: (opts: SpawnOptions) => Promise<SpawnResult>
|
|
27
29
|
runLifecycleFn: (task: string, name: string, opts: LifecycleRunOpts) => Promise<LifecycleRunResult>
|
|
@@ -69,6 +71,7 @@ export function createWorkflowAdapters(
|
|
|
69
71
|
...(base.runLog ? { runLog: base.runLog as SpawnOptions["runLog"] } : {}),
|
|
70
72
|
...(base.tierRegistry ? { tierRegistry: base.tierRegistry as SpawnOptions["tierRegistry"] } : {}),
|
|
71
73
|
...(base.modelRegistry ? { modelRegistry: base.modelRegistry as SpawnOptions["modelRegistry"] } : {}),
|
|
74
|
+
...(base.defaultThinkingLevel ? { defaultThinkingLevel: base.defaultThinkingLevel } : {}),
|
|
72
75
|
})
|
|
73
76
|
|
|
74
77
|
const combineSignal = (timeoutMs?: number): AbortSignal => {
|