@mrclrchtr/supi-review 2.6.1 → 2.7.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "SuPi Review extension — session-aware review command and agent tools",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@mrclrchtr/supi-core": "2.6.1"
34
+ "@mrclrchtr/supi-core": "2.7.0"
35
35
  },
36
36
  "bundledDependencies": [
37
37
  "@mrclrchtr/supi-core"
package/src/review.ts CHANGED
@@ -7,6 +7,7 @@ import { synthesizeReviewBrief } from "./history/synthesize.ts";
7
7
  import { normalizeReviewResult } from "./review-result.ts";
8
8
  import { buildReviewPacket } from "./target/packet.ts";
9
9
  import { registerAgentReviewTools } from "./tool/agent-review-tools.ts";
10
+ import { createUnobservedChildFailureDiagnostics } from "./tool/child-failure-diagnostics.ts";
10
11
  import { runReviewer } from "./tool/review-runner.ts";
11
12
  import type {
12
13
  BriefSynthesisRunResult,
@@ -17,7 +18,11 @@ import type {
17
18
  ReviewTargetSpec,
18
19
  } from "./types.ts";
19
20
  import { collectReviewNote, previewReviewPlan, selectModel, selectTarget } from "./ui/flow.ts";
20
- import { formatReviewContent } from "./ui/format-content.ts";
21
+ import {
22
+ formatBriefSynthesisFailureContent,
23
+ formatBriefSynthesisFailureCopy,
24
+ formatReviewContent,
25
+ } from "./ui/format-content.ts";
21
26
  import { registerReviewRenderer } from "./ui/renderer.ts";
22
27
 
23
28
  type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
@@ -74,22 +79,21 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
74
79
  );
75
80
 
76
81
  if (!synthesis) {
77
- notifyBriefDone(
78
- pi,
79
- {
80
- kind: "failed",
81
- reason: "Brief synthesis encountered an unexpected error",
82
- } as BriefSynthesisRunResult,
83
- snapshot,
84
- model.canonicalId,
85
- );
86
- ctx.ui.notify("Brief synthesis was canceled or failed", "warning");
82
+ const failure: BriefSynthesisRunResult = {
83
+ kind: "failed",
84
+ failureCode: "unexpected-runner-failure",
85
+ diagnostics: createUnobservedChildFailureDiagnostics(),
86
+ };
87
+ notifyBriefDone(pi, failure, snapshot, model.canonicalId);
88
+ injectBriefSynthesisFailureMessage(pi, failure, snapshot, model.canonicalId);
89
+ ctx.ui.notify(formatBriefSynthesisFailureCopy(failure), "warning");
87
90
  return;
88
91
  }
89
92
 
90
93
  notifyBriefDone(pi, synthesis, snapshot, model.canonicalId);
91
94
 
92
95
  if (synthesis.kind !== "success") {
96
+ injectBriefSynthesisFailureMessage(pi, synthesis, snapshot, model.canonicalId);
93
97
  notifySynthesisFailure(synthesis, ctx);
94
98
  return;
95
99
  }
@@ -122,14 +126,17 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
122
126
  );
123
127
 
124
128
  if (!rawResult) {
125
- notifyReviewDone(pi, {
129
+ const failure: ReviewResult = {
126
130
  kind: "failed",
127
- reason: "Review encountered an unexpected error",
131
+ failureCode: "unexpected-runner-failure",
132
+ diagnostics: createUnobservedChildFailureDiagnostics(),
128
133
  snapshot,
129
134
  brief,
130
135
  modelId: model.canonicalId,
131
- } as ReviewResult);
132
- ctx.ui.notify("Review was canceled or failed", "warning");
136
+ };
137
+ notifyReviewDone(pi, failure);
138
+ injectReviewMessage(pi, failure);
139
+ ctx.ui.notify("Reviewer ended unexpectedly.", "warning");
133
140
  return;
134
141
  }
135
142
 
