Repository navigation
feat: customizable tool render behavior in ai-core - #561
Conversation
Introduces a ToolRenderBehavior API on Chat.Root that lets host apps override how tool calls appear in FlatAgentRenderer: - isPassthroughTool: collapse wrapper tool calls so their children are promoted into the caller's segment list with no summary line - getToolDisplayName: override the label on agent summary lines - getActivityLabel: override the label on leaf activity log lines Also removes the hardcoded humanizeToolName helper (label generation is now the host app's responsibility) and fixes streamSubAgent so that pending or approval-requested tool calls are marked cancelled when the abort signal fires, instead of being left in a stuck state.
📝 WalkthroughWalkthroughIntroduces a ToolRenderBehavior context and provider to customize tool labeling and structure (passthrough flattening), exposes related types, extends Chat to accept a top-level Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/ai-core/src/agents/AgentUtils.ts (1)
444-455:⚠️ Potential issue | 🟠 MajorMake the approval wait abortable too.
The new final sweep at Lines 520-532 is not reached if
abortSignalfires while this Promise is awaiting user approval. That can still leave anapproval-requestedtool call and approval UI stuck until the user responds. Route this path through the same cancel/snapshot handling and clear the pending approval on abort.🐛 Proposed direction
+ const markPendingToolCallsCancelled = () => { + const now = Date.now(); + for (const [id, tc] of toolCallMap) { + if (tc.state === 'pending' || tc.state === 'approval-requested') { + toolCallMap.set(id, { + ...tc, + state: 'error', + errorText: TOOL_CALL_CANCELLED, + completedAt: now, + }); + } + } + }; + + const throwAbortWithSnapshot = (): never => { + markPendingToolCallsCancelled(); + const snapshot = buildAbortSnapshot( + parentToolCallId, + toolCallMap, + finalText, + store, + ); + store.getState().ai.writeAbortSnapshot?.(parentToolCallId, snapshot); + pushProgress(); + throw new ToolAbortError(TOOL_CALL_CANCELLED, snapshot); + }; + - const approved = await new Promise<boolean>((resolve) => { - throwIfAborted(); + const approved = await new Promise<boolean>((resolve, reject) => { + if (abortSignal?.aborted) { + reject(new ToolAbortError(TOOL_CALL_CANCELLED)); + return; + } + + const onAbort = () => reject(new ToolAbortError(TOOL_CALL_CANCELLED)); + abortSignal?.addEventListener('abort', onAbort, {once: true}); + store.getState().ai.requestSubAgentApproval!({ toolCallId: pendingApproval!.toolCallId, approvalId: pendingApproval!.approvalId, toolName: approvedEntry.toolName, input: approvedEntry.input, - resolve, + resolve: (approved) => { + abortSignal?.removeEventListener('abort', onAbort); + resolve(approved); + }, }); + }).catch((err) => { + if (abortSignal?.aborted) { + store.getState().ai.clearSubAgentApproval?.( + pendingApproval!.approvalId, + ); + throwAbortWithSnapshot(); + } + throw err; });Also applies to: 520-532
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai-core/src/agents/AgentUtils.ts` around lines 444 - 455, The approval-await block using new Promise<boolean> is not abortable: if abortSignal fires while waiting the pendingApproval and the `approval-requested` tool UI remain stuck. Make this wait listen to the same `abortSignal`/cancel + snapshot handling used later (the path around the code at 520-532) by registering an abort handler before calling `store.getState().ai.requestSubAgentApproval` that clears `pendingApproval` and invokes the same cancel/snapshot cleanup (or rejects/resolves the promise) and then removes the abort listener; ensure `throwIfAborted()` is still called before and after and that the handler clears the pending approval and resolves/rejects the awaiting Promise so the flow continues into the existing abort cleanup.
🧹 Nitpick comments (2)
packages/ai-core/src/components/Chat.tsx (1)
35-40: Optional: stabilize the defaulttoolRenderBehaviorvalue to avoid unnecessary consumer re-renders.Using
toolRenderBehavior = {}in the destructuring creates a fresh object on every render ofChat.Root, which causesToolRenderBehaviorProvider's value identity to change each render and forces all consumers (e.g.FlatAgentRenderer,ActivityLogLine,ParentSummaryLine,OrchestratorLogLineInner) to re-read context. Since these consumers also calluseMemo/selectors keyed onisPassthroughTool, a new identity will invalidatebuildFlatSegmentsmemoization on every render.Consider hoisting a shared constant or memoizing:
♻️ Proposed refactor
-const Root: FC<RootProps> = ({children, toolRenderBehavior = {}}) => ( - <ToolRenderBehaviorProvider value={toolRenderBehavior}> +const EMPTY_TOOL_RENDER_BEHAVIOR: ToolRenderBehavior = {}; + +const Root: FC<RootProps> = ({ + children, + toolRenderBehavior = EMPTY_TOOL_RENDER_BEHAVIOR, +}) => ( + <ToolRenderBehaviorProvider value={toolRenderBehavior}> <SessionChatManager /> {children} </ToolRenderBehaviorProvider> );Note this only fully helps if callers also pass a stable
toolRenderBehaviorreference (e.g. a module-level const or auseMemo-ed value); worth calling out in the JSDoc forChat.Root.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai-core/src/components/Chat.tsx` around lines 35 - 40, The default prop for toolRenderBehavior in the Root component creates a new object each render which changes the ToolRenderBehaviorProvider value identity and forces consumers to re-read context; fix this by replacing the inline default ({}) with a stable reference—either hoist a module-level constant (e.g., DEFAULT_TOOL_RENDER_BEHAVIOR) or compute a memoized value in Root (useMemo) and pass that to ToolRenderBehaviorProvider when toolRenderBehavior is not provided; update the JSDoc for Root to recommend callers pass a stable reference too so components like FlatAgentRenderer, ActivityLogLine, ParentSummaryLine, and OrchestratorLogLineInner won’t trigger unnecessary re-renders.packages/ai-core/src/components/FlatAgentRenderer.tsx (1)
667-684: Passthrough check on the synthesized parent tool call sees a stripped-downAgentToolCall.
parentToolCallis synthesized from only{toolCallId, toolName, input, state}, and then passed toisPassthroughTool(parentToolCall)at lines 682‑684 to decide whether to hide the top-level summary. If a host implementsisPassthroughToolby looking at fields other thantoolName/input(e.g.output,agentToolCalls,startedAt,errorText), the top-level check will behave differently from the recursive check insidebuildFlatSegments, which receives the realAgentToolCallobjects.Worth either documenting on
ToolStructureBehavior.isPassthroughToolthat the predicate should rely only on tool identity/input (so the callback stays pure with respect totoolName), or threading more of the real parent fields through here so both call sites see equivalent shapes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai-core/src/components/FlatAgentRenderer.tsx` around lines 667 - 684, The synthesized parentToolCall currently only contains {toolCallId, toolName, input, state} but isPassthroughTool(parentToolCall) is later used to decide hiding the summary, which can mismatch the recursive check in buildFlatSegments that receives full AgentToolCall objects; fix by passing the real/full AgentToolCall to isPassthroughTool (including fields like output, agentToolCalls, startedAt, errorText) instead of the stripped parentToolCall, or alternatively document/require that ToolStructureBehavior.isPassthroughTool must only use tool identity/input (toolName/input) so both call sites behave the same; update the call in FlatAgentRenderer (the parentToolCall creation/use and the isPassthroughTool invocation) to use the complete object or adjust the interface contract accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ai-core/src/components/FlatAgentRenderer.tsx`:
- Around line 42-55: The current label precedence in ActivityLogLine and
OrchestratorLogLineInner contradicts the JSDoc for
ToolDisplayBehavior.getActivityLabel because reasoning currently wins before the
host callback; change the expression in both ActivityLogLine and
OrchestratorLogLineInner so it prefers getActivityLabel(toolCall) first, then
falls back to reasoning, then to 'Thinking...' if isPending, then to
toolCall.toolName (i.e. label = (getActivityLabel ? getActivityLabel(toolCall) :
undefined) ?? reasoning ?? (isPending ? 'Thinking...' : toolCall.toolName));
update only these expressions so the runtime behavior matches the documented
precedence for getActivityLabel.
---
Outside diff comments:
In `@packages/ai-core/src/agents/AgentUtils.ts`:
- Around line 444-455: The approval-await block using new Promise<boolean> is
not abortable: if abortSignal fires while waiting the pendingApproval and the
`approval-requested` tool UI remain stuck. Make this wait listen to the same
`abortSignal`/cancel + snapshot handling used later (the path around the code at
520-532) by registering an abort handler before calling
`store.getState().ai.requestSubAgentApproval` that clears `pendingApproval` and
invokes the same cancel/snapshot cleanup (or rejects/resolves the promise) and
then removes the abort listener; ensure `throwIfAborted()` is still called
before and after and that the handler clears the pending approval and
resolves/rejects the awaiting Promise so the flow continues into the existing
abort cleanup.
---
Nitpick comments:
In `@packages/ai-core/src/components/Chat.tsx`:
- Around line 35-40: The default prop for toolRenderBehavior in the Root
component creates a new object each render which changes the
ToolRenderBehaviorProvider value identity and forces consumers to re-read
context; fix this by replacing the inline default ({}) with a stable
reference—either hoist a module-level constant (e.g.,
DEFAULT_TOOL_RENDER_BEHAVIOR) or compute a memoized value in Root (useMemo) and
pass that to ToolRenderBehaviorProvider when toolRenderBehavior is not provided;
update the JSDoc for Root to recommend callers pass a stable reference too so
components like FlatAgentRenderer, ActivityLogLine, ParentSummaryLine, and
OrchestratorLogLineInner won’t trigger unnecessary re-renders.
In `@packages/ai-core/src/components/FlatAgentRenderer.tsx`:
- Around line 667-684: The synthesized parentToolCall currently only contains
{toolCallId, toolName, input, state} but isPassthroughTool(parentToolCall) is
later used to decide hiding the summary, which can mismatch the recursive check
in buildFlatSegments that receives full AgentToolCall objects; fix by passing
the real/full AgentToolCall to isPassthroughTool (including fields like output,
agentToolCalls, startedAt, errorText) instead of the stripped parentToolCall, or
alternatively document/require that ToolStructureBehavior.isPassthroughTool must
only use tool identity/input (toolName/input) so both call sites behave the
same; update the call in FlatAgentRenderer (the parentToolCall creation/use and
the isPassthroughTool invocation) to use the complete object or adjust the
interface contract accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f8b1d1cd-1efa-47dc-8f58-773de9993ce2
📒 Files selected for processing (6)
packages/ai-core/src/agents/AgentUtils.tspackages/ai-core/src/components/Chat.tsxpackages/ai-core/src/components/FlatAgentRenderer.tsxpackages/ai-core/src/index.tspackages/ai-core/src/utils.tspackages/ai/src/index.ts
💤 Files with no reviewable changes (1)
- packages/ai-core/src/utils.ts
There was a problem hiding this comment.
Pull request overview
This PR adds a host-app customization surface for how tool calls are labeled/structured in ai-core’s FlatAgentRenderer, removes the now-unneeded humanizeToolName export, and fixes an abort-path issue where in-flight tool calls could remain stuck in non-terminal states.
Changes:
- Introduces
ToolRenderBehavior(structure + display callbacks) plumbed fromChat.RoottoFlatAgentRenderervia context. - Removes
humanizeToolNamefromai-coreutilities/exports and adjusts default labeling (“Thinking…” while pending). - Updates
streamSubAgentto mark pending / approval-requested tool calls as cancelled errors on abort.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ai/src/index.ts | Re-exports new ToolRenderBehavior types from @sqlrooms/ai-core. |
| packages/ai-core/src/utils.ts | Removes humanizeToolName helper implementation. |
| packages/ai-core/src/index.ts | Stops exporting humanizeToolName; exports new tool render behavior types. |
| packages/ai-core/src/components/FlatAgentRenderer.tsx | Adds tool-render behavior context/types; passthrough flattening; updated labeling logic and summary-line rendering. |
| packages/ai-core/src/components/Chat.tsx | Adds toolRenderBehavior prop to Chat.Root and wires provider. |
| packages/ai-core/src/agents/AgentUtils.ts | Ensures pending tool calls become cancelled errors when streams abort. |
Comments suppressed due to low confidence (1)
packages/ai-core/src/components/FlatAgentRenderer.tsx:551
approval-requestedtool parts currently fall into theisPendingbranch, which makes the default label render as "Thinking...". That’s misleading for approval flows (and regresses from previously showing the tool name). Consider mappingpart.state === 'approval-requested'totoolCall.state = 'approval-requested'and treating it separately from "pending" so the fallback label remains the tool name (and the UI can distinguish approval vs running).
const state = part.state;
const isSuccess =
state === 'output-available' || state === 'approval-responded';
const isError = state === 'output-error' || state === 'output-denied';
const isPending = !isSuccess && !isError;
const toolCall: AgentToolCall = {
toolCallId,
toolName,
input: part.input,
output: (part as Record<string, unknown>).output,
state: isError ? 'error' : isSuccess ? 'success' : 'pending',
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/ai-core/src/agents/AgentUtils.ts (1)
456-467:⚠️ Potential issue | 🟠 MajorHandle aborts while waiting for sub-agent approval.
Line 456 can wait forever if
abortSignalfires afterrequestSubAgentApprovalis surfaced, so the new cancellation normalization is never reached and the tool can remainapproval-requested. Add an abort listener around the approval promise and reuse the same snapshot/cancel path.🐛 Proposed fix
- const approved = await new Promise<boolean>((resolve) => { - throwIfAborted(); - store.getState().ai.requestSubAgentApproval!({ - toolCallId: pendingApproval!.toolCallId, - approvalId: pendingApproval!.approvalId, - toolName: approvedEntry.toolName, - input: approvedEntry.input, - resolve, - }); - }); - - throwIfAborted(); + let approved: boolean; + try { + approved = await new Promise<boolean>((resolve, reject) => { + const onAbort = () => reject(new ToolAbortError(TOOL_CALL_CANCELLED)); + + if (abortSignal?.aborted) { + onAbort(); + return; + } + + abortSignal?.addEventListener('abort', onAbort, {once: true}); + store.getState().ai.requestSubAgentApproval!({ + toolCallId: pendingApproval!.toolCallId, + approvalId: pendingApproval!.approvalId, + toolName: approvedEntry.toolName, + input: approvedEntry.input, + resolve: (value) => { + abortSignal?.removeEventListener('abort', onAbort); + resolve(value); + }, + }); + }); + + throwIfAborted(); + } catch (err) { + if (abortSignal?.aborted) { + store.getState().ai.clearSubAgentApproval?.(pendingApproval.approvalId); + markPendingToolCallsAsCancelled(toolCallMap); + const snapshot = buildAbortSnapshot( + parentToolCallId, + toolCallMap, + finalText, + store, + ); + store.getState().ai.writeAbortSnapshot?.(parentToolCallId, snapshot); + pushProgress(); + throw new ToolAbortError(TOOL_CALL_CANCELLED, snapshot); + } + throw err; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai-core/src/agents/AgentUtils.ts` around lines 456 - 467, The approval Promise can hang if abortSignal fires after requestSubAgentApproval is emitted; wrap the new Promise in an abort-aware pattern: register an abort listener on abortSignal before calling store.getState().ai.requestSubAgentApproval and, if aborted, call the same snapshot/cancel path used by throwIfAborted (reuse throwIfAborted behavior) to resolve/reject the Promise and clean up the request (e.g., call the original cancel logic for pendingApproval), and ensure you remove the abort listener when the Promise resolves; update the code surrounding pendingApproval, requestSubAgentApproval, and throwIfAborted to use this abort-listener + cleanup pattern.packages/ai-core/src/components/FlatAgentRenderer.tsx (1)
100-109:⚠️ Potential issue | 🔴 CriticalAdd runtime type-check for
reasoningbefore rendering.
toolCall.inputisunknown-shaped; theas string | undefinedtype assertion only silences TypeScript and does not validate runtime values. If a tool sends{reasoning: {...}}or{reasoning: [...]}, the assertion allows the non-string value through, causing it to be assigned tolabeland rendered in JSX. React will crash when rendering non-string primitives as children.🛡️ Proposed fix
- const reasoning = inputObj?.reasoning as string | undefined; + const rawReasoning = inputObj?.reasoning; + const reasoning = + typeof rawReasoning === 'string' ? rawReasoning : undefined;Apply to both
ActivityLogLine(lines 100–109) andOrchestratorLogLineInner(lines 580–589).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai-core/src/components/FlatAgentRenderer.tsx` around lines 100 - 109, The code assigns reasoning with a TypeScript-only assertion and may pass non-string values into label causing React to crash; update the runtime check in ActivityLogLine (and similarly in OrchestratorLogLineInner) so that after deriving inputObj and reasoning you validate with typeof reasoning === 'string' before using it (e.g., const reasoning = typeof inputObj?.reasoning === 'string' ? inputObj.reasoning : undefined) and then build label using that validated string along with getActivityLabel and isPending as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ai-core/src/components/FlatAgentRenderer.tsx`:
- Around line 375-392: When iterating calls in FlatAgentRenderer, passthrough
tools with no nested calls are being dropped (see isPassthroughTool,
getNestedCalls, buildFlatSegments), causing a blank timeline; change the logic
so that if nestedSegments is empty you still record a placeholder/pending
tool-group (e.g., push the passthrough tool into pendingTools or emit a minimal
'tool-group' segment with display/state indicating "running" instead of
skipping), while preserving the existing handling that merges seg.tools when
seg.kind === 'tool-group' and calling flushTools()/segments.push for other seg
kinds; apply the same fix to the identical block that handles passthroughs
elsewhere in the file (the other buildFlatSegments loop).
---
Outside diff comments:
In `@packages/ai-core/src/agents/AgentUtils.ts`:
- Around line 456-467: The approval Promise can hang if abortSignal fires after
requestSubAgentApproval is emitted; wrap the new Promise in an abort-aware
pattern: register an abort listener on abortSignal before calling
store.getState().ai.requestSubAgentApproval and, if aborted, call the same
snapshot/cancel path used by throwIfAborted (reuse throwIfAborted behavior) to
resolve/reject the Promise and clean up the request (e.g., call the original
cancel logic for pendingApproval), and ensure you remove the abort listener when
the Promise resolves; update the code surrounding pendingApproval,
requestSubAgentApproval, and throwIfAborted to use this abort-listener + cleanup
pattern.
In `@packages/ai-core/src/components/FlatAgentRenderer.tsx`:
- Around line 100-109: The code assigns reasoning with a TypeScript-only
assertion and may pass non-string values into label causing React to crash;
update the runtime check in ActivityLogLine (and similarly in
OrchestratorLogLineInner) so that after deriving inputObj and reasoning you
validate with typeof reasoning === 'string' before using it (e.g., const
reasoning = typeof inputObj?.reasoning === 'string' ? inputObj.reasoning :
undefined) and then build label using that validated string along with
getActivityLabel and isPending as before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 79087e78-0f95-4b91-979b-b3e4bb5e778d
📒 Files selected for processing (3)
packages/ai-core/src/agents/AgentUtils.tspackages/ai-core/src/components/Chat.tsxpackages/ai-core/src/components/FlatAgentRenderer.tsx
| for (const tc of calls) { | ||
| if (isPassthroughTool?.(tc)) { | ||
| const nested = getNestedCalls(tc, agentProgress); | ||
| const nestedSegments = buildFlatSegments( | ||
| nested, | ||
| agentProgress, | ||
| isPassthroughTool, | ||
| ); | ||
| for (const seg of nestedSegments) { | ||
| if (seg.kind === 'tool-group') { | ||
| pendingTools.push(...seg.tools); | ||
| } else { | ||
| flushTools(); | ||
| segments.push(seg); | ||
| } | ||
| } | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Avoid a blank timeline before passthrough children arrive.
A pending passthrough tool with no nested calls yet is currently dropped entirely, and a passthrough parent with displayCalls.length === 0 also hides its summary. That can leave users with no visible running state until the first child tool appears.
🛠️ Proposed adjustment
if (isPassthroughTool?.(tc)) {
const nested = getNestedCalls(tc, agentProgress);
+ if (nested.length === 0) {
+ pendingTools.push(tc);
+ continue;
+ }
const nestedSegments = buildFlatSegments(
nested,
agentProgress,
isPassthroughTool,
); const hideParentSummary = !!(
parentToolCall &&
isPassthroughTool &&
- isPassthroughTool(parentToolCall)
+ isPassthroughTool(parentToolCall) &&
+ displayCalls.length > 0
);Also applies to: 682-690
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ai-core/src/components/FlatAgentRenderer.tsx` around lines 375 -
392, When iterating calls in FlatAgentRenderer, passthrough tools with no nested
calls are being dropped (see isPassthroughTool, getNestedCalls,
buildFlatSegments), causing a blank timeline; change the logic so that if
nestedSegments is empty you still record a placeholder/pending tool-group (e.g.,
push the passthrough tool into pendingTools or emit a minimal 'tool-group'
segment with display/state indicating "running" instead of skipping), while
preserving the existing handling that merges seg.tools when seg.kind ===
'tool-group' and calling flushTools()/segments.push for other seg kinds; apply
the same fix to the identical block that handles passthroughs elsewhere in the
file (the other buildFlatSegments loop).
lixun910
left a comment
There was a problem hiding this comment.
👍 let's try it and get some feedbacks
Summary
Adds a
ToolRenderBehaviorAPI onChat.Rootso host apps can customize how tool calls are rendered inFlatAgentRenderer, and fixes a bug where pending tool calls were left in a stuck state when a stream was aborted.Motivation
FlatAgentRendererpreviously hardcoded tool-call presentation: every agent tool call produced a summary line, leaf calls were labeled with their raw tool name (or areasoninginput field, if present), and display names were passed through a built-inhumanizeToolNamehelper with a fixed acronym list.This works for generic cases but becomes limiting when host apps introduce higher-level tool-call concepts — for example, an orchestrator tool whose only job is to dispatch work to another agent, where rendering a wrapper summary line adds noise without adding information. Host apps also typically have richer context for producing human-readable labels (domain vocabulary, dynamic inputs) than any generic helper baked into the library.
Changes
ai-core—ToolRenderBehaviorAPIChat.Rootnow accepts an optionaltoolRenderBehaviorprop, made available toFlatAgentRendererviaToolRenderBehaviorContext. The API is split into two concerns:isPassthroughTool(toolCall): when true, the tool call renders no summary line and its nested calls are flattened into the caller's segment list. Applied recursively inbuildFlatSegmentsand also respected at the top-level parent-summary inFlatAgentRenderer.getToolDisplayName(toolCall)overridesParentSummaryLinelabels;getActivityLabel(toolCall)overrides leaf log-line labels inActivityLogLineandOrchestratorLogLineInner. Both returnundefinedto fall back to the default.All three callbacks are optional; the default behavior is unchanged when
toolRenderBehavioris omitted.Removed
humanizeToolNameWith host apps now able to provide display names directly, the built-in
humanizeToolNamehelper (and its hardcoded acronym list) is no longer needed and has been removed from the public exports. The default fallback is simply the raw tool name, and the pending state shows "Thinking..." to avoid flashing the raw name beforereasoninginput is available. Leaf labels that come from areasoninginput field are now rendered in italic to distinguish them from tool names.Dropped
reasoningtail fromParentSummaryLinePreviously parent summary lines appended a masked, italicized
reasoningtail next to the tool name. This has been removed in favor of a clean, single-label line — host apps that want richer labels can compose them viagetToolDisplayName.Bug fix — abort leaves tool calls stuck
streamSubAgentnow marks any tool calls still inpendingorapproval-requestedstate aserrorwith a cancellation message when the abort signal fires. This happens both inside the stream error path and after the main loop. Previously those calls could remain indefinitely in their pre-abort state in the progress snapshot.Public API additions
Exported from
@sqlrooms/ai-core(and re-exported from@sqlrooms/ai):ToolRenderBehaviorToolStructureBehaviorToolDisplayBehaviorRemoved exports
humanizeToolNameTest plan
FlatAgentRendererusage renders identically when notoolRenderBehavioris passedgetToolDisplayName/getActivityLabeloverrides apply;undefinedreturns fall back to defaultspendingstateSummary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores