@getpipher/armory-fleet 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -4
- package/package.json +1 -1
- package/src/engine/retry-fallback.ts +14 -0
- package/src/engine/spawnSubagent.ts +6 -1
- package/src/index.ts +95 -4
- package/src/panel/fleet-panel.ts +8 -1
- package/src/rpc/rpc-server.ts +45 -5
- package/src/runtime/async-runner.ts +7 -1
- 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.0",
|
|
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
|
+
}
|
|
@@ -158,6 +158,10 @@ export interface SpawnOptions {
|
|
|
158
158
|
modelRegistry?: ModelRegistryLike;
|
|
159
159
|
/** SPEC-6-3: workflow adapter tier override — replaces agent.tier before model resolution. */
|
|
160
160
|
tierOverride?: string;
|
|
161
|
+
/** #78: fleet-wide default thinking level (`fleet.defaultSubagentThinking` settings field).
|
|
162
|
+
* Applied only when the agent frontmatter does NOT pin `thinkingLevel` — precedence:
|
|
163
|
+
* agent.thinkingLevel > opts.defaultThinkingLevel > backend/session default. */
|
|
164
|
+
defaultThinkingLevel?: ThinkingLevel;
|
|
161
165
|
/** #31: when true, the caller asserts this dispatch will NOT mutate the working directory
|
|
162
166
|
* (review/audit/research). Read-only dispatches bypass the foreground single-slot lock so
|
|
163
167
|
* multiple readOnly dispatches — and/or a readOnly dispatch alongside a write dispatch — can
|
|
@@ -335,7 +339,8 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
335
339
|
const result = await backend.factory.create({
|
|
336
340
|
cwd: childCwd,
|
|
337
341
|
model: cand,
|
|
338
|
-
|
|
342
|
+
// #78: agent frontmatter wins; the fleet-wide default fills the gap.
|
|
343
|
+
thinkingLevel: childAgent.thinkingLevel ?? opts.defaultThinkingLevel,
|
|
339
344
|
tools,
|
|
340
345
|
rolePrompt: childAgent.rolePrompt,
|
|
341
346
|
skills: childAgent.skills ?? [],
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ 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";
|
|
@@ -52,6 +52,7 @@ import { FleetWidgetController } from "./panel/fleet-widget.ts";
|
|
|
52
52
|
import { TierRegistry, mergeTiers } from "./tiers/tier-registry.ts";
|
|
53
53
|
import { BUILTIN_TIERS } from "./tiers/builtin.ts";
|
|
54
54
|
import { TierStore } from "./tiers/tier-store.ts";
|
|
55
|
+
import { FleetSettingsStore } from "./settings/fleet-settings.ts";
|
|
55
56
|
import { splitModel } from "./tiers/resolve.ts";
|
|
56
57
|
import { WorkflowJournal } from "./workflows/journal.ts";
|
|
57
58
|
import { discoverWorkflows, WorkflowRegistry, type WorkflowDef } from "./workflows/registry.ts";
|
|
@@ -304,7 +305,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
304
305
|
// in-place runs so the cross-cwd surfacing (↗ glyph + sessionCwd audit) still fires.
|
|
305
306
|
cwd: isolated ? opts.worktreePath : o.cwd,
|
|
306
307
|
runLog: deps.runLog, tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
307
|
-
|
|
308
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
309
|
+
}), opts.modelFallback ?? deps.defaultModelFallback),
|
|
308
310
|
};
|
|
309
311
|
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
312
|
return res as unknown as import("./runtime/async-runner.ts").FakeLifecycleResult;
|
|
@@ -429,33 +431,84 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
429
431
|
journal: fleetJournal,
|
|
430
432
|
parentCwd: ctx.cwd,
|
|
431
433
|
hasAsyncRunner: true,
|
|
434
|
+
// #83 D4: schedule registration — scheduler.register + nextFire lookup. Throws (invalid
|
|
435
|
+
// expression) propagate to the verb, which maps them to E-BAD-PARAMS.
|
|
436
|
+
schedule: (spec) => {
|
|
437
|
+
if (!deps.scheduler) throw new Error("scheduling not configured in this session (scheduler missing)");
|
|
438
|
+
const id = deps.scheduler.register({
|
|
439
|
+
task: String(spec.task),
|
|
440
|
+
expression: String(spec.expression),
|
|
441
|
+
...(typeof spec.lifecycle === "string" ? { lifecycle: spec.lifecycle } : {}),
|
|
442
|
+
...(typeof spec.auto === "boolean" ? { auto: spec.auto } : {}),
|
|
443
|
+
...(spec.isolation === "worktree" || spec.isolation === "none" || spec.isolation === "auto" ? { isolation: spec.isolation } : {}),
|
|
444
|
+
...(typeof spec.cwd === "string" ? { cwd: spec.cwd } : {}),
|
|
445
|
+
});
|
|
446
|
+
const entry = deps.scheduler.list().find((s) => s.id === id);
|
|
447
|
+
return { scheduleId: id, nextFire: entry?.nextFire?.toISOString() ?? null };
|
|
448
|
+
},
|
|
432
449
|
spawn: (params: Record<string, unknown>, runId: string) => {
|
|
433
450
|
// Detached fire-and-forget: NEVER throws — spawnSubagent journals its own fail path
|
|
434
451
|
// (run:ended + todo revert), so the caller's pre-minted runId always resolves to events.
|
|
435
452
|
void (async () => {
|
|
436
453
|
const requestedCwd = typeof params.cwd === "string" ? params.cwd : undefined;
|
|
437
454
|
const { cwd: resolvedCwd } = resolveDispatchCwd(requestedCwd, ctx.cwd);
|
|
455
|
+
// #83: lifecycle + per-request modelFallback params (validated by the verb; re-narrowed here).
|
|
456
|
+
const rpcLifecycle = typeof params.lifecycle === "string" && params.lifecycle ? params.lifecycle : "default";
|
|
457
|
+
const rpcFallback = typeof params.modelFallback === "string" && params.modelFallback ? params.modelFallback : undefined;
|
|
438
458
|
if (params.background === true) {
|
|
439
459
|
// RPC background: routed through the async runner (pool slot + isolation + origin),
|
|
440
460
|
// identical to the tool's runBackground path (spec §3.2). Ghost-runId prevention:
|
|
441
461
|
// a synchronous pre-flight failure journals run:ended failed under the caller's id.
|
|
442
462
|
const handle = runBackground(String(params.task), {
|
|
443
463
|
deps: deps.asyncRunner!,
|
|
444
|
-
lifecycle:
|
|
464
|
+
lifecycle: rpcLifecycle,
|
|
445
465
|
mode: "auto",
|
|
446
466
|
isolation: params.isolation as "worktree" | "none" | "auto" | undefined,
|
|
447
467
|
cwd: resolvedCwd ?? ctx.cwd,
|
|
448
468
|
origin: "background",
|
|
449
469
|
runId,
|
|
470
|
+
...(rpcFallback ? { modelFallback: rpcFallback } : {}),
|
|
450
471
|
});
|
|
451
472
|
if (handle.status === "failed") {
|
|
452
473
|
deps.runLog?.append(runId, { type: "run:ended", runId, status: "failed", endedAt: Date.now(), tokenTotal: 0, error: handle.error });
|
|
453
474
|
}
|
|
454
475
|
return;
|
|
455
476
|
}
|
|
477
|
+
if (params.lifecycle !== undefined) {
|
|
478
|
+
// #83 D2: lifecycle WITHOUT background = detached foreground-semantics lifecycle run
|
|
479
|
+
// (tool parity: mode "auto", failed-phase checkpoint aborts, session lock on the phase
|
|
480
|
+
// spawn, pre-minted runId via the genRunId override — same pattern as asyncRunLifecycle).
|
|
481
|
+
const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts");
|
|
482
|
+
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
483
|
+
const { withModelFallbackRetry } = await import("./engine/retry-fallback.ts");
|
|
484
|
+
const lifecycleFullDeps = {
|
|
485
|
+
...deps.lifecycleDeps,
|
|
486
|
+
genRunId: () => runId,
|
|
487
|
+
spawn: withModelFallbackRetry(async (o) => spawnSubagent({
|
|
488
|
+
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
489
|
+
skillsOverride: mergeLifecycleSkills(o.skills, Array.isArray(params.skills) ? params.skills as string[] : undefined),
|
|
490
|
+
backendOverride: o.backend,
|
|
491
|
+
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
492
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: ctx.cwd,
|
|
493
|
+
runLog: deps.runLog, signal: undefined,
|
|
494
|
+
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
495
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
496
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
497
|
+
readOnly: params.readOnly === true,
|
|
498
|
+
cwd: o.cwd ?? resolvedCwd,
|
|
499
|
+
}), rpcFallback ?? deps.defaultModelFallback),
|
|
500
|
+
};
|
|
501
|
+
const res = await runLifecycle(String(params.task), rpcLifecycle, {
|
|
502
|
+
deps: lifecycleFullDeps, mode: "auto", entryCwd: resolvedCwd,
|
|
503
|
+
onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" },
|
|
504
|
+
});
|
|
505
|
+
deps.lifecycleRuns.set(res.runId, res); // panel Lifecycle-view visibility (workflow-lambda precedent)
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
456
508
|
// Foreground-semantics detached spawn (no mode — "foreground" is the truth).
|
|
457
509
|
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
458
|
-
await
|
|
510
|
+
const { retryForegroundOnce } = await import("./engine/retry-fallback.ts");
|
|
511
|
+
const primary = await spawnSubagent({
|
|
459
512
|
agent: params.agent as string,
|
|
460
513
|
task: params.task as string,
|
|
461
514
|
todoId: typeof params.todoId === "string" ? params.todoId : undefined,
|
|
@@ -476,7 +529,33 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
476
529
|
modelRegistry: deps.modelRegistry,
|
|
477
530
|
cwd: resolvedCwd ?? ctx.cwd,
|
|
478
531
|
runId,
|
|
532
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
479
533
|
});
|
|
534
|
+
// #83 D3: per-request fallback retry — the retry mints a FRESH runId (the primary's
|
|
535
|
+
// pre-minted id stays retired; a reuse would double-emit run:started) and relinks the
|
|
536
|
+
// primary's todo, exactly like the tool's direct path.
|
|
537
|
+
await retryForegroundOnce(primary, rpcFallback, (o) => spawnSubagent({
|
|
538
|
+
agent: params.agent as string,
|
|
539
|
+
task: params.task as string,
|
|
540
|
+
todoId: o.todoId,
|
|
541
|
+
track: params.track === true,
|
|
542
|
+
model: o.model,
|
|
543
|
+
readOnly: params.readOnly === true,
|
|
544
|
+
skillsOverride: Array.isArray(params.skills) ? params.skills as string[] : undefined,
|
|
545
|
+
registry: deps.registry,
|
|
546
|
+
todoSync: deps.todoSync,
|
|
547
|
+
runRegistry: deps.runRegistry,
|
|
548
|
+
lock: deps.lock,
|
|
549
|
+
backendRegistry: deps.backendRegistry,
|
|
550
|
+
parentModel: deps.parentModel,
|
|
551
|
+
parentCwd: ctx.cwd,
|
|
552
|
+
runLog: deps.runLog,
|
|
553
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
554
|
+
tierRegistry: deps.tierRegistry,
|
|
555
|
+
modelRegistry: deps.modelRegistry,
|
|
556
|
+
cwd: resolvedCwd ?? ctx.cwd,
|
|
557
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
558
|
+
}));
|
|
480
559
|
})().catch(() => {});
|
|
481
560
|
},
|
|
482
561
|
});
|
|
@@ -521,6 +600,16 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
521
600
|
deps.reloadTiers = reloadTiers;
|
|
522
601
|
reloadTiers(); // build the real merged registry (replaces the builtin-only placeholder)
|
|
523
602
|
|
|
603
|
+
// #78: fleet-dir settings.json (global + project, project wins) — currently carries
|
|
604
|
+
// `defaultSubagentThinking`. Warnings are surfaced, never swallowed silently.
|
|
605
|
+
const fleetSettingsStore = new FleetSettingsStore({
|
|
606
|
+
projectPath: join(dir, "settings.json"),
|
|
607
|
+
globalPath: join(process.env.HOME ?? "", ".pi", "agent", "fleet", "settings.json"),
|
|
608
|
+
});
|
|
609
|
+
const fleetSettings = fleetSettingsStore.load();
|
|
610
|
+
for (const w of fleetSettings.warnings) ctx.ui.notify(w, "warning");
|
|
611
|
+
deps.defaultSubagentThinking = fleetSettings.settings.defaultSubagentThinking;
|
|
612
|
+
|
|
524
613
|
// SPEC-6-3: workflow journal + registry + runner + fleet tool wiring.
|
|
525
614
|
const workflowJournal = new WorkflowJournal(join(dir, "workflows"));
|
|
526
615
|
wfRegistry = new WorkflowRegistry(discoverWorkflows({
|
|
@@ -543,6 +632,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
543
632
|
runLog: deps.runLog,
|
|
544
633
|
tierRegistry: deps.tierRegistry,
|
|
545
634
|
modelRegistry: deps.modelRegistry,
|
|
635
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
546
636
|
lifecycleDeps: deps.lifecycleDeps,
|
|
547
637
|
spawnSubagentFn: async (opts) => { const { spawnSubagent } = await import("./engine/spawnSubagent.ts"); return spawnSubagent(opts); },
|
|
548
638
|
runLifecycleFn: async (task, name, lcOpts) => { const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts"); return runLifecycle(task, name, lcOpts); },
|
|
@@ -677,6 +767,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
677
767
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
678
768
|
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd,
|
|
679
769
|
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry, // SPEC-6-1
|
|
770
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
680
771
|
}), deps.defaultModelFallback),
|
|
681
772
|
};
|
|
682
773
|
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/rpc/rpc-server.ts
CHANGED
|
@@ -40,6 +40,9 @@ export interface RpcServerDeps {
|
|
|
40
40
|
* Never throws — runtime failures land via the registry + RunLog journal (spawnSubagent's own
|
|
41
41
|
* fail path journals run:ended), so the caller's { runId } always resolves to a real run. */
|
|
42
42
|
spawn: (params: Record<string, unknown>, runId: string) => void;
|
|
43
|
+
/** #83: schedule registration (scheduler.register under the hood). Throws on an invalid
|
|
44
|
+
* expression (surfaced as E-BAD-PARAMS); absent = scheduling not configured in this session. */
|
|
45
|
+
schedule?: (spec: Record<string, unknown>) => { scheduleId: string; nextFire: string | null };
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const LIST_CAP = 25;
|
|
@@ -84,14 +87,15 @@ export class RpcServer {
|
|
|
84
87
|
case "spawn": return gated ? this.spawnVerb(id, params) : this.controlDisabled(id);
|
|
85
88
|
case "steer": return gated ? this.steerVerb(id, params) : this.controlDisabled(id);
|
|
86
89
|
case "abort": return gated ? this.abortVerb(id, params) : this.controlDisabled(id);
|
|
90
|
+
case "schedule": return gated ? this.scheduleVerb(id, params) : this.controlDisabled(id);
|
|
87
91
|
case "observe": return this.observeVerb(id, params);
|
|
88
92
|
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)`);
|
|
93
|
+
default: return this.err(id, "E-BAD-VERB", `unknown verb '${verb}' (known: spawn, steer, observe, abort, status, schedule)`);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
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)");
|
|
98
|
+
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
99
|
}
|
|
96
100
|
|
|
97
101
|
private err(id: string, code: RpcErrorCode, message: string): RpcReply {
|
|
@@ -107,9 +111,9 @@ export class RpcServer {
|
|
|
107
111
|
if (!p) return this.err(id, "E-BAD-PARAMS", "spawn requires params: { agent, task, ... }");
|
|
108
112
|
if (typeof p.agent !== "string" || !p.agent) return this.err(id, "E-BAD-PARAMS", "params.agent must be a non-empty string");
|
|
109
113
|
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
|
|
114
|
+
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)");
|
|
115
|
+
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)");
|
|
116
|
+
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
117
|
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
118
|
if (p.cwd !== undefined) {
|
|
115
119
|
const { error } = resolveDispatchCwd(p.cwd, this.deps.parentCwd);
|
|
@@ -135,6 +139,42 @@ export class RpcServer {
|
|
|
135
139
|
return { id, ok: true, data: { runId } };
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/** #83 D4: register a recurring lifecycle run. Reply shape { scheduleId, nextFire } — schedules
|
|
143
|
+
* are NOT runs, so no runId (spawn's uniform { runId } contract stays unbranched). */
|
|
144
|
+
private scheduleVerb(id: string, params: unknown): RpcReply {
|
|
145
|
+
const p = this.obj(params);
|
|
146
|
+
if (!p) return this.err(id, "E-BAD-PARAMS", "schedule requires params: { task, expression, ... }");
|
|
147
|
+
if (typeof p.task !== "string" || !p.task) return this.err(id, "E-BAD-PARAMS", "params.task must be a non-empty string");
|
|
148
|
+
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')");
|
|
149
|
+
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");
|
|
150
|
+
if (p.auto !== undefined && typeof p.auto !== "boolean") return this.err(id, "E-BAD-PARAMS", "params.auto must be a boolean");
|
|
151
|
+
if (p.isolation !== undefined && p.isolation !== "worktree" && p.isolation !== "none" && p.isolation !== "auto") {
|
|
152
|
+
return this.err(id, "E-BAD-PARAMS", "params.isolation must be 'worktree' | 'none' | 'auto'");
|
|
153
|
+
}
|
|
154
|
+
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");
|
|
155
|
+
let cwd: string | undefined;
|
|
156
|
+
if (p.cwd !== undefined) {
|
|
157
|
+
const resolved = resolveDispatchCwd(p.cwd, this.deps.parentCwd);
|
|
158
|
+
if (resolved.error) return this.err(id, "E-BAD-PARAMS", resolved.error);
|
|
159
|
+
cwd = resolved.cwd;
|
|
160
|
+
}
|
|
161
|
+
if (!this.deps.schedule) {
|
|
162
|
+
return this.err(id, "E-BAD-PARAMS", "scheduling not configured in this session (scheduler missing)");
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const out = this.deps.schedule({
|
|
166
|
+
task: p.task, expression: p.expression,
|
|
167
|
+
...(p.lifecycle !== undefined ? { lifecycle: p.lifecycle } : {}),
|
|
168
|
+
...(p.auto !== undefined ? { auto: p.auto } : {}),
|
|
169
|
+
...(p.isolation !== undefined ? { isolation: p.isolation } : {}),
|
|
170
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
171
|
+
});
|
|
172
|
+
return { id, ok: true, data: { scheduleId: out.scheduleId, nextFire: out.nextFire } };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return this.err(id, "E-BAD-PARAMS", (e as Error).message || "schedule registration failed");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
138
178
|
private statusVerb(id: string, params: unknown): RpcReply {
|
|
139
179
|
const p = this.obj(params) ?? {};
|
|
140
180
|
if (p.runId !== undefined) {
|
|
@@ -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) {
|
|
@@ -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 => {
|