@@ -172,20 +179,27 @@ function notifySynthesisFailure(
172
179
  result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
173
180
  ctx: CommandContext,
174
181
  ): void {
175
- switch (result.kind) {
176
- case "failed":
177
- ctx.ui.notify(result.reason, "error");
178
- break;
179
- case "timeout":
180
- ctx.ui.notify(
181
- `Brief synthesis timed out after ${(result.timeoutMs / 1000).toFixed(0)}s`,
182
- "warning",
183
- );
184
- break;
185
- case "canceled":
186
- ctx.ui.notify("Review canceled", "warning");
187
- break;
188
- }
182
+ ctx.ui.notify(
183
+ formatBriefSynthesisFailureCopy(result),
184
+ result.kind === "failed" ? "error" : "warning",
185
+ );
186
+ }
187
+
188
+ /** Persist a brief-synthesis failure so bounded diagnostics remain inspectable in parent history. */
189
+ function injectBriefSynthesisFailureMessage(
190
+ pi: ExtensionAPI,
191
+ result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
192
+ snapshot: ReviewSnapshot,
193
+ modelId: string,
194
+ ): void {
195
+ pi.sendMessage({
196
+ customType: "supi-review",
197
+ content: formatBriefSynthesisFailureContent(result),
198
+ display: true,
199
+ details: {
200
+ briefSynthesisFailure: { result, snapshot, modelId },
201
+ },
202
+ });
189
203
  }
190
204
 
191
205
  /** Ring the terminal bell so the user knows to check back. */
@@ -20,6 +20,7 @@ import type {
20
20
  ReviewProgress,
21
21
  ReviewTargetSpec,
22
22
  } from "../types.ts";
23
+ import { formatBriefSynthesisFailureCopy } from "../ui/format-content.ts";
23
24
  import { formatAgentReviewBatch, formatPreparedAgentReview } from "../ui/review-tool-format.ts";
24
25
  import {
25
26
  type AgentReviewProgressDetails,
@@ -35,6 +36,7 @@ import {
35
36
  runAgentReviewSchema,
36
37
  } from "./agent-review-schemas.ts";
37
38
  import { prepareAgentReviewPlan, runAgentReviewBatch } from "./agent-review-workflow.ts";
39
+ import { formatChildLifecycleTrace } from "./child-lifecycle-trace.ts";
38
40
  import {
39
41
  PREPARE_REVIEW_TOOL_NAME,
40
42
  prepareReviewPromptGuidelines,
@@ -228,14 +230,12 @@ function formatPreparationFailure(
228
230
  outcome: Exclude<Awaited<ReturnType<typeof prepareAgentReviewPlan>>, { kind: "prepared" }>,
229
231
  ): string {
230
232
  if (outcome.kind === "no-snapshot") return outcome.reason;
231
- switch (outcome.result.kind) {
232
- case "failed":
233
- return `Brief synthesis failed: ${outcome.result.reason}`;
234
- case "canceled":
235
- return "Brief synthesis was canceled.";
236
- case "timeout":
237
- return `Brief synthesis timed out after ${(outcome.result.timeoutMs / 1_000).toFixed(0)}s.`;
238
- }
233
+
234
+ const { result } = outcome;
235
+ const trace = result.diagnostics?.lifecycleTrace;
236
+ return trace
237
+ ? `${formatBriefSynthesisFailureCopy(result)}\n\n${formatChildLifecycleTrace(trace)}`
238
+ : formatBriefSynthesisFailureCopy(result);
239
239
  }
240
240
 
241
241
  function activateRunTool(pi: ExtensionAPI): void {
@@ -22,8 +22,8 @@ import type {
22
22
  ReviewTargetSpec,
23
23
  SynthesizedReviewBrief,
24
24
  } from "../types.ts";
25
+ import { createUnobservedChildFailureDiagnostics } from "./child-failure-diagnostics.ts";
25
26
  import { runReviewer } from "./review-runner.ts";
26
-
27
27
  /** Inputs required to synthesize and retain one agent-driven review plan. */
28
28
  export interface PrepareAgentReviewWorkflowInput {
29
29
  cwd: string;
@@ -35,13 +35,11 @@ export interface PrepareAgentReviewWorkflowInput {
35
35
  onProgress?: (progress: ReviewProgress) => void;
36
36
  planStore: ReviewPlanStore;
37
37
  }
38
-
39
38
  /** Typed preparation outcome before any reviewer child session starts. */
40
39
  export type PrepareAgentReviewWorkflowOutcome =
41
40
  | { kind: "prepared"; plan: StoredAgentReviewPlan }
42
41
  | { kind: "no-snapshot"; reason: string }
43
42
  | { kind: "synthesis-failed"; result: Exclude<BriefSynthesisRunResult, { kind: "success" }> };
44
-
45
43
  /** Inputs required to critique and execute one prepared review plan. */
46
44
  export interface RunAgentReviewWorkflowInput {
47
45
  cwd: string;
@@ -55,13 +53,11 @@ export interface RunAgentReviewWorkflowInput {
55
53
  onReviewerDone?: (reviewerId: string) => void;
56
54
  planStore: ReviewPlanStore;
57
55
  }
58
-
59
56
  /** Typed batch outcome after validation and snapshot freshness checks. */
60
57
  export type RunAgentReviewWorkflowOutcome =
61
58
  | { kind: "completed"; details: AgentReviewBatchDetails }
62
59
  | { kind: "invalid"; reason: string }
63
60
  | { kind: "stale"; reason: string };
64
-
65
61
  /** Prepare one session-scoped review plan without starting reviewer sessions. */
66
62
  export async function prepareAgentReviewPlan(
67
63
  input: PrepareAgentReviewWorkflowInput,
@@ -74,15 +70,27 @@ export async function prepareAgentReviewPlan(
74
70
  };
75
71
  }
76
72
 
77
- const synthesis = await synthesizeReviewBrief({
78
- model: input.model,
79
- cwd: input.cwd,
80
- snapshot,
81
- serializedContext: input.serializedContext,
82
- note: input.note,
83
- signal: input.signal,
84
- onProgress: input.onProgress,
85
- });
73
+ let synthesis: BriefSynthesisRunResult;
74
+ try {
75
+ synthesis = await synthesizeReviewBrief({
76
+ model: input.model,
77
+ cwd: input.cwd,
78
+ snapshot,
79
+ serializedContext: input.serializedContext,
80
+ note: input.note,
81
+ signal: input.signal,
82
+ onProgress: input.onProgress,
83
+ });
84
+ } catch {
85
+ return {
86
+ kind: "synthesis-failed",
87
+ result: {
88
+ kind: "failed",
89
+ failureCode: "unexpected-runner-failure",
90
+ diagnostics: createUnobservedChildFailureDiagnostics(),
91
+ },
92
+ };
93
+ }
86
94
  if (synthesis.kind !== "success") {
87
95
  return { kind: "synthesis-failed", result: synthesis };
88
96
  }
@@ -195,13 +203,14 @@ async function runAssignment(
195
203
  onProgress: (progress) => input.onReviewerProgress?.(assignment.id, progress),
196
204
  });
197
205
  return { assignment, result: compactReviewResult(normalizeReviewResult(rawResult)) };
198
- } catch (error) {
206
+ } catch {
199
207
  return {
200
208
  assignment,
201
209
  result: {
202
210
  kind: "failed",
203
- reason: `Reviewer session failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`,
211
+ failureCode: "unexpected-runner-failure",
204
212
  modelId: plan.model.canonicalId,
213
+ diagnostics: createUnobservedChildFailureDiagnostics(),
205
214
  },
206
215
  };
207
216
  }
@@ -212,21 +221,30 @@ function compactReviewResult(result: ReviewResult): AgentReviewerResult {
212
221
  case "success":
213
222
  return { kind: "success", output: result.output, modelId: result.modelId };
214
223
  case "failed":
224
+ return result.failureCode === "session-creation-failed"
225
+ ? {
226
+ kind: "failed",
227
+ failureCode: result.failureCode,
228
+ modelId: result.modelId,
229
+ }
230
+ : {
231
+ kind: "failed",
232
+ failureCode: result.failureCode,
233
+ modelId: result.modelId,
234
+ diagnostics: result.diagnostics,
235
+ };
236
+ case "canceled":
215
237
  return {
216
- kind: "failed",
217
- reason: result.reason,
238
+ kind: "canceled",
218
239
  modelId: result.modelId,
219
- debug: result.debug,
240
+ diagnostics: result.diagnostics,
220
241
  };
221
- case "canceled":
222
- return { kind: "canceled", modelId: result.modelId, debug: result.debug };
223
242
  case "timeout":
224
243
  return {
225
244
  kind: "timeout",
226
245
  timeoutMs: result.timeoutMs,
227
- partialOutput: result.partialOutput,
228
246
  modelId: result.modelId,
229
- debug: result.debug,
247
+ diagnostics: result.diagnostics,
230
248
  };
231
249
  }
232
250
  }
@@ -11,7 +11,8 @@ import type {
11
11
  BriefSynthesisRunResult,
12
12
  SynthesizedReviewBrief,
13
13
  } from "../types.ts";
14
- import { buildProgressTokens, extractLastAssistantText } from "./runner-helpers.ts";
14
+ import { createEarlyCancellationDiagnostics } from "./child-failure-diagnostics.ts";
15
+ import { buildProgressTokens } from "./runner-helpers.ts";
15
16
  import { reviewBriefSchema } from "./schemas.ts";
16
17
  import { type LifecycleCtx, runWithLifecycle } from "./session-lifecycle.ts";
17
18
 
@@ -88,24 +89,19 @@ function emitBriefProgress(
88
89
 
89
90
  /** Finalize after retries and compaction recovery, never at the earlier `agent_end` boundary. */
90
91
  function handleAgentSettled(options: {
91
- session: AgentSession;
92
92
  brief: SynthesizedReviewBrief | undefined;
93
93
  state: { settled: boolean; aborting: boolean };
94
94
  cleanup: (result: BriefSynthesisRunResult) => BriefSynthesisRunResult;
95
+ getFailureDiagnostics: LifecycleCtx<BriefSynthesisRunResult>["getFailureDiagnostics"];
95
96
  }): BriefSynthesisRunResult | undefined {
96
- const { session, brief, state, cleanup } = options;
97
- if (state.settled || state.aborting) {
98
- return undefined;
99
- }
100
- if (brief) {
101
- return cleanup({ kind: "success", brief });
102
- }
103
- const lastText = extractLastAssistantText(session.messages);
97
+ const { brief, state, cleanup, getFailureDiagnostics } = options;
98
+ if (state.settled || state.aborting) return undefined;
99
+ if (brief) return cleanup({ kind: "success", brief });
100
+
104
101
  return cleanup({
105
102
  kind: "failed",
106
- reason: lastText
107
- ? `Brief synthesizer did not call submit_review_brief. Assistant said: ${lastText}`
108
- : "Brief synthesizer did not produce any output.",
103
+ failureCode: "missing-structured-output",
104
+ diagnostics: getFailureDiagnostics(),
109
105
  });
110
106
  }
111
107
 
@@ -114,7 +110,7 @@ export async function runBriefSynthesis(
114
110
  invocation: BriefSynthesisInvocation,
115
111
  ): Promise<BriefSynthesisRunResult> {
116
112
  if (invocation.signal?.aborted) {
117
- return { kind: "canceled" };
113
+ return { kind: "canceled", diagnostics: createEarlyCancellationDiagnostics() };
118
114
  }
119
115
 
120
116
  const resultHolder: { value: SynthesizedReviewBrief | undefined } = { value: undefined };
@@ -123,11 +119,8 @@ export async function runBriefSynthesis(
123
119
  let session: AgentSession;
124
120
  try {
125
121
  session = await createBriefSession(invocation, submitBriefTool);
126
- } catch (error) {
127
- return {
128
- kind: "failed",
129
- reason: `Failed to create brief synthesis session: ${error instanceof Error ? error.message : String(error)}`,
130
- };
122
+ } catch {
123
+ return { kind: "failed", failureCode: "session-creation-failed" };
131
124
  }
132
125
 
133
126
  return runWithLifecycle<BriefSynthesisRunResult>({
@@ -155,10 +148,10 @@ export async function runBriefSynthesis(
155
148
  break;
156
149
  case "agent_settled": {
157
150
  const result = handleAgentSettled({
158
- session,
159
151
  brief: resultHolder.value,
160
152
  state: ctx.state,
161
153
  cleanup: ctx.cleanup,
154
+ getFailureDiagnostics: ctx.getFailureDiagnostics,
162
155
  });
163
156
  if (result) {
164
157
  ctx.resolve(result);
@@ -169,11 +162,19 @@ export async function runBriefSynthesis(
169
162
  break;
170
163
  }
171
164
  },
172
- canceledResult: () => ({ kind: "canceled" as const }),
173
- failedResult: (reason) => ({
165
+ canceledResult: (ctx) => ({
166
+ kind: "canceled" as const,
167
+ diagnostics: ctx.getFailureDiagnostics(),
168
+ }),
169
+ failedResult: (failureCode, ctx) => ({
174
170
  kind: "failed" as const,
175
- reason,
171
+ failureCode,
172
+ diagnostics: ctx.getFailureDiagnostics(),
173
+ }),
174
+ timeoutResult: (ms, ctx) => ({
175
+ kind: "timeout" as const,
176
+ timeoutMs: ms,
177
+ diagnostics: ctx.getFailureDiagnostics(),
176
178
  }),
177
- timeoutResult: (ms) => ({ kind: "timeout" as const, timeoutMs: ms }),
178
179
  });
179
180
  }
@@ -0,0 +1,145 @@
1
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ ChildFailureCode,
4
+ ChildFailureDiagnostics,
5
+ ChildStage,
6
+ ReviewProgress,
7
+ } from "../types.ts";
8
+ import {
9
+ type ChildLifecycleTrace,
10
+ ChildLifecycleTraceCollector,
11
+ formatChildLifecycleTrace,
12
+ getRegisteredToolNames,
13
+ toSafeAssistantStopReason,
14
+ } from "./child-lifecycle-trace.ts";
15
+ import { buildProgressTokens } from "./runner-helpers.ts";
16
+
17
+ /** Inputs used to create one safe non-success child-run diagnostic artifact. */
18
+ export interface BuildChildFailureDiagnosticsInput {
19
+ progress: ReviewProgress;
20
+ lifecycleTrace: ChildLifecycleTrace;
21
+ recentActivity: string[];
22
+ session?: AgentSession;
23
+ }
24
+
25
+ /** Build diagnostics from allowlisted control metadata only. */
26
+ export function buildChildFailureDiagnostics(
27
+ input: BuildChildFailureDiagnosticsInput,
28
+ ): ChildFailureDiagnostics {
29
+ const { session } = input;
30
+ const tokens = session
31
+ ? buildProgressTokens(() => session.getSessionStats())
32
+ : input.progress.tokens;
33
+ const lastAssistant = session ? extractLastAssistantMetadata(session) : undefined;
34
+
35
+ return {
36
+ lifecycleTrace: input.lifecycleTrace,
37
+ turns: input.progress.turns,
38
+ toolUses: input.progress.toolUses,
39
+ tokens,
40
+ recentActivity: input.recentActivity.length > 0 ? [...input.recentActivity] : undefined,
41
+ lastAssistantStopReason: lastAssistant?.stopReason,
42
+ lastAssistantToolCalls: lastAssistant?.toolCalls,
43
+ };
44
+ }
45
+
46
+ /** Create safe diagnostics when a host failure occurred without observed child lifecycle events. */
47
+ export function createUnobservedChildFailureDiagnostics(): ChildFailureDiagnostics {
48
+ return buildChildFailureDiagnostics({
49
+ progress: { turns: 0, toolUses: 0 },
50
+ lifecycleTrace: { entries: [], droppedCount: 0 },
51
+ recentActivity: [],
52
+ });
53
+ }
54
+
55
+ /** Create safe cancellation diagnostics when no child session was started. */
56
+ export function createEarlyCancellationDiagnostics(): ChildFailureDiagnostics {
57
+ const collector = new ChildLifecycleTraceCollector();
58
+ collector.recordHostMarker({ type: "abort_requested", reason: "canceled" });
59
+ return buildChildFailureDiagnostics({
60
+ progress: { turns: 0, toolUses: 0 },
61
+ lifecycleTrace: collector.snapshot(),
62
+ recentActivity: collector.recentActivitySnapshot(),
63
+ });
64
+ }
65
+
66
+ /** Generate static parent-facing copy for a host-owned child failure code. */
67
+ export function formatChildFailureCopy(stage: ChildStage, code: ChildFailureCode): string {
68
+ const label = stage === "brief-synthesis" ? "Brief synthesis" : "Reviewer";
69
+ switch (code) {
70
+ case "session-creation-failed":
71
+ return `${label} session could not be created.`;
72
+ case "prompt-rejected":
73
+ return `${label} prompt was rejected before it ran.`;
74
+ case "missing-structured-output":
75
+ return `${label} ended without the required structured output.`;
76
+ case "unexpected-runner-failure":
77
+ return `${label} ended unexpectedly.`;
78
+ }
79
+ }
80
+
81
+ /** Format the complete retained safe diagnostic artifact for parent-facing text. */
82
+ export function formatChildFailureDiagnostics(diagnostics: ChildFailureDiagnostics): string[] {
83
+ const lines = [
84
+ `- Turns: ${diagnostics.turns}`,
85
+ `- Tool uses: ${diagnostics.toolUses}`,
86
+ diagnostics.tokens
87
+ ? `- Tokens: ${diagnostics.tokens.input} in / ${diagnostics.tokens.output} out / ${diagnostics.tokens.total} total`
88
+ : undefined,
89
+ diagnostics.recentActivity && diagnostics.recentActivity.length > 0
90
+ ? `- Recent activity: ${diagnostics.recentActivity.join(" → ")}`
91
+ : undefined,
92
+ diagnostics.lastAssistantStopReason
93
+ ? `- Last assistant stop: ${diagnostics.lastAssistantStopReason}`
94
+ : undefined,
95
+ diagnostics.lastAssistantToolCalls && diagnostics.lastAssistantToolCalls.length > 0
96
+ ? `- Last assistant tools: ${diagnostics.lastAssistantToolCalls.join(", ")}`
97
+ : undefined,
98
+ `- ${formatChildLifecycleTrace(diagnostics.lifecycleTrace)}`,
99
+ ];
100
+ return lines.filter((line): line is string => !!line);
101
+ }
102
+
103
+ interface LastAssistantMetadata {
104
+ stopReason?: string;
105
+ toolCalls?: string[];
106
+ }
107
+
108
+ function extractLastAssistantMetadata(session: AgentSession): LastAssistantMetadata | undefined {
109
+ try {
110
+ const messages = session.messages as unknown;
111
+ if (!Array.isArray(messages)) return undefined;
112
+
113
+ const registeredToolNames = getRegisteredToolNames(session);
114
+ for (let index = messages.length - 1; index >= 0; index--) {
115
+ const message = messages[index] as Record<string, unknown> | undefined;
116
+ if (message?.role !== "assistant") continue;
117
+
118
+ const stopReason = toSafeAssistantStopReason(message.stopReason);
119
+ const toolCalls = extractAssistantToolCalls(message.content, registeredToolNames);
120
+ return {
121
+ stopReason,
122
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
123
+ };
124
+ }
125
+ return undefined;
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ function extractAssistantToolCalls(content: unknown, registeredToolNames: Set<string>): string[] {
132
+ if (!Array.isArray(content)) return [];
133
+
134
+ return content
135
+ .map((part) => {
136
+ if (typeof part !== "object" || !part) return undefined;
137
+ const tool = part as { type?: unknown; name?: unknown };
138
+ return tool.type === "toolCall" &&
139
+ typeof tool.name === "string" &&
140
+ registeredToolNames.has(tool.name)
141
+ ? tool.name
142
+ : undefined;
143
+ })
144
+ .filter((name): name is string => !!name);
145
+ }