@nklisch/pi-enhanced 0.1.8 → 0.1.9

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-arm64-msvc.node +0 -0
  3. package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
  4. package/node_modules/@nklisch/pi-plugins/README.md +4 -4
  5. package/node_modules/@nklisch/pi-plugins/dist/application/plugin-lifecycle-service.js +19 -2
  6. package/node_modules/@nklisch/pi-plugins/dist/application/plugin-lifecycle-service.js.map +1 -1
  7. package/node_modules/@nklisch/pi-plugins/dist/application/ports/lifecycle-transition-store.d.ts +12 -0
  8. package/node_modules/@nklisch/pi-plugins/dist/application/ports/lifecycle-transition-store.js.map +1 -1
  9. package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/create-node-recovery-adapters.js +2 -1
  10. package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/create-node-recovery-adapters.js.map +1 -1
  11. package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-transition-journal.d.ts +4 -0
  12. package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-transition-journal.js +28 -1
  13. package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-transition-journal.js.map +1 -1
  14. package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-package.js +1 -1
  15. package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-runtime.js +1 -1
  16. package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/pi-subagents-package.js +1 -1
  17. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +11 -0
  18. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +3 -1
  19. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +1 -1
  20. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +2 -1
  21. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +4 -4
  22. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/client-server-opportunities.md +4 -4
  23. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +1 -1
  24. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
  25. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +8 -8
  26. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +1 -0
  27. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/parent-snapshot.ts +3 -0
  28. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +16 -1
  29. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +12 -1
  30. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +18 -1
  31. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/composite-subagent-observer.ts +8 -0
  32. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +3 -0
  33. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +2 -2
  34. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/runtime.ts +1 -0
  35. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +11 -3
  36. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/session-config.ts +7 -3
  37. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/thinking-level.ts +42 -0
  38. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +3 -3
  39. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +7 -2
  40. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +7 -5
  41. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +4 -1
  42. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +1 -0
  43. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +1 -1
  44. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +7 -4
  45. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +15 -2
  46. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +3 -3
  47. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +6 -2
  48. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +117 -62
  49. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +8 -1
  50. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +7 -3
  51. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +2 -2
  52. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +6 -4
  53. package/node_modules/@nklisch/pi-plugins/package.json +3 -3
  54. package/package.json +2 -2
@@ -21,7 +21,7 @@ A subagent **is** a child `AgentSession`. pi-subagents already creates one via `
21
21
  Today that child session is an in-process object visible only through machinery pi-subagents had to invent because Pi has no session-sync client:
22
22
 
23
23
  - `record-observer` re-derives live activity from raw session events.
24
- - the 80 ms `SubagentManager.listAgents()` widget poll.
24
+ - the lifecycle-fed widget read model and 500 ms refresh cadence.
25
25
  - the [#277] Law-of-Demeter accessors (`Subagent.getConversation()`, `.messages`, `.subscribeToUpdates()`, `.getContextPercent()`) that re-expose session internals.
26
26
  - the bespoke `ConversationViewer`, and [ADR-0004]'s replacement, native session navigation.
27
27
  - [ADR-0004]'s dual-source-by-liveness split (tracked agent → in-memory record; evicted → file snapshot).
@@ -40,9 +40,9 @@ What changes versus today:
40
40
  The snapshot carries in-flight state, so an operator attaching to an already-running subagent immediately sees its current streaming message and pending tools — not just future events.
41
41
  Today the conversation viewer only catches future deltas plus whatever happens to be in the in-memory record.
42
42
  2. **The widget and viewer become thin renderers** of synchronized session state.
43
- The 80 ms poll, `record-observer`, and the [#277] accessors disappear — they were all substitutes for `subscribeSession`.
43
+ The lifecycle-fed widget read model, `record-observer`, and the [#277] accessors disappear — they were all substitutes for `subscribeSession`.
44
44
  3. **A unified, reconnect-safe live session list.**
45
- The global event scope (`session_created`, `session_status_changed` idle/busy) means the operator sees every subagent the instant it spawns, with a live status badge per agent — replacing both `listAgents()` polling and the hand-rolled `subagents:*` broadcast tier.
45
+ The global event scope (`session_created`, `session_status_changed` idle/busy) means the operator sees every subagent the instant it spawns, with a live status badge per agent — replacing the widget's lifecycle-fed read model and the hand-rolled `subagents:*` broadcast tier.
46
46
  4. **Multi-session client state** lets one operator client hold synchronized caches for N subagents at once.
47
47
  This is the clean foundation for the parallel-agent navigation gesture that [ADR-0004]'s spike (entry-criterion #3) struggled to design — tabbed or split views of multiple live subagents fall out of the model with no redesign.
48
48
 
@@ -96,7 +96,7 @@ This validates and sharpens the direction the architecture doc is already headin
96
96
  | Today (pi-subagents reinvents it) | Under the server architecture |
97
97
  | ------------------------------------------------------------------------ | ---------------------------------------------------------- |
98
98
  | `record-observer` re-deriving live activity | `subscribeSession` delta stream |
99
- | 80 ms `listAgents()` widget poll | `session_created` / `session_status_changed` global events |
99
+ | Lifecycle-fed widget read model + 500 ms refresh | `session_created` / `session_status_changed` global events |
100
100
  | [#277] accessors (`messages`, `subscribeToUpdates`, `getContextPercent`) | `SessionSnapshot` + deltas |
101
101
  | `ConversationViewer` / native session navigation | client renders the snapshot through Pi's own components |
102
102
  | dual-source-by-liveness split | server rehydrates; client sees one session shape |
@@ -21,7 +21,7 @@ The direct `gotgenes` lineage and this fork also share a focused-core architectu
21
21
  This fork retains several contracts that must survive selective upstream ports:
22
22
 
23
23
  - ordered lifecycle interception for prompt, result, and continuation decisions;
24
- - exact `provider/id` and elapsed or final runtime on every operator status surface;
24
+ - exact `provider/id`, exact effective thinking level, and elapsed or final runtime on every operator status surface;
25
25
  - a typed service and deterministic child-session lifecycle;
26
26
  - only `general-purpose` and `Explore` as built-in agents;
27
27
  - model runtime inheritance for runtime-registered providers and authentication;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nklisch/pi-subagents",
3
- "version": "18.1.0-nklisch.0",
3
+ "version": "18.1.0-nklisch.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -56,9 +56,9 @@ export default function (pi: ExtensionAPI) {
56
56
  const runtime = createSubagentRuntime();
57
57
 
58
58
  // ---- Notification system ----
59
- // Owns completion nudges and live-activity cleanup. The widget detects finished
60
- // agents itself (AgentWidget.update self-seeds), so NotificationManager has no
61
- // widget dependency — keeping the construction graph a cycle-free DAG.
59
+ // Owns completion nudges and live-activity cleanup. The widget owns its
60
+ // bounded lifecycle read model, so NotificationManager has no widget
61
+ // dependency — keeping the construction graph a cycle-free DAG.
62
62
  const notifications = new NotificationManager(
63
63
  (msg, opts) => pi.sendMessage(msg, opts),
64
64
  );
@@ -84,8 +84,7 @@ export default function (pi: ExtensionAPI) {
84
84
 
85
85
  // Fan-out observer: lets the widget subscribe as a second lifecycle consumer
86
86
  // while the manager keeps its single-observer contract. The widget is added
87
- // after construction (it needs the manager); the manager consults the observer
88
- // only at spawn time, so registering late is safe.
87
+ // before any spawn, so lifecycle callbacks populate its bounded read model.
89
88
  const observer = new CompositeSubagentObserver([eventsObserver]);
90
89
 
91
90
  const subagentSessionDeps: SubagentSessionDeps = {
@@ -134,10 +133,11 @@ export default function (pi: ExtensionAPI) {
134
133
  pi.on("session_before_switch", () => lifecycle.handleSessionBeforeSwitch());
135
134
  pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
136
135
 
137
- // Live widget: constructed after the manager (it polls listAgents()) and
138
- // registered as a lifecycle observer so it self-drives its update timer.
139
- const widget = new AgentWidget(manager, registry);
136
+ // Live widget: a lifecycle observer with a bounded reactive read model; it
137
+ // never rescans the manager's retained terminal-record history.
138
+ const widget = new AgentWidget(registry);
140
139
  observer.add(widget);
140
+ pi.on("session_shutdown", () => widget.dispose());
141
141
 
142
142
  // Grab UI context from first tool execution + clear lingering widget on new turn
143
143
  const toolStart = new ToolStartHandler(widget);
@@ -164,6 +164,7 @@ export async function createSubagentSession(
164
164
  cwd: snapshot.cwd,
165
165
  parentSystemPrompt: snapshot.systemPrompt,
166
166
  parentModel: snapshot.model,
167
+ parentThinkingLevel: snapshot.thinkingLevel,
167
168
  modelRegistry: snapshot.modelRegistry,
168
169
  },
169
170
  {
@@ -16,6 +16,8 @@ export interface ParentSnapshot {
16
16
  systemPrompt: string;
17
17
  /** Parent's current model instance (fallback when agent config has no model). */
18
18
  model: unknown;
19
+ /** Parent's current effective thinking level, used when the child inherits it. */
20
+ thinkingLevel?: SessionContext["thinkingLevel"];
19
21
  /** Model registry compatibility facade used for synchronous model lookup. */
20
22
  modelRegistry: {
21
23
  find(provider: string, modelId: string): unknown;
@@ -48,6 +50,7 @@ export function buildParentSnapshot(
48
50
  cwd: ctx.cwd,
49
51
  systemPrompt: ctx.getSystemPrompt(),
50
52
  model: ctx.model,
53
+ thinkingLevel: ctx.thinkingLevel,
51
54
  modelRegistry,
52
55
  modelRuntime,
53
56
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts empty string to undefined as well as null/undefined
@@ -31,7 +31,11 @@ import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType,
31
31
  export interface SubagentManagerObserver {
32
32
  onSubagentStarted(record: Subagent): void;
33
33
  onSubagentCompleted(record: Subagent): void;
34
+ /** Fires when a retained session begins a resumed turn. */
35
+ onSubagentResumedStarted?(record: Subagent): void;
34
36
  onSubagentResumed?(record: Subagent): void;
37
+ /** Fires when clearCompleted removes a terminal record from the parent session. */
38
+ onSubagentCleared?(record: Subagent): void;
35
39
  onSubagentCompacted(record: Subagent, info: CompactionInfo): void;
36
40
  /** Fires synchronously after a background agent record is created (before run). */
37
41
  onSubagentCreated(record: Subagent): void;
@@ -240,7 +244,13 @@ export class SubagentManager {
240
244
  ): Promise<Subagent | undefined> {
241
245
  const agent = this.agents.get(id);
242
246
  if (!agent?.isSessionReady()) return undefined;
243
- await agent.resume(prompt, signal);
247
+ const resumed = agent.resume(prompt, signal);
248
+ try {
249
+ this.observer?.onSubagentResumedStarted?.(agent);
250
+ } catch (err) {
251
+ debugLog("onSubagentResumedStarted observer", err);
252
+ }
253
+ await resumed;
244
254
  return agent;
245
255
  }
246
256
 
@@ -301,6 +311,11 @@ export class SubagentManager {
301
311
  clearCompleted(): void {
302
312
  for (const [id, record] of this.agents) {
303
313
  if (record.status === "running" || record.status === "queued") continue;
314
+ try {
315
+ this.observer?.onSubagentCleared?.(record);
316
+ } catch (err) {
317
+ debugLog("onSubagentCleared observer", err);
318
+ }
304
319
  this.removeRecord(id, record);
305
320
  }
306
321
 
@@ -15,6 +15,7 @@ import {
15
15
  type AgentSessionEvent,
16
16
  type ToolDefinition,
17
17
  } from "@earendil-works/pi-coding-agent";
18
+ import type { Model } from "@earendil-works/pi-ai";
18
19
  import type { ChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
19
20
  import {
20
21
  type SubagentLifecycleOutcome,
@@ -24,7 +25,7 @@ import { normalizeMaxTurns } from "#src/lifecycle/turn-limits";
24
25
  import { getSessionContextPercent, type SessionStatsLike } from "#src/lifecycle/usage";
25
26
  import { extractText } from "#src/session/context";
26
27
  import { getAgentConversation } from "#src/session/conversation";
27
- import type { SessionMessage } from "#src/types";
28
+ import type { SessionMessage, ThinkingLevel } from "#src/types";
28
29
 
29
30
  /** Outcome of one turn loop. */
30
31
  export interface TurnLoopResult {
@@ -94,6 +95,16 @@ export class SubagentSession {
94
95
  return this.meta.sessionId;
95
96
  }
96
97
 
98
+ /** Current model selected by the child session. */
99
+ get model(): Model<any> | undefined {
100
+ return this._session.model;
101
+ }
102
+
103
+ /** Effective level after the child session applied defaults and clamping. */
104
+ get thinkingLevel(): ThinkingLevel {
105
+ return this._session.thinkingLevel;
106
+ }
107
+
97
108
  /** Drive the initial run's turn loop; emits `completed` on accepted success. */
98
109
  async runTurnLoop(prompt: string, opts: TurnLoopOptions): Promise<TurnLoopResult> {
99
110
  const session = this._session;
@@ -26,6 +26,7 @@ import { WorkspaceBracket } from "#src/lifecycle/workspace-bracket";
26
26
  import { subscribeSubagentObserver } from "#src/observation/record-observer";
27
27
  import type { RunConfig } from "#src/runtime";
28
28
  import { formatModelLabel } from "#src/session/model-label";
29
+ import { resolveEffectiveThinkingLevel } from "#src/session/thinking-level";
29
30
  import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SessionMessage, SubagentType, ThinkingLevel } from "#src/types";
30
31
 
31
32
  /** Per-subagent lifecycle observer — created by SubagentManager for each spawn. */
@@ -126,13 +127,17 @@ export class Subagent {
126
127
  get responseText(): string { return this.state.responseText; }
127
128
  get maxTurns(): number | undefined { return this.execution.maxTurns; }
128
129
  /** Exact effective model label used by every operator-facing status surface. */
129
- get modelLabel(): string { return formatModelLabel(this.execution.model ?? this.execution.snapshot.model); }
130
+ get modelLabel(): string { return this._modelLabel; }
131
+ /** Exact effective thinking level used by every operator-facing status surface. */
132
+ get effectiveThinkingLevel(): ThinkingLevel { return this._effectiveThinkingLevel; }
130
133
 
131
134
  abortController: AbortController;
132
135
  private _promise?: Promise<void>;
133
136
  get promise(): Promise<void> | undefined { return this._promise; }
134
137
 
135
138
  private readonly execution: SubagentExecution;
139
+ private _modelLabel: string;
140
+ private _effectiveThinkingLevel: ThinkingLevel;
136
141
  private readonly listeners = new RunListeners();
137
142
  private readonly workspaceBracket: WorkspaceBracket;
138
143
 
@@ -235,6 +240,12 @@ export class Subagent {
235
240
 
236
241
  // Execution machinery — a single mandatory collaborator
237
242
  this.execution = init.execution;
243
+ this._modelLabel = formatModelLabel(this.execution.model ?? this.execution.snapshot.model);
244
+ this._effectiveThinkingLevel = resolveEffectiveThinkingLevel(
245
+ this.execution.model ?? this.execution.snapshot.model,
246
+ this.execution.thinkingLevel,
247
+ this.execution.snapshot.thinkingLevel,
248
+ );
238
249
 
239
250
  // Per-run lifecycle collaborators
240
251
  this.workspaceBracket = new WorkspaceBracket(
@@ -291,6 +302,12 @@ export class Subagent {
291
302
  return;
292
303
  }
293
304
 
305
+ // The SDK session is authoritative after creation: it has applied its
306
+ // defaults and model-capability clamp. Keep the record as the one source
307
+ // consumed by every operator-facing status surface.
308
+ this._modelLabel = formatModelLabel(this.subagentSession.model ?? this.execution.model ?? this.execution.snapshot.model);
309
+ this._effectiveThinkingLevel = this.subagentSession.thinkingLevel ?? this._effectiveThinkingLevel;
310
+
294
311
  this.flushPendingSteers();
295
312
  this.listeners.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, {
296
313
  onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
@@ -33,10 +33,18 @@ export class CompositeSubagentObserver implements SubagentManagerObserver {
33
33
  this.dispatch((o) => o.onSubagentCompleted(record), "onSubagentCompleted");
34
34
  }
35
35
 
36
+ onSubagentResumedStarted(record: Subagent): void {
37
+ this.dispatch((o) => o.onSubagentResumedStarted?.(record), "onSubagentResumedStarted");
38
+ }
39
+
36
40
  onSubagentResumed(record: Subagent): void {
37
41
  this.dispatch((o) => o.onSubagentResumed?.(record), "onSubagentResumed");
38
42
  }
39
43
 
44
+ onSubagentCleared(record: Subagent): void {
45
+ this.dispatch((o) => o.onSubagentCleared?.(record), "onSubagentCleared");
46
+ }
47
+
40
48
  onSubagentCompacted(record: Subagent, info: CompactionInfo): void {
41
49
  this.dispatch((o) => o.onSubagentCompacted(record, info), "onSubagentCompacted");
42
50
  }
@@ -7,6 +7,7 @@ export interface NotificationDetails {
7
7
  id: string;
8
8
  description: string;
9
9
  modelLabel: string;
10
+ thinkingLevel: Subagent["effectiveThinkingLevel"];
10
11
  status: string;
11
12
  toolUses: number;
12
13
  turnCount: number;
@@ -72,6 +73,7 @@ export function formatTaskNotification(record: Subagent, resultMaxLen: number):
72
73
  outputFile ? `<output-file>${escapeXml(outputFile)}</output-file>` : null,
73
74
  `<status>${escapeXml(status)}</status>`,
74
75
  `<model>${escapeXml(record.modelLabel)}</model>`,
76
+ `<thinking_level>${escapeXml(record.effectiveThinkingLevel)}</thinking_level>`,
75
77
  `<summary>Subagent "${escapeXml(record.description)}" ${record.status}</summary>`,
76
78
  `<result>${escapeXml(resultPreview)}</result>`,
77
79
  `<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
