Skip to content

FE-1448: Let Petrinaut hosts render interactive chat tools - #9249

Open
lunelson wants to merge 2 commits into
mainfrom
ln/fe-1448-host-interactive-tools
Open

FE-1448: Let Petrinaut hosts render interactive chat tools#9249
lunelson wants to merge 2 commits into
mainfrom
ln/fe-1448-host-interactive-tools

Conversation

@lunelson

@lunelson lunelson commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🌟 What is the purpose of this PR?

Brunch — the elicitation agent being integrated with Petrinaut — needs to ask its questions inline in Petrinaut's chat panel, but the panel could only render its own built-in interactive tool, and @hashintel/petrinaut must stay free of Brunch-specific code. This PR adds a public, typed extension point through which an embedding application registers its own interactive chat tools: a registered tool renders inline, collects exactly one submitted answer, and returns it to the assistant through the same follow-up path the built-in tools use.

🔗 Related links

🚫 Blocked by

Nothing.

🔍 What does this change?

Before this change, the chat panel's interactive-tool registry was private and hard-coded to Petrinaut's own applyAutoLayout confirmation widget, so any other dynamic tool call was rejected before a host could render or handle it. The panel now accepts host-registered tools through the public aiAssistant configuration: a host defines a tool with definePetrinautAiInteractiveTool — a tool name, input and output parsers, and a React component — and the panel renders it inline, keeps it visible as a read-only record after submission, and sends exactly one output back to the assistant. Built-in tools keep their behavior, and a dynamic tool that no host registered still fails loudly.

🏗️ Agent notes

Public API and type erasure

  • definePetrinautAiInteractiveTool and the PetrinautAiInteractiveTool* types are exported from @hashintel/petrinaut; hosts pass registrations through aiAssistant.interactiveTools.
  • The definition type preserves the relationship between the input/output schemas and the component's props; registration erases those generics behind a symbol-keyed opaque type so heterogeneous host tools share one registry without any.
  • Parsers are structural ({ parse(value: unknown): Value }), so Zod, Valibot, or hand-written parsers all fit without a validation-library choice leaking into the public API.

Lifecycle

  • Widget props are a discriminated union over state: "awaiting" | "submitted", with a stable AI SDK toolCallId; submit accepts exactly one output — duplicate submissions are ignored and produce exactly one automatic follow-up through the existing addToolOutput path.
  • A submission the output parser rejects leaves the widget retryable rather than wedged half-submitted.

Verification

  • Registry tests cover merging built-in and host tools and the loud unknown-tool failure.
  • Lifecycle tests drive the real AiAssistantPanel through submission, duplicate submission, and rejected-then-retried submission, asserting exactly one follow-up request; the synthetic test transport's steps are delimited to prevent repeated follow-ups.
  • The Petrinaut / WithHostInteractiveAiTool story registers a visible synthetic host tool, styled with Panda.

Commits

  • 57913da2bb adds the host interactive-tool API and registry routing.
  • 720c6b1097 proves the follow-up lifecycle against the real panel and adds the visible story.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • I am unsure / need advice

@hashintel/petrinaut is npm-publishable and this branch carries no changeset; the changeset for this extension point currently lives on #9274, so whether this branch needs its own depends on merge order — see Known issues.

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

The end-user AI assistant guide now describes host-provided inline tools, and the package README documents the public API.

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

PR #9274 (FE-1437, the Brunch monorepo import) carries an evolved variant of this extension point together with a real host tool (brunch_ask in apps/petrinaut-website) and the @hashintel/petrinaut changeset. The two branches' Petrinaut trees have diverged, so whichever merges second must be reconciled against the first; merge order is an open sequencing decision.

🐾 Next steps

FE-1449 (already done) built on this mechanism to prove that a structured Brunch question suspends and resumes through the real panel; that work now travels with the FE-1437 import branch (#9274). The remaining step for this PR is the reconciliation described under Known issues.

🛡 What tests cover this?

  • Interactive-tool registry tests: built-in and host tools merge, and an unregistered dynamic tool fails loudly.
  • AiAssistantPanel lifecycle tests: submission, duplicate submission, and rejected-then-retried submission, with exactly one follow-up request.
  • The visible WithHostInteractiveAiTool Storybook story exercising a synthetic host tool.

❓ How to test this?

  1. Run turbo run test:unit --filter '@hashintel/petrinaut'.
  2. Open the Storybook story Petrinaut / WithHostInteractiveAiTool.
  3. Trigger the synthetic host tool, submit it, and confirm the widget moves from awaiting to a read-only submitted record with a single assistant follow-up and no duplicate question.

📹 Demo

The WithHostInteractiveAiTool story is the runnable demo surface: it shows the synthetic host tool inline in the chat panel, before and after submission.

🤖 Generated with Claude Code

Exercise the real AiAssistantPanel and AI SDK addToolOutput path, including exactly one automatic follow-up after duplicate submits.

Keep rejected submissions retryable, use Panda styles in the visible story, and delimit the synthetic transport steps to prevent repeated follow-ups.
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 19, 2026 12:59pm
petrinaut Ready Ready Preview Aug 19, 2026 12:59pm
petrinaut-docs Ready Ready Preview Aug 19, 2026 12:59pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 19, 2026 12:59pm

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps > hash.design Affects the `hash.design` design site (app) labels Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@lunelson lunelson changed the title FE-1448: add host interactive AI tools FE-1448: Petrinaut hosts render their own interactive chat tools Aug 19, 2026
@lunelson lunelson changed the title FE-1448: Petrinaut hosts render their own interactive chat tools FE-1448: Let Petrinaut hosts render interactive chat tools Aug 23, 2026
@lunelson
lunelson marked this pull request as ready for review August 24, 2026 07:54
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Extends the public Petrinaut AI assistant API and tool-dispatch path, including dynamic addToolOutput handling. Built-in tools stay gated, but a host-supplied widget can now inject structured tool results into the conversation.

Overview
Hosts can now register their own inline interactive AI tools on aiAssistant.interactiveTools via definePetrinautAiInteractiveTool, instead of only Petrinaut’s built-in applyAutoLayout confirmation.

A registered tool is a name, structural input/output parsers, and a React widget. Dynamic calls are validated, rendered in chat (awaitingsubmitted), and submitted once through the existing AI SDK follow-up. Duplicate submits are ignored; a rejected output can be retried. Unknown names, duplicate host names, and collisions with built-ins throw.

Docs, registry/panel tests, and the WithHostInteractiveAiTool story cover the lifecycle.

Reviewed by Cursor Bugbot for commit 720c6b1. Bugbot is set up for automated code reviews on this repo. Configure here.

@lunelson
lunelson requested a review from kube August 24, 2026 07:54

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 720c6b1. Configure here.

export const AiAssistantContents = ({
error,
input,
interactiveTools = [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty default busts message memo

Medium Severity

interactiveTools = [] allocates a new array whenever the prop is omitted, which is the common no-host-tools path. That new reference is passed into memoized AiAssistantMessage, so completed transcript rows re-render on every throttled stream chunk and undo the memoization this panel relies on to stay responsive.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 720c6b1. Configure here.

const submitted = tool.state === "output-available";
const submittedOnceRef = useRef(submitted);
const Widget = definition.Widget;
const typedInput = definition.parseInput(input);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host parse crashes mid-stream

High Severity

Host interactive tools always shouldHandle, and InteractiveToolItem now calls parseInput during render with no fallback. While a dynamic tool call is still input-streaming (or otherwise has incomplete input), schema parse throws and can take down the chat panel before onToolCall validation runs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 720c6b1. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash.design Affects the `hash.design` design site (app) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant