pi-advisor-flow 0.6.0 → 0.8.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { registerCommands } from "../src/commands/registration.ts";
3
4
  import { setHerdrBlockedEmitter } from "../src/herdr.ts";
4
5
  import { AdvisorSessionState } from "../src/session-state.ts";
@@ -30,7 +31,7 @@ export const runAdvisorGate = (
30
31
  ...args: Parameters<typeof runAdvisorGateImplementation>
31
32
  ) => runAdvisorGateImplementation(...args);
32
33
 
33
- export default function (pi: ExtensionAPI) {
34
+ export default function registerPiAdvisor(pi: ExtensionAPI) {
34
35
  const sessionState = new AdvisorSessionState();
35
36
  const scoutStatus = new ScoutStatusManager();
36
37
  setHerdrBlockedEmitter((active, label) =>
package/package.json CHANGED
@@ -1,25 +1,23 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
- "pi-package",
7
- "pi-extension",
8
- "pi-coding-agent",
9
6
  "advisor",
7
+ "herdr",
8
+ "jev",
10
9
  "pi-advisor",
11
- "herdr"
10
+ "pi-coding-agent",
11
+ "pi-extension",
12
+ "pi-package"
12
13
  ],
13
14
  "homepage": "https://github.com/philipbrembeck/pi-advisor",
15
+ "license": "MIT",
16
+ "author": "Philip Brembeck",
14
17
  "repository": {
15
18
  "type": "git",
16
19
  "url": "https://github.com/philipbrembeck/pi-advisor.git"
17
20
  },
18
- "license": "MIT",
19
- "author": "Philip Brembeck",
20
- "type": "module",
21
- "main": "dist/index.js",
22
- "types": "extensions/index.ts",
23
21
  "files": [
24
22
  "dist",
25
23
  "extensions",
@@ -28,6 +26,9 @@
28
26
  "LICENSE",
29
27
  "README.md"
30
28
  ],
29
+ "type": "module",
30
+ "main": "dist/index.js",
31
+ "types": "extensions/index.ts",
31
32
  "scripts": {
32
33
  "bench:decisions": "bun bench/src/cli.ts decisions",
33
34
  "bench:evaluate": "bun bench/src/cli.ts evaluate",
@@ -35,10 +36,10 @@
35
36
  "bench:screen": "bun bench/src/cli.ts screen",
36
37
  "build": "bun build ./extensions/index.ts --outfile dist/index.js --target node --format esm --external @earendil-works/pi-ai --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --external typebox",
37
38
  "check:boundaries": "node scripts/check-module-boundaries.mjs",
38
- "format": "bunx ultracite fix --linter-enabled=false",
39
+ "format": "oxfmt",
39
40
  "knip": "knip",
40
- "lint": "bunx ultracite check",
41
- "lint:fix": "bunx ultracite fix",
41
+ "lint": "oxlint",
42
+ "lint:fix": "oxlint --fix",
42
43
  "prepack": "bun run build >/dev/null",
43
44
  "package:check": "node scripts/check-package.mjs",
44
45
  "prepare": "husky",
@@ -46,39 +47,30 @@
46
47
  "test": "bun test && bun run test:node-loader",
47
48
  "test:bench": "bun --config=./bunfig.bench.toml test ./bench/test/",
48
49
  "test:node-loader": "node --test test/default-resource-loader.node.mjs",
49
- "typecheck": "tsc --noEmit"
50
- },
51
- "lint-staged": {
52
- "*.{json,jsonc,ts}": "bun run lint:fix --"
53
- },
54
- "resolutions": {
55
- "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
56
- "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9"
57
- },
58
- "overrides": {
59
- "brace-expansion": "5.0.9",
60
- "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
61
- "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
62
- "undici": "8.9.0"
50
+ "typecheck": "tsc --noEmit",
51
+ "format:check": "oxfmt --check"
63
52
  },
64
53
  "devDependencies": {
65
- "@biomejs/biome": "2.5.12",
66
- "@earendil-works/pi-ai": "^0.85.1",
67
- "@earendil-works/pi-coding-agent": "^0.85.1",
68
- "@earendil-works/pi-tui": "^0.85.1",
69
- "@types/node": "^26.4.1",
70
- "bun-types": "1.4.1",
54
+ "@earendil-works/pi-ai": "0.87.1",
55
+ "@earendil-works/pi-coding-agent": "0.87.1",
56
+ "@earendil-works/pi-tui": "0.87.1",
57
+ "@oxlint/plugins": "1.85.0",
58
+ "@types/node": "^26.6.2",
59
+ "@typesafe-ai/sdk": "^0.6.0",
60
+ "bun-types": "1.4.2",
71
61
  "husky": "^9.1.7",
72
- "knip": "^6.35.1",
73
- "lint-staged": "^17.4.1",
74
- "typebox": "^1.3.25",
62
+ "knip": "^6.37.0",
63
+ "lint-staged": "^17.5.1",
64
+ "oxfmt": "0.70.0",
65
+ "oxlint": "1.85.0",
66
+ "typebox": "^1.3.34",
75
67
  "typescript": "^7.0.2",
76
- "ultracite": "7.10.8"
68
+ "ultracite": "7.12.0"
77
69
  },
78
70
  "peerDependencies": {
79
- "@earendil-works/pi-ai": "^0.84.1 || ^0.85.1",
80
- "@earendil-works/pi-coding-agent": "^0.84.1 || ^0.85.1",
81
- "@earendil-works/pi-tui": "^0.84.1 || ^0.85.1",
71
+ "@earendil-works/pi-ai": "^0.85.1 || ^0.87.1",
72
+ "@earendil-works/pi-coding-agent": "^0.85.1 || ^0.87.1",
73
+ "@earendil-works/pi-tui": "^0.85.1 || ^0.87.1",
82
74
  "typebox": "^1.1.38"
83
75
  },
84
76
  "peerDependenciesMeta": {
@@ -95,6 +87,29 @@
95
87
  "optional": true
96
88
  }
97
89
  },
90
+ "resolutions": {
91
+ "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
92
+ "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9"
93
+ },
94
+ "overrides": {
95
+ "brace-expansion": "5.0.9",
96
+ "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
97
+ "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
98
+ "undici": "8.9.0"
99
+ },
100
+ "lint-staged": {
101
+ "*.{ts,tsx}": [
102
+ "oxfmt",
103
+ "oxlint --fix"
104
+ ],
105
+ "{src,test,scripts,tools,extensions}/**/*.{ts,tsx}": [
106
+ "oxfmt",
107
+ "oxlint --fix"
108
+ ],
109
+ "*.{json,jsonc}": [
110
+ "oxfmt"
111
+ ]
112
+ },
98
113
  "pi": {
99
114
  "extensions": [
100
115
  "./dist/index.js"
@@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, open, realpath } from "node:fs/promises";
4
4
  import { isAbsolute, relative, resolve } from "node:path";
5
+
6
+ import { isString } from "./content-utils.ts";
5
7
  import { redactAndCapText } from "./redaction.ts";
6
8
 
7
9
  export const ADVISOR_FILE_MAX_BYTES = 8 * 1024;
@@ -12,7 +14,7 @@ export interface UntrackedAttachment {
12
14
  text: string;
13
15
  }
14
16
 
15
- const PATH_SEGMENTS = /[\\/]/;
17
+ const PATH_SEGMENTS = /[\\/]/u;
16
18
 
17
19
  const within = (root: string, candidate: string) => {
18
20
  const path = relative(root, candidate);
@@ -21,9 +23,9 @@ const within = (root: string, candidate: string) => {
21
23
  const normalizeRelativePath = (root: string, path: string) =>
22
24
  relative(root, resolve(root, path));
23
25
 
24
- const normalizeRequestedPath = (root: string, value: unknown) => {
26
+ const normalizeRequestedPath = (root: string, value: string) => {
25
27
  if (
26
- typeof value !== "string" ||
28
+ !isString(value) ||
27
29
  !value ||
28
30
  isAbsolute(value) ||
29
31
  value.split(PATH_SEGMENTS).includes("..")
@@ -36,7 +38,7 @@ const normalizeRequestedPath = (root: string, value: unknown) => {
36
38
  const git = (cwd: string, args: string[]) =>
37
39
  execFileSync("git", args, {
38
40
  cwd,
39
- encoding: "utf8",
41
+ encoding: "utf-8",
40
42
  maxBuffer: 16 * 1024 * 1024,
41
43
  shell: false,
42
44
  stdio: ["ignore", "pipe", "pipe"],
@@ -48,7 +50,7 @@ const repositoryRoot = (cwd: string) => {
48
50
  try {
49
51
  return realpath(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
50
52
  } catch {
51
- return Promise.resolve(undefined);
53
+ return Promise.resolve();
52
54
  }
53
55
  };
54
56
 
@@ -108,9 +110,9 @@ const readAttachment = async (
108
110
  if (!within(root, resolved)) {
109
111
  return;
110
112
  }
113
+ // fs.open needs combined numeric flags for O_NOFOLLOW; O_RDONLY (0) and O_NOFOLLOW occupy disjoint bits, so + is exact.
111
114
  const flags = constants.O_NOFOLLOW
112
- ? // biome-ignore lint/suspicious/noBitwiseOperators: fs.open requires numeric flags for O_NOFOLLOW.
113
- constants.O_RDONLY | constants.O_NOFOLLOW
115
+ ? constants.O_RDONLY + constants.O_NOFOLLOW
114
116
  : constants.O_RDONLY;
115
117
  const file = await open(resolved, flags);
116
118
  try {
@@ -120,13 +122,13 @@ const readAttachment = async (
120
122
  }
121
123
  const buffer = Buffer.alloc(available + 1);
122
124
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
123
- const raw = buffer.subarray(0, bytesRead).toString("utf8");
125
+ const raw = buffer.subarray(0, bytesRead).toString("utf-8");
124
126
  if (raw.includes("\0")) {
125
127
  return;
126
128
  }
127
129
  const text = redactAndCapText(raw, available, redact);
128
130
  return {
129
- bytes: Buffer.byteLength(text, "utf8"),
131
+ bytes: Buffer.byteLength(text, "utf-8"),
130
132
  path: normalizedName,
131
133
  text,
132
134
  };
@@ -169,7 +171,6 @@ const readFiles = async (
169
171
  break;
170
172
  }
171
173
  // Sequentially enforce the aggregate disclosure budget.
172
- // biome-ignore lint/performance/noAwaitInLoops: each accepted file consumes the remaining budget.
173
174
  const attachment = await readAttachment(
174
175
  root,
175
176
  normalizedName,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import {
3
4
  advisorEffortRef,
4
5
  advisorRef,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { parseArgs } from "../config/args.ts";
3
4
  import {
4
5
  advisorEffortRef,
@@ -116,6 +117,7 @@ export const activateAdvisor = async (
116
117
  // later attempt.
117
118
  runtime.pendingExecutorModelRef = undefined;
118
119
  if (executorEffortRef) {
120
+ // SAFETY: executorEffortRef is operator-configured and trusted to name a ThinkingLevel.
119
121
  runtime.pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
120
122
  }
121
123
  if (!runtime.flowEnabled()) {
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import {
3
4
  alwaysOnRef,
4
5
  executorRef,
@@ -4,11 +4,13 @@ import {
4
4
  isSimpleMode,
5
5
  } from "../config/state.ts";
6
6
  import type { GitContextLevel } from "../git.ts";
7
+ import { advisorModelAccessReason } from "../tools/model-access.ts";
7
8
  import { resolveAdvisorRequest } from "../tools/render-common.ts";
8
9
  import { ManualAdvisorDialog } from "../ui/manual-dialog.ts";
9
10
  import type { ManualAdvisorRequest } from "../ui/types.ts";
10
11
  import { loadCommandConfig } from "./activation-preparation.ts";
11
12
  import { startManualConsultation } from "./manual-consultation.ts";
13
+ import { notify } from "./runtime.ts";
12
14
  import type { CommandRuntime, ManualAdvisorProgressState } from "./types.ts";
13
15
 
14
16
  export const registerManualCommand = (runtime: CommandRuntime) => {
@@ -19,6 +21,11 @@ export const registerManualCommand = (runtime: CommandRuntime) => {
19
21
  if (!loadCommandConfig(ctx)) {
20
22
  return;
21
23
  }
24
+ const accessReason = advisorModelAccessReason(ctx);
25
+ if (accessReason) {
26
+ notify(ctx, accessReason, "warning");
27
+ return;
28
+ }
22
29
  if (
23
30
  !(
24
31
  isSimpleMode() ||
@@ -96,7 +103,6 @@ export const registerManualCommand = (runtime: CommandRuntime) => {
96
103
  // Intentional fire-and-forget: the consultation streams after the command
97
104
  // handler returns. void satisfies noFloatingPromises; noVoid is ignored here
98
105
  // because this Biome version offers no ignoreVoidAsExpression option.
99
- // biome-ignore lint/complexity/noVoid: marks an intentional fire-and-forget promise
100
106
  void startManualConsultation(
101
107
  runtime,
102
108
  ctx,
@@ -1,14 +1,25 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { advisorRef } from "../config/state.ts";
4
+ import { isString } from "../content-utils.ts";
3
5
  import type { GitContextLevel } from "../git.ts";
4
6
  import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.ts";
7
+ import { normalizeScreeningQuestion } from "../tools/jev-filter.ts";
5
8
  import { appendScoutLifecycleEntry } from "../tools/scout-status.ts";
6
9
  import type { ScoutToolDetails } from "../tools/types.ts";
7
10
  import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.ts";
11
+ import type { AdvisorUsageSnapshot } from "../usage.ts";
8
12
  import { notify } from "./runtime.ts";
9
13
  import type { CommandRuntime, ManualAdvisorProgressState } from "./types.ts";
10
14
 
11
- export const startManualConsultation = (
15
+ interface ManualResultDetails {
16
+ advisor: string;
17
+ question?: string;
18
+ text: string;
19
+ usage?: AdvisorUsageSnapshot;
20
+ }
21
+
22
+ export const startManualConsultation = async (
12
23
  runtime: CommandRuntime,
13
24
  ctx: ExtensionContext,
14
25
  question: string | undefined,
@@ -34,8 +45,8 @@ export const startManualConsultation = (
34
45
  runtime.manualProgressTimers.set(controller, timer);
35
46
  }
36
47
  let scoutDetails: ScoutToolDetails | undefined;
37
- return runtime
38
- .requestAdvisor(
48
+ try {
49
+ const { adviceId, markdown, usage } = await runtime.requestAdvisor(
39
50
  ctx,
40
51
  question,
41
52
  controller.signal,
@@ -62,83 +73,96 @@ export const startManualConsultation = (
62
73
  }
63
74
  },
64
75
  gitContext
65
- )
66
- .then(({ markdown, usage }) => {
67
- if (controller.signal.aborted) {
68
- return;
69
- }
70
- progress.phase = "complete";
71
- runtime.advisorSessionState.recordInvocation({
72
- cost: advisorUsageCost(usage),
73
- executionEffect: "continued",
74
- kind: "markdown",
75
- model: advisorRef,
76
- trigger: "manual",
77
- usage,
78
- });
79
- runtime.updateAdvisorUsageStatus(ctx);
80
- const normalizedUsage = snapshotAdvisorUsage(usage);
81
- runtime.pi.sendMessage(
82
- {
83
- content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
84
- customType: "advisor-manual-result",
85
- details: {
86
- advisor: advisorRef,
87
- question,
88
- text: markdown,
89
- ...(normalizedUsage ? { usage: normalizedUsage } : {}),
90
- },
91
- display: true,
92
- },
93
- {
94
- // Steer lets the current turn finish its active work; the Executor sees
95
- // the result before its next model call rather than being interrupted.
96
- deliverAs: "steer",
97
- triggerTurn: true,
98
- }
99
- );
100
- })
101
- .catch((error: unknown) => {
102
- if (controller.signal.aborted) {
103
- return;
104
- }
105
- progress.phase = "error";
106
- const message = error instanceof Error ? error.message : String(error);
107
- runtime.advisorSessionState.recordInvocation({
108
- executionEffect: "continued",
109
- failure: "provider-error",
110
- kind: "markdown",
111
- model: advisorRef,
112
- trigger: "manual",
113
- });
114
- runtime.updateAdvisorUsageStatus(ctx);
115
- runtime.pi.sendMessage(
116
- {
117
- content: `Manual Advisor consultation failed: ${message}`,
118
- customType: "advisor-manual-result",
119
- details: {
120
- advisor: advisorRef,
121
- text: `**Advisor consultation failed:** ${message}`,
122
- },
123
- display: true,
124
- },
125
- { deliverAs: "steer", triggerTurn: true }
76
+ );
77
+ if (controller.signal.aborted) {
78
+ return;
79
+ }
80
+ progress.phase = "complete";
81
+ runtime.advisorSessionState.resetTurnsSinceConsultation();
82
+ runtime.advisorSessionState.recordInvocation({
83
+ cost: advisorUsageCost(usage),
84
+ executionEffect: "continued",
85
+ kind: "markdown",
86
+ model: advisorRef,
87
+ trigger: "manual",
88
+ usage,
89
+ });
90
+ if (isString(adviceId)) {
91
+ // Registering the manual question lets an identical later ask_advisor
92
+ // hit repeat detection instead of re-consulting.
93
+ runtime.advisorSessionState.issueAdvice(
94
+ adviceId,
95
+ markdown,
96
+ "manual",
97
+ false,
98
+ normalizeScreeningQuestion(question)
126
99
  );
127
- notify(ctx, `Advisor consultation failed: ${message}`, "error");
128
- notifyHerdrAdvisorFailure("Advisor consultation failed", message);
129
- })
130
- .finally(() => {
131
- if (controller.signal.aborted) {
132
- progress.phase = "cancelled";
133
- }
134
- const timer = runtime.manualProgressTimers.get(controller);
135
- if (timer) {
136
- clearInterval(timer);
137
- runtime.manualProgressTimers.delete(controller);
100
+ }
101
+ runtime.updateAdvisorUsageStatus(ctx);
102
+ const details: ManualResultDetails = {
103
+ advisor: advisorRef,
104
+ question,
105
+ text: markdown,
106
+ };
107
+ const normalizedUsage = snapshotAdvisorUsage(usage);
108
+ if (normalizedUsage) {
109
+ details.usage = normalizedUsage;
110
+ }
111
+ runtime.pi.sendMessage(
112
+ {
113
+ content: `Manual Advisor consultation${question ? ` (${question})` : ""} — for your awareness; no action or follow-up consultation is needed unless the user asks:\n\n${markdown}`,
114
+ customType: "advisor-manual-result",
115
+ details,
116
+ display: true,
117
+ },
118
+ {
119
+ // Steer lets the current turn finish its active work; the Executor sees
120
+ // the result before its next model call rather than being interrupted.
121
+ deliverAs: "steer",
122
+ triggerTurn: true,
138
123
  }
139
- runtime.requestManualRender(ctx);
140
- runtime.scoutStatus.release(ctx, scoutStatusToken);
141
- runtime.manualConsultations.delete(controller);
142
- herdrAdvisorActivity.finish();
124
+ );
125
+ } catch (error) {
126
+ if (controller.signal.aborted) {
127
+ return;
128
+ }
129
+ progress.phase = "error";
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ runtime.advisorSessionState.resetTurnsSinceConsultation();
132
+ runtime.advisorSessionState.recordInvocation({
133
+ executionEffect: "continued",
134
+ failure: "provider-error",
135
+ kind: "markdown",
136
+ model: advisorRef,
137
+ trigger: "manual",
143
138
  });
139
+ runtime.updateAdvisorUsageStatus(ctx);
140
+ runtime.pi.sendMessage(
141
+ {
142
+ content: `Manual Advisor consultation failed: ${message}`,
143
+ customType: "advisor-manual-result",
144
+ details: {
145
+ advisor: advisorRef,
146
+ text: `**Advisor consultation failed:** ${message}`,
147
+ },
148
+ display: true,
149
+ },
150
+ { deliverAs: "steer", triggerTurn: true }
151
+ );
152
+ notify(ctx, `Advisor consultation failed: ${message}`, "error");
153
+ notifyHerdrAdvisorFailure("Advisor consultation failed", message);
154
+ } finally {
155
+ if (controller.signal.aborted) {
156
+ progress.phase = "cancelled";
157
+ }
158
+ const timer = runtime.manualProgressTimers.get(controller);
159
+ if (timer) {
160
+ clearInterval(timer);
161
+ runtime.manualProgressTimers.delete(controller);
162
+ }
163
+ runtime.requestManualRender(ctx);
164
+ runtime.scoutStatus.release(ctx, scoutStatusToken);
165
+ runtime.manualConsultations.delete(controller);
166
+ herdrAdvisorActivity.finish();
167
+ }
144
168
  };
@@ -1,7 +1,11 @@
1
- import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, Markdown, Text } from "@earendil-works/pi-tui";
1
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Markdown, Text } from "@earendil-works/pi-tui";
4
+ import type { Component } from "@earendil-works/pi-tui";
5
+
3
6
  import {
4
7
  adviceForDisplay,
8
+ noop,
5
9
  renderAdvisorCallBox,
6
10
  renderThinkingMarkdown,
7
11
  SPINNER_FRAMES,
@@ -87,7 +91,5 @@ export class ManualAdvisorProgressComponent implements Component {
87
91
  return box.render(width);
88
92
  }
89
93
 
90
- invalidate(): void {
91
- // The live progress state is read during each render.
92
- }
94
+ readonly invalidate = noop;
93
95
  }
@@ -10,8 +10,8 @@ import {
10
10
  setExecutorRef,
11
11
  } from "../config/state.ts";
12
12
  import { saveConfig } from "../config/storage.ts";
13
- import { activateAdvisor } from "./activation.ts";
14
13
  import { loadCommandConfig } from "./activation-preparation.ts";
14
+ import { activateAdvisor } from "./activation.ts";
15
15
  import { selectAdvisorModels } from "./model-picker.ts";
16
16
  import type { CommandRuntime } from "./types.ts";
17
17
 
@@ -1,5 +1,7 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { type getPersistedModelRefs, splitRef } from "../config/state.ts";
2
+
3
+ import { splitRef } from "../config/state.ts";
4
+ import type { getPersistedModelRefs } from "../config/state.ts";
3
5
  import type { ContextPreset } from "../ui/types.ts";
4
6
 
5
7
  export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
@@ -33,7 +35,7 @@ export const selectedEffort = (choice: string): string | undefined => {
33
35
  export const ADVISOR_ACTIVATION_EXPLANATION =
34
36
  "The Advisor is a second-opinion model that reviews the Executor's context and returns risks, alternatives, and verification steps without changing files or running tools.";
35
37
 
36
- const ARGUMENT_WHITESPACE = /\s+/;
38
+ const ARGUMENT_WHITESPACE = /\s+/u;
37
39
  const hasModelOverride = (args: string, key: "advisor" | "executor") =>
38
40
  args
39
41
  .trim()
@@ -91,13 +93,14 @@ export const findConfiguredModel = (
91
93
  return;
92
94
  }
93
95
  const [provider, modelId] = splitRef(ref);
96
+ // oxlint-disable-next-line unicorn/no-array-method-this-argument -- Pi ModelRegistry.find(provider, id), not Array#find.
94
97
  return ctx.modelRegistry.find(provider, modelId);
95
98
  };
96
99
 
97
100
  export const getAvailableModelRefs = (
98
101
  ctx: ExtensionContext
99
102
  ): string[] | undefined => {
100
- if (typeof ctx.modelRegistry.getAvailable !== "function") {
103
+ if (!ctx.modelRegistry.getAvailable) {
101
104
  return undefined;
102
105
  }
103
106
  return ctx.modelRegistry
@@ -105,6 +108,14 @@ export const getAvailableModelRefs = (
105
108
  .map((model) => `${model.provider}/${model.id}`);
106
109
  };
107
110
 
111
+ export const getConfiguredModelRefs = (ctx: ExtensionContext): string[] => {
112
+ const registry = ctx.modelRegistry;
113
+ const models = registry?.getAvailable ? registry.getAvailable() : [];
114
+ return [
115
+ ...new Set(models.map((model) => `${model.provider}/${model.id}`)),
116
+ ].toSorted((left, right) => left.localeCompare(right));
117
+ };
118
+
108
119
  const isSelectableModel = (
109
120
  ctx: ExtensionContext,
110
121
  ref: string | undefined,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { SearchableModelSelector } from "../ui/model-selector.ts";
3
4
  import {
4
5
  effortChoices,
@@ -2,6 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+
5
6
  import { activateAdvisor } from "./activation.ts";
6
7
  import { registerCommandLifecycle } from "./lifecycle.ts";
7
8
  import { registerManualCommand } from "./manual-command.ts";