@hyperdreamer/pi-webui 1.10.6 → 1.11.0-beta.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.
Files changed (34) hide show
  1. package/dist/client/assets/{CodeViewer-BJJZQ999.js → CodeViewer-DLZCHlMU.js} +1 -1
  2. package/dist/client/assets/{UnifiedDiffViewer-jpQy9-p7.js → UnifiedDiffViewer-XCb8NlrO.js} +1 -1
  3. package/dist/client/assets/{index-DWWtrUuQ.js → index-BaDGxg8F.js} +850 -422
  4. package/dist/client/index.html +1 -1
  5. package/dist/config.js +88 -14
  6. package/dist/config.js.map +1 -1
  7. package/dist/pi-webui-plugins/workspace-memory/pi-webui-plugin.js +3 -2
  8. package/dist/plugin-api.d.ts +28 -0
  9. package/dist/server/configRoutes.js +8 -1
  10. package/dist/server/configRoutes.js.map +1 -1
  11. package/dist/server/sessiond/sessionProxyRoutes.js +1 -0
  12. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  13. package/dist/server/sessiond.js +21 -3
  14. package/dist/server/sessiond.js.map +1 -1
  15. package/dist/server/sessions/modelTierRegistry.js +88 -0
  16. package/dist/server/sessions/modelTierRegistry.js.map +1 -0
  17. package/dist/server/sessions/modelTierSettingsRoutes.js +36 -0
  18. package/dist/server/sessions/modelTierSettingsRoutes.js.map +1 -0
  19. package/dist/server/sessions/modelTierSettingsService.js +100 -0
  20. package/dist/server/sessions/modelTierSettingsService.js.map +1 -0
  21. package/dist/server/sessions/piSessionService.js +49 -4
  22. package/dist/server/sessions/piSessionService.js.map +1 -1
  23. package/dist/server/sessions/spawnSubsessionTool.js +12 -0
  24. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  25. package/dist/shared/apiTypes.d.ts +33 -0
  26. package/dist/shared/apiTypes.js +2 -0
  27. package/dist/shared/apiTypes.js.map +1 -1
  28. package/dist/shared/capabilities.js +3 -0
  29. package/dist/shared/capabilities.js.map +1 -1
  30. package/dist/shared/federatedRoutes.js +2 -0
  31. package/dist/shared/federatedRoutes.js.map +1 -1
  32. package/docs/config.md +25 -2
  33. package/docs/plugins.md +93 -16
  34. package/package.json +2 -1
