feat(chat): surface sidecar interactions via onInteraction on the chat-stream primitive#176
Merged
Merged
Conversation
Lift the framework-agnostic interaction contract (ChatInteraction type, wire<->persisted codecs, dedupe/transition/compose-answer helpers) into web-react so every agent-app consumer imports one source of truth instead of hand-rolling it. Pure module (no React), re-exported from the web-react barrel alongside the chat-stream primitives.
dispatchChatStreamLine silently dropped the sidecar 'interaction' NDJSON line — the backend already threads it into the stream (#166), but the client parser had no seam for it. Add an optional onInteraction callback to ChatStreamCallbacks and a case in the switch that validates the ask with the shared wire codec and surfaces the parsed ChatInteraction; a malformed ask is logged and skipped. Optional callback keeps every existing consumer unchanged. Covers dispatch, the {kind:'event'} envelope, and consumeChatStream drain, plus a non-breaking (no callback) path.
tangletools
approved these changes
Jul 8, 2026
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 5abf89de
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-08T19:25:09Z
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Lifts the proven client-side chat-interaction handling into the
web-reactchat-stream primitive so any product on@tangle-network/agent-appgets live agent asks (kindquestion/plan/…) without hand-rolling the wire codec.Closes the verified gap: the backend already threads
backend.interactionsinto the stream (#166), so the sidecarinteractionline IS in the stream — but the client parser (dispatchChatStreamLine/consumeChatStream/streamChatTurn) exposed callbacks for text/reasoning/tool/usage/metadata/error and noonInteraction, so it silently dropped the ask.Refs #175.
Changes
src/web-react/chat-interactions.ts(new) — the framework-agnostic interaction contract, lifted verbatim in semantics from the reference consumer (gtm-agent): theChatInteractiontype, wire↔persisted codecs (interactionFromWireRequest,interactionToPersistedPart,persistedPartToInteraction,parseInteractionRequest/parseInteractionCancel), and the pure helpers (dedupeQuestionInteractionsByContent,canTransitionInteractionStatus,cancelStatusFor,composerAnswerDeliveries/composerAnswerData/fieldAcceptsFreeText,isRenderableInteractionKind, part-key helpers). No React, no server imports — schema validation via the@tangle-network/agent-interfacepeer. One source of truth so gtm/tax/legal/physim stop forking it.ChatStreamCallbacks.onInteraction?— optional callback:(interaction: ChatInteraction) => void.dispatchChatStreamLine— recognizes the{type:'interaction', data:{request}}line (and the{kind:'event'}envelope form), validates with the shared codec, and firesonInteractionwith the parsedChatInteraction; a malformed ask is logged and skipped (never half-surfaced). Counts as received turn activity (the run is blocked on the user).src/web-react/index.tsx— barrel re-exports the contract next to the chat-stream primitives.src/web-react/chat-stream.test.ts(new) — feeds aninteractionline throughdispatchChatStreamLineand assertsonInteractionfires with the rightChatInteraction; covers the{kind:'event'}envelope, theconsumeChatStreamdrain path, a malformed-line drop, and — proving non-breaking — a consumer with noonInteractionthat parses the same stream unchanged.Non-breaking
Purely additive: the callback is optional and no existing signature changed. Existing consumers compile and behave identically (covered by a dedicated "no onInteraction" test). Consumers opt in by wiring
onInteractionand importing the shared contract instead of hand-rolling it.Verification
pnpm typecheck— clean.pnpm test— 137 files / 2227 tests green (includes 6 new interaction tests).pnpm build(tsup ESM + d.ts) — clean;onInteraction+ the contract exports present indist/web-react/index.d.ts.Not published to npm — that's the separate gated release step.