@@ -92,6 +94,7 @@ export function buildNotificationDetails(
92
94
  id: record.id,
93
95
  description: record.description,
94
96
  modelLabel: record.modelLabel,
97
+ thinkingLevel: record.effectiveThinkingLevel,
95
98
  status: record.status,
96
99
  toolUses: record.toolUses,
97
100
  turnCount: record.turnCount,
@@ -1,6 +1,6 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import type { NotificationDetails } from "#src/observation/notification";
3
- import { formatMs, formatTokens, formatTurns } from "#src/ui/display";
3
+ import { formatModelThinking, formatMs, formatTokens, formatTurns } from "#src/ui/display";
4
4
 
5
5
  /** Narrow theme interface — only the methods the renderer actually calls. */
6
6
  interface RendererTheme {
@@ -39,7 +39,7 @@ export function createNotificationRenderer() {
39
39
  let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
40
40
 
41
41
  // Line 2: stats
42
- const parts: string[] = [d.modelLabel];
42
+ const parts: string[] = [formatModelThinking(d.modelLabel, d.thinkingLevel)];
43
43
  if (d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
44
44
  if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
45
45
  if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
@@ -58,6 +58,7 @@ export class SubagentRuntime {
58
58
  getModelInfo(): ModelInfo {
59
59
  return {
60
60
  parentModel: this.currentCtx?.model as Model<any> | undefined,
61
+ parentThinkingLevel: this.currentCtx?.thinkingLevel,
61
62
  modelRegistry: this.currentCtx?.modelRegistry,
62
63
  };
63
64
  }
@@ -16,9 +16,10 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
16
16
  import type { WorkspaceProvider } from "#src/lifecycle/workspace";
17
17
  import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
18
18
  import { formatModelLabel } from "#src/session/model-label";
19
+ import { resolveEffectiveThinkingLevel } from "#src/session/thinking-level";
19
20
  import type { ModelRegistry } from "#src/session/model-resolver";
20
21
  import { resolveDefaultModel } from "#src/session/session-config";
21
- import type { AgentInvocation, SessionContext, Subagent } from "#src/types";
22
+ import type { AgentInvocation, SessionContext, Subagent, ThinkingLevel } from "#src/types";
22
23
 
23
24
  /** Narrow interface for the SubagentManager — avoids coupling to the concrete class. */
24
25
  export interface SubagentManagerLike {
@@ -71,6 +72,14 @@ export class SubagentsServiceAdapter implements SubagentsService {
71
72
  : this.resolveTypeDefaultModel(resolvedType);
72
73
  const description = options?.description ?? prompt.slice(0, 80);
73
74
  const isBackground = !(options?.foreground ?? false);
75
+ const snapshot = this.runtime.buildSnapshot(options?.inheritContext ?? false);
76
+ const requestedThinking = (options?.thinkingLevel
77
+ ?? this.agentRegistry?.resolveAgentConfig(resolvedType).thinking) as ThinkingLevel | undefined;
78
+ const effectiveThinkingLevel = resolveEffectiveThinkingLevel(
79
+ model,
80
+ requestedThinking,
81
+ snapshot.thinkingLevel,
82
+ );
74
83
  const invocation: AgentInvocation = {
75
84
  modelName: formatModelLabel(model),
76
85
  maxTurns: options?.maxTurns,
@@ -78,13 +87,12 @@ export class SubagentsServiceAdapter implements SubagentsService {
78
87
  runInBackground: isBackground,
79
88
  };
80
89
 
81
- const snapshot = this.runtime.buildSnapshot(options?.inheritContext ?? false);
82
90
  const parent = this.runtime.getSessionInfo();
83
91
  return this.manager.spawn(snapshot, resolvedType, prompt, {
84
92
  description,
85
93
  model,
86
94
  maxTurns: options?.maxTurns,
87
- thinkingLevel: options?.thinkingLevel,
95
+ thinkingLevel: effectiveThinkingLevel,
88
96
  inheritContext: options?.inheritContext,
89
97
  bypassQueue: options?.bypassQueue,
90
98
  isBackground,
@@ -50,6 +50,8 @@ export interface AssemblerContext {
50
50
  parentSystemPrompt: string;
51
51
  /** Parent's current model instance (fallback when agent config has no model). */
52
52
  parentModel?: unknown;
53
+ /** Parent's current effective level for inherited child thinking. */
54
+ parentThinkingLevel?: ThinkingLevel;
53
55
  /** Model registry for resolving config.model strings. */
54
56
  modelRegistry: {
55
57
  find(provider: string, modelId: string): unknown;
@@ -88,7 +90,7 @@ export interface SessionConfig {
88
90
  * Caller casts to the SDK’s Model<any> at the session-creation boundary.
89
91
  */
90
92
  model: unknown;
91
- /** Resolved thinking level (undefined inherit from session). */
93
+ /** Resolved thinking level; undefined only when no parent level is available. */
92
94
  thinkingLevel: ThinkingLevel | undefined;
93
95
  /** Per-agent configured max turns (from agentConfig.maxTurns). */
94
96
  agentMaxTurns: number | undefined;
@@ -175,8 +177,10 @@ export function assembleSessionConfig(
175
177
  options.model ??
176
178
  resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig.model);
177
179
 
178
- // Thinking level: explicit option > agent config > undefined (inherit)
179
- const thinkingLevel = options.thinkingLevel ?? agentConfig.thinking;
180
+ // Thinking level: explicit option > agent config > parent level (inherit).
181
+ // Passing the inherited value explicitly lets queued status use the same
182
+ // value that the child session will receive once it is admitted.
183
+ const thinkingLevel = options.thinkingLevel ?? agentConfig.thinking ?? ctx.parentThinkingLevel;
180
184
 
181
185
  // Per-agent max turns (combined with per-call maxTurns and defaultMaxTurns by SubagentSession.runTurnLoop)
182
186
  const agentMaxTurns = agentConfig.maxTurns;
@@ -0,0 +1,42 @@
1
+ import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat";
2
+ import type { Model } from "@earendil-works/pi-ai";
3
+ import type { ThinkingLevel } from "#src/types";
4
+
5
+ /** Pi's default when no session setting is available. */
6
+ export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium";
7
+
8
+ /**
9
+ * Resolve the level Pi will use for a model before its AgentSession exists.
10
+ *
11
+ * The session factory passes this resolved value to Pi, while the Subagent
12
+ * record replaces it with AgentSession.thinkingLevel after construction. That
13
+ * two-step flow keeps queued/launch status useful without allowing a local
14
+ * approximation to outrank the SDK's authoritative value.
15
+ */
16
+ export function resolveEffectiveThinkingLevel(
17
+ model: unknown,
18
+ requested?: ThinkingLevel,
19
+ inherited?: ThinkingLevel,
20
+ ): ThinkingLevel {
21
+ const level = requested ?? inherited ?? DEFAULT_THINKING_LEVEL;
22
+ // With no model yet, preserve the level Pi would receive rather than forcing
23
+ // `off`: createAgentSession may still resolve a configured default model.
24
+ // The created child session remains authoritative and supplies the clamp.
25
+ if (model == null) return level;
26
+ if (!isThinkingModel(model)) return "off";
27
+
28
+ const supported = getSupportedThinkingLevels(model);
29
+ return supported.includes(level)
30
+ ? level
31
+ : clampThinkingLevel(model, level);
32
+ }
33
+
34
+ function isThinkingModel(value: unknown): value is Model<any> {
35
+ if (typeof value !== "object" || value === null) return false;
36
+ const candidate = value as { provider?: unknown; id?: unknown; reasoning?: unknown };
37
+ return typeof candidate.provider === "string"
38
+ && candidate.provider.length > 0
39
+ && typeof candidate.id === "string"
40
+ && candidate.id.length > 0
41
+ && typeof candidate.reasoning === "boolean";
42
+ }
@@ -13,7 +13,7 @@ import { buildAgentGuidelines, buildDetails, buildTypeListText, textResult } fro
13
13
  import { renderAgentResult } from "#src/tools/result-renderer";
14
14
  import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
15
15
  import type { ParentSessionInfo, Subagent } from "#src/types";
16
- import { type AgentDetails, formatDuration, getDisplayName } from "#src/ui/display";
16
+ import { type AgentDetails, formatDuration, formatModelThinking, getDisplayName } from "#src/ui/display";
17
17
 
18
18
  // ---- Deps interfaces ----
19
19
 
@@ -113,9 +113,9 @@ export class AgentTool {
113
113
  : `Error: ${record.error}`
114
114
  : partial ?? "No output.";
115
115
  return textResult(
116
- `Model: ${record.modelLabel}\nRuntime: ${formatDuration(record.startedAt, record.completedAt)}\n\n${resultText}`,
116
+ `Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatDuration(record.startedAt, record.completedAt)}\n\n${resultText}`,
117
117
  buildDetails(
118
- { ...config.presentation.detailBase, modelName: record.modelLabel },
118
+ { ...config.presentation.detailBase, modelName: record.modelLabel, thinkingLevel: record.effectiveThinkingLevel },
119
119
  record,
120
120
  ),
121
121
  );
@@ -1,6 +1,7 @@
1
1
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
2
2
  import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
3
3
  import { textResult } from "#src/tools/helpers";
4
+ import { formatModelThinking } from "#src/ui/display";
4
5
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
5
6
  import type { ParentSessionInfo, Subagent } from "#src/types";
6
7
 
@@ -36,7 +37,7 @@ export function spawnBackground(
36
37
  model: execution.model,
37
38
  maxTurns: execution.effectiveMaxTurns,
38
39
  inheritContext: execution.inheritContext,
39
- thinkingLevel: execution.thinking,
40
+ thinkingLevel: execution.effectiveThinkingLevel,
40
41
  isBackground: true,
41
42
  origin: "tool",
42
43
  invocation: execution.agentInvocation,
@@ -52,7 +53,10 @@ export function spawnBackground(
52
53
  `Agent ${isQueued ? "queued" : "started"} in background.\n` +
53
54
  `Agent ID: ${id}\n` +
54
55
  `Type: ${identity.displayName}\n` +
55
- `Model: ${presentation.modelName}\n` +
56
+ `Model: ${formatModelThinking(
57
+ record?.modelLabel ?? presentation.modelName,
58
+ record?.effectiveThinkingLevel ?? execution.effectiveThinkingLevel,
59
+ )}\n` +
56
60
  `Runtime: 0.0s\n` +
57
61
  `Description: ${execution.description}\n` +
58
62
  (identity.fellBack ? `Fallback: requested ${identity.rawType}; using ${identity.subagentType}\n` : "") +
@@ -66,6 +70,7 @@ export function spawnBackground(
66
70
  `Do not duplicate this agent's work.`,
67
71
  {
68
72
  ...presentation.detailBase,
73
+ thinkingLevel: record?.effectiveThinkingLevel ?? execution.effectiveThinkingLevel,
69
74
  toolUses: 0,
70
75
  tokens: "",
71
76
  durationMs: 0,
@@ -12,6 +12,7 @@ import type { ParentSessionInfo, Subagent } from "#src/types";
12
12
  import {
13
13
  type AgentDetails,
14
14
  describeActivity,
15
+ formatModelThinking,
15
16
  formatMs,
16
17
  SPINNER,
17
18
  } from "#src/ui/display";
@@ -73,11 +74,12 @@ export async function runForeground(
73
74
  });
74
75
  };
75
76
 
76
- // Animate spinner at ~80ms (smooth rotation through 10 braille frames)
77
+ // Each update traverses the parent TUI tree; avoid doing that at animation
78
+ // speed while the foreground tool already owns the progress surface.
77
79
  const spinnerInterval = setInterval(() => {
78
80
  spinnerFrame++;
79
81
  streamUpdate();
80
- }, 80);
82
+ }, 500);
81
83
 
82
84
  streamUpdate();
83
85
 
@@ -92,7 +94,7 @@ export async function runForeground(
92
94
  model: execution.model,
93
95
  maxTurns: execution.effectiveMaxTurns,
94
96
  inheritContext: execution.inheritContext,
95
- thinkingLevel: execution.thinking,
97
+ thinkingLevel: execution.effectiveThinkingLevel,
96
98
  origin: "tool",
97
99
  invocation: execution.agentInvocation,
98
100
  signal,
@@ -125,7 +127,7 @@ export async function runForeground(
125
127
  if (record.status === "error") {
126
128
  const partial = record.result?.trim();
127
129
  return textResult(
128
- `${fallbackNote}Model: ${record.modelLabel}\nRuntime: ${formatMs(details.durationMs)}\nAgent failed: ${record.error}` +
130
+ `${fallbackNote}Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(details.durationMs)}\nAgent failed: ${record.error}` +
129
131
  (partial ? `\n\nPartial output before the failure:\n${partial}` : ""),
130
132
  details,
131
133
  );
@@ -135,7 +137,7 @@ export async function runForeground(
135
137
  const statsParts = [`${record.toolUses} tool uses`];
136
138
  if (tokenText) statsParts.push(tokenText);
137
139
  return textResult(
138
- `${fallbackNote}Model: ${record.modelLabel}\nRuntime: ${formatMs(durationMs)}\nAgent completed (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
140
+ `${fallbackNote}Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(durationMs)}\nAgent completed (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
139
141
  (record.result?.trim() ?? "No output."),
140
142
  details,
141
143
  );
@@ -8,12 +8,15 @@
8
8
  */
9
9
 
10
10
  import type { SubagentStatus } from "#src/lifecycle/subagent";
11
+ import type { ThinkingLevel } from "#src/types";
12
+ import { formatModelThinking } from "#src/ui/display";
11
13
 
12
14
  /** The data a get_subagent_result report renders from — only what the formatter reads. */
13
15
  export interface AgentReport {
14
16
  id: string;
15
17
  displayName: string;
16
18
  modelLabel: string;
19
+ thinkingLevel: ThinkingLevel;
17
20
  status: SubagentStatus;
18
21
  toolUses: number;
19
22
  /** Pre-formatted lifetime token total; "" when zero. */
@@ -63,7 +66,7 @@ export function renderReportBody(report: AgentReport): string {
63
66
  export function formatAgentReport(report: AgentReport): string {
64
67
  let output =
65
68
  `Agent: ${report.id}\n` +
66
- `Type: ${report.displayName} | Model: ${report.modelLabel} | Status: ${report.status} | ${renderStatsParts(report).join(" | ")}\n` +
69
+ `Type: ${report.displayName} | Model: ${formatModelThinking(report.modelLabel, report.thinkingLevel)} | Status: ${report.status} | ${renderStatsParts(report).join(" | ")}\n` +
67
70
  `Description: ${report.description}\n\n`;
68
71
  output += renderReportBody(report);
69
72
  if (report.conversation) {
@@ -47,6 +47,7 @@ export class GetResultTool {
47
47
  id: record.id,
48
48
  displayName: getDisplayName(record.type, this.registry),
49
49
  modelLabel: record.modelLabel,
50
+ thinkingLevel: record.effectiveThinkingLevel,
50
51
  status: record.status,
51
52
  toolUses: record.toolUses,
52
53
  tokens: formatLifetimeTokens(record),
@@ -18,7 +18,7 @@ export function getStatusNote(status: string): string {
18
18
 
19
19
  /** Build AgentDetails from a base + record-specific fields. */
20
20
  export function buildDetails(
21
- base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
21
+ base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "thinkingLevel" | "tags">,
22
22
  record: {
23
23
  toolUses: number;
24
24
  startedAt: number;
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { AgentDetails, Theme } from "#src/ui/display";
10
- import { formatMs, formatTurns, SPINNER } from "#src/ui/display";
10
+ import { formatModelThinking, formatMs, formatTurns, SPINNER } from "#src/ui/display";
11
11
 
12
12
  // ---- Dispatcher ----
13
13
 
@@ -42,7 +42,10 @@ export function renderRunning(details: AgentDetails, theme: Theme): string {
42
42
 
43
43
  /** Render background launch status. */
44
44
  export function renderBackground(details: AgentDetails, theme: Theme): string {
45
- const identity = [details.modelName, formatMs(details.durationMs)].filter(Boolean).join(" · ");
45
+ const identity = [
46
+ formatModelThinking(details.modelName ?? "unknown model", details.thinkingLevel),
47
+ formatMs(details.durationMs),
48
+ ].join(" · ");
46
49
  return theme.fg("dim", ` \u23BF Running in background (ID: ${details.agentId}) · ${identity}`);
47
50
  }
48
51
 
@@ -113,8 +116,8 @@ export function renderFailed(details: AgentDetails, theme: Theme): string {
113
116
  */
114
117
  export function renderStats(details: AgentDetails, theme: Theme): string {
115
118
  const parts: string[] = [];
116
- if (details.modelName) parts.push(details.modelName);
117
- if (details.tags) parts.push(...details.tags);
119
+ parts.push(formatModelThinking(details.modelName ?? "unknown model", details.thinkingLevel));
120
+ if (details.tags) parts.push(...details.tags.filter((tag) => !tag.startsWith("thinking: ")));
118
121
  if (details.turnCount != null && details.turnCount > 0) {
119
122
  parts.push(formatTurns(details.turnCount, details.maxTurns));
120
123
  }