@@ -0,0 +1,88 @@
1
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2
+ import { isKnownThinkingLevel } from "../../shared/thinkingLevels.js";
3
+ import { MODEL_TIERS } from "../../shared/apiTypes.js";
4
+ export { MODEL_TIERS } from "../../shared/apiTypes.js";
5
+ export function isModelTier(value) {
6
+ return MODEL_TIERS.some((tier) => tier === value);
7
+ }
8
+ /** Default thinking-level lookup, delegating to Pi rather than a local table. */
9
+ export function runtimeThinkingLevels(model) {
10
+ return model === undefined ? [] : getSupportedThinkingLevels(model);
11
+ }
12
+ /**
13
+ * Bind config intent to one currently available runtime model. This wrapper is
14
+ * deliberately synchronous: the session daemon's model runtime publishes a
15
+ * current catalog snapshot, and a request must fail closed against that
16
+ * snapshot rather than inventing a fallback while it refreshes.
17
+ */
18
+ export function createModelTierRegistry(deps) {
19
+ return {
20
+ resolve(tier) {
21
+ const config = deps.loadConfig();
22
+ if (config.modelTiersError !== undefined) {
23
+ throw new Error(`model tier configuration is invalid: ${config.modelTiersError}`);
24
+ }
25
+ if (config.modelTiers === undefined) {
26
+ throw new Error("model tier configuration is missing");
27
+ }
28
+ const models = deps.models();
29
+ const resolved = resolveTier(tier, config.modelTiers, {
30
+ models,
31
+ supportedThinkingLevels: (model) => deps.supportedThinkingLevels(model),
32
+ });
33
+ const model = models.find((candidate) => candidate.provider === resolved.model.provider && candidate.id === resolved.model.id);
34
+ if (model === undefined) {
35
+ // This is defensive against a catalog changing between the resolution
36
+ // lookup and the runtime handoff. It is still a terminal resolution
37
+ // failure, never a neighbouring-tier substitution.
38
+ throw new Error(`tier ${tier} names unavailable model ${resolved.model.provider}/${resolved.model.id}`);
39
+ }
40
+ return { tier: resolved.tier, model, thinkingLevel: resolved.thinkingLevel };
41
+ },
42
+ };
43
+ }
44
+ function describeModel(model) {
45
+ return `${model.provider}/${model.id}`;
46
+ }
47
+ /**
48
+ * Resolve one tier, or throw explaining why not.
49
+ *
50
+ * Every failure is terminal by design. Substituting a neighbouring tier would
51
+ * silently run weaker or stronger work than the caller asked for, which is the
52
+ * exact failure the tier system exists to prevent.
53
+ */
54
+ export function resolveTier(tier, ladder, deps) {
55
+ if (!isModelTier(tier))
56
+ throw new Error(`unknown tier: ${tier}`);
57
+ const entry = ladder[tier];
58
+ if (entry === undefined)
59
+ throw new Error(`tier ${tier} has no ladder entry`);
60
+ const available = deps.models.find((candidate) => candidate.provider === entry.model.provider && candidate.id === entry.model.id);
61
+ if (available === undefined) {
62
+ throw new Error(`tier ${tier} names unavailable model ${describeModel(entry.model)}`);
63
+ }
64
+ if (!isKnownThinkingLevel(entry.thinkingLevel)) {
65
+ throw new Error(`tier ${tier} names unknown thinking level ${entry.thinkingLevel}`);
66
+ }
67
+ const supported = deps.supportedThinkingLevels(available);
68
+ if (!supported.includes(entry.thinkingLevel)) {
69
+ throw new Error(`tier ${tier} names thinking level ${entry.thinkingLevel}, unsupported by ${describeModel(entry.model)}`);
70
+ }
71
+ return { tier, model: { ...entry.model }, thinkingLevel: entry.thinkingLevel };
72
+ }
73
+ /**
74
+ * Validate a complete ladder. Returns a reason rather than throwing, because a
75
+ * broken ladder is a reportable configuration state, not an exception path.
76
+ */
77
+ export function validateLadder(ladder, deps) {
78
+ for (const tier of MODEL_TIERS) {
79
+ try {
80
+ resolveTier(tier, ladder, deps);
81
+ }
82
+ catch (error) {
83
+ return { valid: false, reason: error instanceof Error ? error.message : String(error) };
84
+ }
85
+ }
86
+ return { valid: true };
87
+ }
88
+ //# sourceMappingURL=modelTierRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelTierRegistry.js","sourceRoot":"","sources":["../../../src/server/sessions/modelTierRegistry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { parseModelTiersConfig } from "../../config.js";
2
+ export function registerModelTierSettingsRoutes(app, service, prefix = "") {
3
+ app.get(`${prefix}/model-tiers`, async (_request, reply) => {
4
+ try {
5
+ return await service.inspect();
6
+ }
7
+ catch (error) {
8
+ return reply.code(400).send({ error: errorMessage(error) });
9
+ }
10
+ });
11
+ app.put(`${prefix}/model-tiers`, async (request, reply) => {
12
+ try {
13
+ return await service.replace(parseReplacement(request.body));
14
+ }
15
+ catch (error) {
16
+ return reply.code(400).send({ error: errorMessage(error) });
17
+ }
18
+ });
19
+ }
20
+ function parseReplacement(value) {
21
+ if (!isRecord(value))
22
+ throw new Error("Expected object body");
23
+ const unknownField = Object.keys(value).find((key) => key !== "ladder");
24
+ if (unknownField !== undefined)
25
+ throw new Error(`unknown field ${JSON.stringify(unknownField)}; expected exactly ladder`);
26
+ if (!Object.prototype.hasOwnProperty.call(value, "ladder"))
27
+ throw new Error("ladder is required");
28
+ return parseModelTiersConfig(value["ladder"], "request body ladder");
29
+ }
30
+ function isRecord(value) {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+ function errorMessage(error) {
34
+ return error instanceof Error ? error.message : String(error);
35
+ }
36
+ //# sourceMappingURL=modelTierSettingsRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelTierSettingsRoutes.js","sourceRoot":"","sources":["../../../src/server/sessions/modelTierSettingsRoutes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AASxD,MAAM,UAAU,+BAA+B,CAAC,GAAoB,EAAE,OAAsC,EAAE,MAAM,GAAG,EAAE;IACvH,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACzD,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAoB,GAAG,MAAM,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC3E,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC9D,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC;IACxE,IAAI,YAAY,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,2BAA2B,CAAC,CAAC;IAC1H,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAClG,OAAO,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,qBAAqB,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,100 @@
1
+ import { MODEL_TIERS, } from "../../shared/apiTypes.js";
2
+ import { resolveTier } from "./modelTierRegistry.js";
3
+ /**
4
+ * Inspect and atomically replace the machine-global model-tier ladder against
5
+ * the daemon's latest authenticated model catalog.
6
+ *
7
+ * File access and runtime refresh remain dependency-injected so this boundary
8
+ * can be tested without starting the daemon or making network requests.
9
+ */
10
+ export function createModelTierSettingsService(deps) {
11
+ const inspect = async () => {
12
+ const snapshot = await refreshedSnapshot();
13
+ return responseFor(deps.loadConfig(), snapshot);
14
+ };
15
+ return {
16
+ inspect,
17
+ replace: async (ladder) => {
18
+ const snapshot = await refreshedSnapshot();
19
+ const rows = validationRows(ladder, snapshot);
20
+ const invalidRows = MODEL_TIERS.filter((tier) => !rows[tier].valid);
21
+ if (invalidRows.length > 0) {
22
+ throw new Error(invalidRows.map((tier) => rows[tier].reason ?? `tier ${tier} is invalid`).join("; "));
23
+ }
24
+ await deps.saveConfig({ modelTiers: ladder });
25
+ return await inspect();
26
+ },
27
+ };
28
+ async function refreshedSnapshot() {
29
+ await deps.modelRuntime.refresh({ allowNetwork: false });
30
+ return deps.modelRuntime.getAvailableSnapshot();
31
+ }
32
+ function responseFor(config, models) {
33
+ const modelOptions = models.map((model) => modelOptionFor(model));
34
+ if (config.modelTiersError !== undefined) {
35
+ return {
36
+ contractVersion: 1,
37
+ configError: config.modelTiersError,
38
+ models: modelOptions,
39
+ rows: invalidRows(config.modelTiersError),
40
+ valid: false,
41
+ };
42
+ }
43
+ if (config.modelTiers === undefined) {
44
+ return {
45
+ contractVersion: 1,
46
+ models: modelOptions,
47
+ rows: invalidRows("model tier configuration is missing"),
48
+ valid: false,
49
+ };
50
+ }
51
+ const rows = validationRows(config.modelTiers, models);
52
+ return {
53
+ contractVersion: 1,
54
+ ladder: config.modelTiers,
55
+ models: modelOptions,
56
+ rows,
57
+ valid: MODEL_TIERS.every((tier) => rows[tier].valid),
58
+ };
59
+ }
60
+ function modelOptionFor(model) {
61
+ return {
62
+ model: { provider: model.provider, id: model.id },
63
+ ...(model.name === undefined ? {} : { name: model.name }),
64
+ thinkingLevels: [...deps.thinkingLevelsForModel(model)],
65
+ };
66
+ }
67
+ function validationRows(ladder, models) {
68
+ return {
69
+ economy: validationFor("economy", ladder, models),
70
+ fast: validationFor("fast", ladder, models),
71
+ standard: validationFor("standard", ladder, models),
72
+ advanced: validationFor("advanced", ladder, models),
73
+ capable: validationFor("capable", ladder, models),
74
+ frontier: validationFor("frontier", ladder, models),
75
+ };
76
+ }
77
+ function validationFor(tier, ladder, models) {
78
+ try {
79
+ resolveTier(tier, ladder, {
80
+ models,
81
+ supportedThinkingLevels: (model) => deps.thinkingLevelsForModel(model),
82
+ });
83
+ return { valid: true };
84
+ }
85
+ catch (error) {
86
+ return { valid: false, reason: error instanceof Error ? error.message : String(error) };
87
+ }
88
+ }
89
+ function invalidRows(reason) {
90
+ return {
91
+ economy: { valid: false, reason },
92
+ fast: { valid: false, reason },
93
+ standard: { valid: false, reason },
94
+ advanced: { valid: false, reason },
95
+ capable: { valid: false, reason },
96
+ frontier: { valid: false, reason },
97
+ };
98
+ }
99
+ }
100
+ //# sourceMappingURL=modelTierSettingsService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelTierSettingsService.js","sourceRoot":"","sources":["../../../src/server/sessions/modelTierSettingsService.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { loadPiWebUiConfig } from "../../config.js";
3
4
  import { open, readFile, writeFile } from "node:fs/promises";
4
5
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
5
6
  import { projectBrowserMessage } from "../browserMessageProjection.js";
@@ -15,6 +16,7 @@ import { deterministicSessionName, fallbackSessionName, generateShortSessionName
15
16
  import { computeEditPreview } from "./editPreview.js";
16
17
  import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
17
18
  import { parsePromptAttachments } from "../../shared/promptAttachments.js";
19
+ import { isKnownThinkingLevel } from "../../shared/thinkingLevels.js";
18
20
  import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
19
21
  import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
20
22
  import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
@@ -24,6 +26,7 @@ import { planForceCleanup, planSessionCleanup, summarizeForceCleanupExecution, s
24
26
  import { SessionNotificationStore, } from "./sessionNotificationStore.js";
25
27
  import { plainTextTheme } from "./plainTextTheme.js";
26
28
  import { SessionUnreadStore } from "./sessionUnreadStore.js";
29
+ import { createModelTierRegistry, isModelTier, runtimeThinkingLevels } from "./modelTierRegistry.js";
27
30
  const noopLogger = { info() { } };
28
31
  const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
29
32
  const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
@@ -86,6 +89,15 @@ function requirePromptText(value) {
86
89
  throw new Error("Prompt text is required");
87
90
  return value;
88
91
  }
92
+ function leadingTierDirective(prompt) {
93
+ const match = /^(?:\uFEFF)?\/tier-([^\s\r\n]+)(?:[ \t]*(?:\r?\n|$))/u.exec(prompt);
94
+ if (match === null)
95
+ return undefined;
96
+ const value = match[1];
97
+ if (value === undefined || !isModelTier(value))
98
+ throw new Error(`Unknown leading tier directive: /tier-${value ?? ""}`);
99
+ return value;
100
+ }
89
101
  function parsePromptStreamingBehavior(value) {
90
102
  if (value === undefined)
91
103
  return undefined;
@@ -199,26 +211,30 @@ export function dismissSessionWarning(session, dismissId) {
199
211
  function defaultCreateAgentRuntime(createRuntime, options) {
200
212
  if (!(options.sessionManager instanceof SessionManager))
201
213
  throw new Error("Default runtime creation requires an SDK SessionManager");
202
- const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
214
+ const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
203
215
  return createAgentSessionRuntime(runtimeFactory, {
204
216
  cwd: options.cwd,
205
217
  agentDir: options.agentDir,
206
218
  sessionManager: options.sessionManager,
207
219
  });
208
220
  }
209
- function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
221
+ function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, initialThinkingLevel, delegationToolsEnabled) {
210
222
  // These inputs belong only to the session being opened. A later runtime
211
223
  // replacement resolves its own model and delegation capability.
212
224
  let pendingInitialModel = initialModel;
225
+ let pendingInitialThinkingLevel = initialThinkingLevel;
213
226
  let pendingDelegationToolsEnabled = delegationToolsEnabled;
214
227
  return async (options) => {
215
228
  const model = pendingInitialModel;
229
+ const thinkingLevel = pendingInitialThinkingLevel;
216
230
  const toolsEnabled = pendingDelegationToolsEnabled;
217
231
  pendingInitialModel = undefined;
232
+ pendingInitialThinkingLevel = undefined;
218
233
  pendingDelegationToolsEnabled = undefined;
219
234
  return createRuntime({
220
235
  ...options,
221
236
  ...(model === undefined ? {} : { initialModel: model }),
237
+ ...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
222
238
  ...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
223
239
  });
224
240
  };
@@ -231,7 +247,7 @@ export function createPiWebUiCustomToolDefinitions(cwd, delegationEnabled, spawn
231
247
  ];
232
248
  }
233
249
  function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions) {
234
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
250
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
235
251
  const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
236
252
  const resolvedDelegationToolsEnabled = delegationToolsEnabled
237
253
  ?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
@@ -242,6 +258,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
242
258
  customTools,
243
259
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
244
260
  ...(initialModel === undefined ? {} : { model: initialModel }),
261
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
245
262
  });
246
263
  return { ...result, services, diagnostics: services.diagnostics };
247
264
  };
@@ -326,6 +343,17 @@ export class PiSessionService {
326
343
  read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
327
344
  });
328
345
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
346
+ this.modelTierRegistry = deps.modelTierRegistry ?? createModelTierRegistry({
347
+ loadConfig: () => {
348
+ const loaded = loadPiWebUiConfig();
349
+ return {
350
+ ...(loaded.config.modelTiers === undefined ? {} : { modelTiers: loaded.config.modelTiers }),
351
+ ...(loaded.modelTiersError === undefined ? {} : { modelTiersError: loaded.modelTiersError }),
352
+ };
353
+ },
354
+ models: () => this.modelRuntime.getAvailableSnapshot(),
355
+ supportedThinkingLevels: runtimeThinkingLevels,
356
+ });
329
357
  this.workspaceActivity = deps.workspaceActivity;
330
358
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
331
359
  this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
@@ -522,6 +550,7 @@ export class PiSessionService {
522
550
  async startSession(cwd, options) {
523
551
  const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
524
552
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
553
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
525
554
  ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
526
555
  });
527
556
  const { session } = active.runtime;
@@ -571,9 +600,24 @@ export class PiSessionService {
571
600
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
572
601
  if (!decision.allowed)
573
602
  throw spawnTargetError(decision);
603
+ let initialModel = input.model;
604
+ let initialThinkingLevel;
605
+ if (input.tier !== undefined) {
606
+ const echoedTier = leadingTierDirective(input.prompt);
607
+ if (echoedTier !== undefined && echoedTier !== input.tier) {
608
+ throw new Error(`Leading tier directive /tier-${echoedTier} disagrees with typed tier ${input.tier}`);
609
+ }
610
+ const resolved = this.modelTierRegistry.resolve(input.tier);
611
+ if (!isKnownThinkingLevel(resolved.thinkingLevel)) {
612
+ throw new Error(`tier ${input.tier} resolved to unknown thinking level ${resolved.thinkingLevel}`);
613
+ }
614
+ initialModel = resolved.model;
615
+ initialThinkingLevel = resolved.thinkingLevel;
616
+ }
574
617
  const created = await this.startSession(decision.cwd, {
575
618
  ...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
576
- ...(input.model === undefined ? {} : { initialModel: input.model }),
619
+ ...(initialModel === undefined ? {} : { initialModel }),
620
+ ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
577
621
  creationProvenance: "tracked-subsession",
578
622
  });
579
623
  const parentSessionFile = nonEmptyString(input.parentSessionFile);
@@ -1899,6 +1943,7 @@ export class PiSessionService {
1899
1943
  sessionManager,
1900
1944
  delegationToolsEnabled,
1901
1945
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
1946
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
1902
1947
  });
1903
1948
  const active = { runtime, unsubscribe: noop };
1904
1949
  let boundSession = runtime.session;