@athenaintel/react
v0.28.1
Published
Athena React SDK — Build AI-powered chat applications with the Athena platform
Readme
@athenaintel/react
React SDK for building AI-powered chat applications with the Athena platform.
Installation
npm install @athenaintel/reactIf you use the PropelAuth-specific provider from @athenaintel/react/auth, also install:
npm install @propelauth/reactUpgrading
The SDK pins its assistant-ui runtime (@assistant-ui/tap, store, core,
react, react-statewire, statewire) at exact versions, and those packages
carry React contexts and module singletons: an app must resolve exactly one
copy of each. A fresh install does. An existing app does not necessarily — bun
keeps a bun.lock's existing resolutions on bun add, so after an SDK bump the
transitive @assistant-ui/react-statewire / statewire dependencies can keep
an older nested @assistant-ui/tap next to the SDK's new one. Two tap contexts
mount and the page renders blank with:
Error: AssistantTapContext is not availableMake the app immune with a resolve.dedupe list in vite.config.ts (the Athena
computer templates ship it; other bundlers have an equivalent single-instance
setting):
export default defineConfig({
resolve: {
dedupe: [
'@assistant-ui/tap',
'@assistant-ui/store',
'@assistant-ui/core',
'@assistant-ui/react',
'@assistant-ui/react-statewire',
'react',
'react-dom',
],
},
});Then bump to the release you want and re-resolve from scratch.
Lockfile-preserving commands do not do it — bun update and
bun install --force keep the nested lockfile entries, and bun remove +
bun add (or deleting only bun.lock) rewrites the lockfile but bun leaves
the nested directories in node_modules, which Node still resolves first:
bun add @athenaintel/react@<version> # the release you are upgrading to
rm -rf node_modules bun.lock && bun install
find node_modules -path '*/@assistant-ui/tap/package.json' | wc -l # expect 1Do not add @assistant-ui/* packages to your own dependencies to pin them:
bun update @assistant-ui/tap on a package that is not a direct dependency
behaves like bun add at the latest version and produces more copies, not
fewer.
Quick Start
import { AthenaProvider, AthenaChat, Toolkits } from '@athenaintel/react';
import '@athenaintel/react/styles.css';
function App() {
return (
<AthenaProvider
config={{ apiKey: 'your-api-key' }}
tools={[Toolkits.DOCUMENT, Toolkits.WEB_SEARCH]}
>
<AthenaChat />
</AthenaProvider>
);
}Provider Config
<AthenaProvider config={{ environment: 'staging' }}>
<AthenaChat />
</AthenaProvider>
<AthenaProvider
config={{
apiUrl: 'https://iris.example.com',
backendUrl: 'https://api.example.com',
appUrl: 'https://app.example.com',
}}
>
<AthenaChat />
</AthenaProvider>config accepts apiKey, token, getToken, apiUrl, backendUrl, appUrl, statewireSyncUrl, trustedParentOrigins, environment, extraRunConfig, and posthog. Top-level apiKey, token, apiUrl, backendUrl, appUrl, and environment props remain supported as compatibility aliases, but config is now the preferred API.
If config.appUrl is omitted, the provider resolves it from the parent bridge or from the matching Athena environment defaults. Known Athena staging and production apiUrl and backendUrl values automatically fall back to the corresponding frontend origin.
Use trustedParentOrigins when the SDK runs inside an iframe on a private-cloud or custom parent domain and you want to explicitly allow postMessage auth/config from that parent. Only the window that frames the app is heard. By default athenaintel.com hosts, localhost and your own origin are trusted, except the hosts that serve customer-written code (*.app.athenaintel.com, *.code-sandpack.athenaintel.com and friends; see "What's new in 0.28.0").
Transport (statewire only, as of 0.18.0)
The SDK speaks one chat transport: statewire. The provider attaches to the
deep-agent statewire sync host at <statewireSyncUrl>/<threadId>, so the
thread is hosted server-side and replicated live — history, reconnect, run
scheduling, mid-run queueing, HITL approvals, and client tools (for recognized
SDK hosts, e.g. computer-asset app_ids) all come from the server snapshot.
The legacy Iris /api/chat assistant-transport leg was deleted in 0.18.0
along with the transport prop — there is nothing to opt back onto. See
Migrating from 0.17.
The sync endpoint resolves in this order: an explicit statewireSyncUrl; the
statewireSyncUrl the host environment supplies (a Marathon-served computer
asset mints it from /_athena/auth alongside backendBaseUrl, the bare Agora
base — a trusted parent iframe can post the same fields); then the effective
apiUrl host + /v2/threads (e.g. staging resolves to
https://iris.stg.athenaintel.com/v2/threads, the same statewire base the
mobile app and Chrome extension ship). Pass statewireSyncUrl to pin a custom
host whose sync mount lives on a different origin. If nothing yields a URL
(e.g. an unparseable custom apiUrl), the provider throws instead of falling
back to an Athena default — credentials are never sent to a host you didn't
configure. Hosts that predate the minted statewire fields keep working through
the apiUrl derivation. Auth reuses the provider's existing credential
plumbing: tokens ride Authorization: Bearer and API keys ride X-API-KEY.
A getToken resolver is called with { force: true } right after the sync
host refuses a credential, so a caching getter must mint a fresh token then;
the persisted command lane is scoped to the signed-in user as well as the
host, so a shared browser never resends one user's pending sends under the
next user's token.
What the statewire transport wires up in AthenaChat:
- Approvals (HITL): when the backend parks a run on an approval-gated
tool call, an approval card renders above the composer with Approve/Reject
(or Continue for a generic pause) and resumes the run through the statewire
run/inputcommand. Hosts that build their own thread UI should mount the exportedStatewireApprovalCardthemselves — without it an approval-gated run stays parked with no way to answer it. ItsrenderDetails,isHandledElsewhere, andreadOnlyseams take host-specific review UIs, requests another surface answers, and viewers without edit rights; the headlessuseStatewireApprovalRequestbehind it is exported for hosts with their own decision UI. - Frontend tools: tools in
frontendToolswith a localexecuteare declared to the backend asclient_toolsand executed by the mounted client-tool bridge, which resumes the run with each tool's result. - Mid-run sends and the queue: a message sent while a run is active
queues on the server. The queue panel above the composer
(
StatewireQueuedMessages) lists queued entries with remove, "Send now" (steer), Prioritize (run/prioritize), inline edit, and failed sends with Retry/Discard, plus Continue for a stopped run's queue head; hosts can opt intoautoContinuefor stranded queues and slot their own row renderer and editor. - Lean entries for hosts with their own composer:
@athenaintel/react/chat-ui/statewireexports just the approval card, the queue tray, and the continue/run-ended affordances (plus the runtime-message card and Continue pill they render), and@athenaintel/react/toolsexports the genericToolFallbackcard with its primitives (ToolCard,ExpandableSection,NestedPtcMessages) — so a host that already ships its own composer and mention stack (Olympus) never pulls the SDK's Tiptap graph into its build. Tool names, icons, categories, and running copy come from the shared registry in@athenaintel/agent-runtime(getToolDisplayMeta), and success/failure from itsclassifyToolResult; the fallback takes host seams (displayName,icon,extraAction,notice,renderResult,onOpenAsset,hideInternalIdArgs) instead of being copied.@athenaintel/react/super-groupingexports the grouped ("conversational") view of an assistant turn —SuperGroupingCard,SuperGroupingFinalText,CollapsibleGroup, the activity-timeline derivation, and the tool-based grouping function — with slots for the host's text/tool renderers and controls (renderHeaderActions,renderRunningMeta,summaryDetails,waitingTools,expansionKey), so Olympus and the Chrome extension mount one card instead of copies. - In-repo hosts consume the package, not the source.
@athenaintel/reactis a workspace dependency of Olympus, the Chrome extension, and the Office add-in, resolved through this package'sexportsto the builtdist/(the guardedpreparescript builds it on a coldbun install;scripts/prepare-react-sdk.tsrebuilds a missing or stale dist in front of each host's typecheck, test, and build scripts, andscripts/typecheck-all.tsruns it once before the repo typecheck). While editing the SDK against a running host (next dev, the extension'sbun run dev, the add-in'svite), runbun run devhere so the dist follows your edits. Because the dist inlines the private@athenaintel/agent-runtime, a host that also imports agent-runtime directly runs two copies of it; any agent-runtime state shared between the SDK's components and the host has to live onglobalThis(seeregisterChatAssetMentionHandler). - Errors and connection state: transport errors (send rejections,
paywall refusals) render as a dismissible banner; a degraded connection
shows a reconnect strip; a send into a thread created by the retired legacy
runtime is refused with a dismissible read-only notice (start a new chat).
Custom hosts can read all of it via
useAthenaStatewireLifecycle. - Thread list:
enableThreadListprovides the persisted Agora session list; switching threads (or starting a new chat) re-attaches the statewire session to the selected thread while the host's ownchildrenstay mounted — sidebars keep their scroll position and search text, custom hooks keep their state, and only the per-thread pieces reset: the runtime and its client,AthenaChat's surface (composer draft, viewport scroll, approval cards, banners), and the thread-id contexts. A host composing its own chat from the statewire pieces keys any per-thread UI onuseAthenaStatewireThreadId()(the attached thread's id, defined for a never-started chat too) the wayAthenaChatdoes.ThreadList,SessionList,useAthenaThreadManager,useAthenaSessionAttention, anduseRefreshThreadListread that state (and render nothing / returnnullwithout it). See Session list, attention and wake markers.
The agent prop only accepts collab_agent:<asset_id> refs (see below),
because the statewire host always runs the Athena deep agent. Any other
non-blank value throws at first render — it is a misconfiguration, and a
silently ignored selector would let a deployment answer as the wrong agent.
model defaults to the deep-agent default model (DEFAULT_STATEWIRE_MODEL)
unless a collab agent supplies one.
Session list, attention and wake markers
A Devin-style session list on top of enableThreadList: every row says what
the session asks of the viewer, what to do next, and which pull requests it
opened; the active thread gets a persistent "waiting on you" strip; and a
GitHub wake renders as a runtime marker instead of a user bubble.
import { AthenaChat, AthenaProvider, SessionList } from '@athenaintel/react';
<AthenaProvider enableThreadList config={{ token }}>
<aside><SessionList /></aside>
<AthenaChat />
</AthenaProvider>Field contract (v1)
Rows of POST /api/conversations/threads/list carry, all optional (an older
server omits them and the SDK reads undefined):
| REST field | SDK field (ConversationSummary / AthenaStatewireThreadSummary) | Values |
|---|---|---|
| session_status / run_status | sessionStatus / runStatus | status_v2 planes |
| attention | attention | needs_you · working · ready · watching · done · error |
| next_action | nextAction | one line, ≤ 80 chars |
| attention_source | attentionSource | interrupt · run · pull_request · termination · wake · handoff |
| is_favorited | isFavorited | boolean — toggled through setFavorited / POST /api/conversations/threads/favorite |
| last_wake | lastWake | { source, reason, at } |
| pull_requests (opt-in: include_pull_requests) | pullRequests | [{ repoFullName, number, url, title, state OPEN/CLOSED/MERGED, isDraft, reviewDecision, checksState SUCCESS/FAILURE/PENDING/NONE, updatedAt }] — [] none, null unreadable right now |
The per-user conversation index pushes attention, next_action,
attention_source and is_favorited (never pull-request state), and the
thread list folds every push into its rows through the same
summaryFromConversationIndexEntry that renames them — a session flips to
"needs you" live. deriveConversationAttention (exported) is the one
attention rule: the server value when reported, else needs_you for a human
gate, working for a live run, error for a failed one.
SessionList
Same visual language as ThreadList (Tailwind via cn, lucide icons), plus:
- a status indicator per row —
needs_youan orange dot,workinga spinner, unread (and not needs-you) a blue dot,watchinga clock, otherwise none; - a second line —
nextActioncoloured by attention (needs-you orange, ready green, error red), else the muted relative time of the last update; - one pull-request chip with the state icon of the primary pull request
(open / draft / merged / closed):
#123links to that pull request;3 PRsopens a list of all of them (in flight first, then the most recently updated), each a link to GitHub with its number, state and checks (a repository shared by all of them is named once in the list's header) — a sibling of the row button, so a click on it never selects the row; - an origin badge from
appId/startChannel/triggerType(Chrome, Word, Computer, GitHub, API, …); - a trailing action cluster per row, revealed on hover / focus (a pinned pin
and any action whose write is in flight stay visible) — every action a
labelled
<button>that is a sibling of the row control, never nested:- Rename (prop
showRename, defaulttrue): the title becomes an input in place; Enter commits, Escape cancels, blur commits; an unchanged or empty title is never sent; the row shows the new title at once and rolls back on failure (POST /api/conversations/threads/rename,{ thread_id, title }→ the stored, trimmed title; 1–200 characters). A viewer who can pin a shared session cannot retitle it (the route runs the same EDIT gate asupdateAsset(title)); the row rolls back on the 403. On an agora that predates the route (404 / 405) the action is withdrawn (renameAvailable) after one console warning. - Pin / Unpin (prop
showPinToggle, defaulttrue): a<button aria-pressed>, filled while the session is pinned. - Archive (prop
showArchive, defaulttrue): the row leaves the list at once and stays hidden until both list sources stop reporting it; a failure brings it back (POST /api/conversations/threads/archive). Archiving the active session moves the chat to a new one once the server has confirmed. Withdrawn on a backend without the route (archiveAvailable).
- Rename (prop
- groups
Needs you→Working→Pinned→Recent(propgrouped, defaulttrue; a session appears once, in the first group that applies), labelled in 11 px uppercase muted text; the active row has abg-mutedbackground and a 2 px accent bar; titles aretext-sm font-medium, the next-action linetext-xsin its attention tone and never bolder than the title; the pull-request chip sits on the meta line; every row control and action has afocus-visiblering; - hover prefetch (prop
prefetchOnHover, defaulttrue): after the pointer has rested on a row for 120 ms (or when the row control takes focus) the list warms the provider's thread snapshot cache with that thread's REST state — once per thread per identity, at most two fetches in flight, failures ignored — so selecting it paints the transcript on the first frame instead of the loading scrim while the statewire attach lands (useAthenaStatewireThreadList().prefetchThread(threadId)for hosts drawing their own rows); - skeleton rows (indicator, title line, meta line) while the first page
loads — including while no credential has resolved — and for a refresh
that has no rows to keep; "No sessions yet." only after a settled empty
answer (
hasResolvedFirstPage); a background refresh keeps the rows it has; - a search box wired to the server-side
search(propshowSearch, defaulttrue),Load moreover the shared pager, andNew Chat/ refresh as inThreadList.
The pin writes POST /api/conversations/threads/favorite
({ thread_id, is_favorited } → the server's state after the write) with the
same credential as the other thread endpoints — setThreadFavorite is
exported for hosts calling it themselves. The toggle is optimistic: the row
flips (and moves between Pinned and Recent) on the click, the server's
answer is written back, and a failure restores the previous state. The
optimistic value is held as an override over both list sources — the fetched
page and the pushed conversation index — until they report the toggled value,
so a push that predates the write cannot flip the row back. Writes for one
session are serialized: the row's pin is disabled while its write is in
flight (favoritePendingThreadIds), and a setFavorited call made meanwhile
records the value and sends at most one follow-up write once the first has
answered, so the server never ends on an earlier click. Overrides and
in-flight writes are scoped to the (backendUrl, principal) pair — a
credential swap reads an empty map and drops the previous identity's
answers. On an agora that
predates the route the first toggle gets a 404 / 405
(isThreadFavoriteUnavailableError): the row is restored, one console
warning is logged, favoriteToggleAvailable on the thread-list state turns
false, and the row shows a pinned session's pin read-only, as before. The
conversation-history hooks (useStatewireConversationHistory,
useRestConversationHistory) expose the same toggle as
setConversationFavorited / favoritePending / favoriteToggleAvailable
beside setConversationReadState.
The provider asks for pull-request state by default in thread-list mode
(threadListIncludePullRequests, default true; turn it off in a host that
never shows the chips to spare the server the lookup). The pure rules behind
the rows — groupSessions, sessionIndicator, sessionSecondaryLine,
pullRequestChip, orderedPullRequests, sessionOriginLabel — are exported for hosts drawing their
own rows; useAthenaStatewireThreadList().setFavorited({ threadId,
isFavorited }) is the toggle behind them.
useAthenaSessionAttention(threadId?)
{ threadId, attention, nextAction, attentionSource, pullRequests, isUnread }
for the active (or given) thread, from the thread-list state; null outside
thread-list mode or for a thread the list does not hold. Updates live with the
index pushes.
WaitingOnYouBanner
Mounted by AthenaChat above the thread viewport: a persistent strip while
the active thread's attention is needs_you or the approval hook
(useStatewireApprovalRequest) reports a pending request. Its text is the
server's nextAction, else a summary of the pending request; a click scrolls
to the approval card (or to the composer when there is none). It never
duplicates the card's Approve / Reject buttons. WaitingOnYouBannerView is
the pure presenter for hosts that resolve the inputs themselves.
Wake markers
A user turn injected by a GitHub webhook wake carries a provenance envelope
under metadata.custom.github_event_source. AthenaChat's user message (and
the chat-ui UserMessage) render such a turn as a compact runtime marker —
"Woke: review comment on #36337", linked, with the sender and the comment
excerpt — in the same style as the runtime-ending card, instead of a bubble
quoting the whole wake text. readGithubWakeEvent, isGithubWakeMessage,
githubWakeMarkerText, WakeMarkerCard and WakeMarkerMessage are exported
(also on @athenaintel/react/chat-ui and /chat-ui/statewire).
The envelope is message metadata, so the reader trusts only the shape the
wake handler stamps: the repository must be owner/name, and the link is kept
only when it is an https://github.com/<repo>/… URL for that repository
(githubWakeEventUrl) — anything else renders the reference as plain text.
The full message text always reaches the model; the marker is provenance for
rendering only.
Voice Sessions (@athenaintel/react/voice)
useAthenaVoiceSession runs a real-time voice conversation with an Athena
voice agent and streams the transcript into React state. It owns the whole
browser side — session creation, room connect, mic,
agent audio, live/interim transcript turns, reconnects, StrictMode-safe
teardown — so an app is a start() button and a transcript list.
bun add @athenaintel/react livekit-client # optional peer dep, only needed for voiceimport { useAthenaVoiceSession } from '@athenaintel/react/voice';
const voice = useAthenaVoiceSession({
session: {
system_prompt: 'You are a customer at an auto-parts store...',
provider: 'openai', // "google" | "openai"
model: 'gpt-realtime-2',
voice: 'marin',
enabled_tools: [], // [] = no tools; omit for the server default set
},
onTranscript: (turn) => console.log(turn.speaker, turn.text),
});
// voice.status: 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended' | 'error'
// voice.transcript: AthenaVoiceTranscriptTurn[] voice.sessionId: asset id
<button onClick={voice.start}>Talk</button>
<button onClick={voice.stop}>Hang up</button>Voice tools. enabled_tools takes voice tool names (AthenaVoiceToolId),
not the chat Toolkits ids — 'web_search_browse_toolkit' is not a voice tool,
'web_search' is. Available: web_search (search_web is a legacy alias),
browse (read a URL), search_knowledge_base and search_knowledge_base_lr
(the workspace knowledge base; _lr is the thorough long-running retrieval),
read_full_asset and search_assets (as the viewer), ask_agent (hand a
question to the general agent), save_note, get_current_time_est. Tools that
call back into the client over room RPC (write_to_clipboard, the desktop and
mobile tools) are not part of the hook's contract. Omit the field for the
server's default set; pass [] for pure conversation. The server refuses
unknown names with a 422.
Silent start. By default the agent opens the call with a greeting. Pass
silent_start: true and it stays quiet — no greeting audio, no greeting
transcript turn — until a participant speaks (a deposition witness, a
customer waiting to be addressed). Replies after that are unchanged.
Silence is enforced by not asking the model to speak; with the full-duplex
gpt-live-1 model the first reply still follows whatever the model hears, so
verify on a real call if a pre-speech greeting would matter.
Authentication. Served from a Marathon computer-asset URL, the hook is
zero-config: it takes the viewer bearer from useParentBridge() and creates
the session as the person using the app, so it shows up under their
Sessions in Athena and transcript/grading calls are authorized as them. This
works for workspace members and for external SSO viewers alike: the viewer
credential minted by /_athena/auth is authorized for session creation and
grading the same way it is for /api/v0/sessions, scoped to the viewer's own
workspace. Anywhere else — standalone hosting, custom auth — pass
createSession and create the session from your own server (API key stays server-side):
useAthenaVoiceSession({
createSession: (signal) =>
fetch('/api/voice/session', { method: 'POST', signal }).then((r) => r.json()),
// → { server_url, token, room_name, asset_id }
});Off Marathon without createSession, start() fails with an explicit error;
it never falls back to a shared key.
After the call. voice.sessionId is a regular Athena session asset:
- Transcript:
GET /api/v0/sessions/{asset_id}/download?format=messages - Grading against a rubric:
POST /api/v0/agents/general/invokewithconfig.structured_output(a JSON Schema) → typedstructured_outputin the response.
Both use the same bearer / API key as session creation. The full recipe
(persona → roleplay → transcript → rubric score) lives in the computer
template skill voice-session-guide.md. createAthenaVoiceSession is also
exported if you hold the bridge state yourself.
What's new in 0.28.0
The iframe auth bridge only listens to the page that frames your app, and that page can no longer point the SDK's token at another server. When the SDK runs inside an iframe, useParentBridge takes athena-auth (a token) and athena-config (URLs) from the parent window. Before this release it took them from any window on any *.athenaintel.com origin, including the hosts that serve customer computer and application apps, and an athena-config could replace backendBaseUrl / statewireSyncUrl at any time. A page on one of those hosts could frame or open another app and make its SDK send its token to a server of its choosing. Now:
- A message counts only when its
sourceis the window that frames the app (window.parent). A popup, an opener or a sibling frame on a trusted origin is ignored. - Hosts that serve customer-written code are no longer trusted parents by default: Marathon's computer and application apps (
{id}.app.athenaintel.com,{id}.staging-app.athenaintel.com,{id}.canary-app.athenaintel.com,{id}.app.{env}.athenaintel.com,{id}.computer.stg.athenaintel.com,{id}.computer-canary.stg.athenaintel.com) and User Interface previews ({id}.code-sandpack.athenaintel.com). Every otherathenaintel.comhost (Olympus onapp,staging-appandcanary-app.athenaintel.comand its previews among them), localhost, your own origin and every origin you list intrustedParentOriginsare trusted as before. - A URL in
athena-config(backendBaseUrl,statewireSyncUrl,appUrl) is taken only when it ishttps:(plainhttp:only betweenlocalhost,127.0.0.1and[::1]), carries no credentials, is not one of the customer-code hosts above, and is either an Athena host (athenaintel.com) or, for a parent outsideathenaintel.com, the parent's own host or a subdomain of it (portal.example.com→portal.example.com,api.portal.example.com). Sibling hosts are refused: without a public-suffix listexample.co.ukortenant.github.iocannot be told apart from a private domain. - The first
athena-configsets the URLs. A later one cannot change a URL or add one the first left out, so a running SDK cannot be repointed. Resending the same values is fine. - A refused URL emits the new diagnostics event
auth.bridge.config_rejected(levelwarn, withfieldandreason:insecure,other_domain,customer_app_host,invalidorchanged). When the refused URL is the statewire mount, the provider stops with the existing "without a statewire sync mount" error, which now names that cause.
useQuoteFromPostMessage (mounted by AthenaChat) now stages athena-add-to-composer text only from a frame embedded directly in the page. Any origin is still accepted, but a popup, an opener or another tab can no longer stage text.
The /_athena/auth mint (an SDK app served top-level by Marathon) is unchanged.
Host changes: none for Olympus or a Marathon wrapper. A wrapper whose page lives on a customer-code host passes its origin in trustedParentOrigins; one whose backend is neither an Athena host nor under its own domain (or that runs locally on a host other than the loopback names above) passes its URLs through config (backendUrl, statewireSyncUrl, appUrl). Explicit config always wins over the bridge.
What's new in 0.26.6
Queued messages read like the message you sent. In the StatewireQueuedMessages tray (mounted by AthenaChat), an [@name](…) mention renders as a mention chip instead of the raw markdown link, and a long message clamps to two lines behind a Show more / Show less toggle. A host that passes renderContent keeps its own renderer.
What's new in 0.26.5
useAthenaVoiceSession({ session: { silent_start: true } })starts the voice agent without its opening greeting; it waits for the first participant to speak. Forwarded to agora'sPOST /api/orpheus/create-session, which needs an agora that acceptssilent_start(staging from 2026-10-05).
What's new in 0.26.4
A | inside a table cell stays in its cell when a draft is sent. The composer wrote a pipe inside a table cell bare, so the sent message (and a draft restored into the composer) read it as a cell boundary: a | b became two cells, a code span holding x|y was split, and the cells after it moved over or were dropped. A pipe inside a cell, code spans and link targets included, is now written as \|, which reads back as a literal pipe. Tables without pipes, tables written as HTML and everything outside tables are written as before.
No host code changes are needed.
What's new in 0.27.0
A failing Marathon token mint stops retrying and fails closed. Before this release, when /_athena/auth answered an error other than 404 (for example a 502 {"error":"mint_failed"}), useParentBridge let the app continue with no token and retried in the background forever (every 1 to 5 minutes). Now each mint (the first one, the refresh before the token expires, or a forced renewal) is one bounded cycle:
- A transient failure (5xx, 408, 429, a network error, or no answer within 2 s) is retried with capped exponential backoff and jitter: at most 4 requests in all, about 0.5 s, 1 s and 2 s apart.
- A refusal (401, 403, or any other 4xx) and a 2xx without a JSON body are not retried.
- A 404 still means "no Marathon bridge on this origin", and the other credential sources apply as before.
- When a cycle gives up, the SDK reports one
AthenaSdkErrortoonError. Itscodeisauth_bridge_failed, orauth_bridge_timeoutwhen the last attempt timed out. It has the HTTPstatus, the host's error string indetail.code, andcontext.attempts. The same error is on the newuseParentBridge().errorfield. The SDK starts no new cycle on its own after that, with one exception: when it still holds a token from an earlier mint, the single refresh already scheduled before that token expires still runs, as one bounded cycle.renewToken({ force: true })starts one new bounded cycle, and a successful mint clearserror. A 2xx answer without a token counts as a failed mint. - The bridge never puts an empty token in place of a real one. A renewal that gives up keeps the token that an earlier mint handed out.
AthenaProviderrenders nothing when the mint gave up and it has no other credential (token,getToken,apiKey, or a sign-in provider's token). Before, it attached the chat with no credential to the default environment. To try again, remount the provider (for example, change itskey).
New diagnostics event: auth.bridge.retry (level warn, one for each retry, with status, attempt, max_attempts and delay_ms). ParentBridgeState gains the optional error field.
No host code changes are needed. To show a clear state when the mint fails, handle auth_bridge_failed / auth_bridge_timeout in onError.
What's new in 0.26.3
A thread opens on its newest prompt, not halfway through a long agent run. When the newest turn is longer than the first page of history, the thread keeps loading until the message that started it shows, along with what came right before it.
What's new in 0.26.2
"Load earlier messages" and scrolling to the top of the thread show older history even when the oldest loaded turn is one long agent run. A page of history that lands entirely inside the first loaded turn (its tool steps) no longer uses up the request: the thread keeps paging until an earlier message appears.
What's new in 0.26.1
Source and mention chips in chat messages render their favicon/thumbnail at chip size again. The 64px prose image-thumbnail rule no longer applies to images inside not-prose chips.
What's new in 0.26.0
The composer and message bodies run on Tiptap 3. @athenaintel/react/chat-ui (the composer, the @ mention menu and the chat message body) moves from Tiptap 2 to Tiptap 3.31.3, with tiptap-markdown 0.9. Drafts, markdown and rendered messages are unchanged: the schema keeps its node and mark names, Enter after a typed 1. or - still starts a list instead of sending, and Shift+Enter still inserts a line break. An app that also imports Tiptap itself should move to Tiptap 3 as well, so its bundle carries one copy of @tiptap/core and ProseMirror.
No host code changes are needed.
What's new in 0.25.0
A refused token no longer leaves the app on "Unauthorized". Before this release, a deploy or a long idle could close the chat connection, and the reconnect sent the token the server had just refused. Nothing renewed it, so the app stayed on "Unauthorized" until a reload. Now every sign-in mode feeds one credential source, config.getToken, and each mode renews its own token:
AthenaAuthProvider(PropelAuth) andAthenaSSOProviderrenew through the sign-in provider.AthenaAuthStategains an optionalgetToken, which a custom auth provider can set too.- A Marathon-served app re-mints
/_athena/auth; an app inside a parent window asks the parent for a new token.useParentBridge()now also returnsrenewToken({ force })(theParentBridgetype). - A host
config.getTokenis called as before, with{ force: true }to renew. A statictokenand an API key never renew.
A token that expires within 30 seconds (read from the JWT exp) is renewed before it is sent, and requests that need a renewal at the same time share one. After a refusal, the chat connection and the session list restart as soon as a new token arrives, or when the tab becomes visible or comes back online. Each refusal gets at most one restart, so a credential the server keeps refusing never loops.
Every SDK request renews a refused token once. After a 401, these requests resend once with a renewed token: the session list and its actions (archive, pin, rename, read state), the account lookup, snapshot prefetch, dictation uploads, repository search, voice session creation, collab and presence token mints, mention search, asset embeds and attachment previews. A 403, a second 401, API-key requests and a static token that renewal cannot change are not retried.
CollabAuthgainsgetToken, anduseWorkspacePresenceResolverreads the current token on every call.MentionAuthContextgainsrenewAccessToken, and itsgetAccessTokenmay resolve nothing, which falls back toaccessToken.useAssetEmbedacceptsgetToken, which renews the held token.useGenericDocanduseWorkspacePresencekeep their connection when the token is renewed for the same user (the JWTsub), and rebuild it when someone else signs in, including under a hostgetToken.
No host code changes are needed.
What's new in 0.24.1
See every pull request a session links. In SessionList, a chip that counts several pull requests (3 PRs) is now a button. It opens a list of all of them, in flight first and then the most recently updated. Each row shows the title with the number, state and checks beneath it, and links to GitHub in a new tab. When every PR is in one repository, the header names it once; otherwise each row names its repository. A single pull request (#123) is still a direct link. New export: orderedPullRequests, the order the chip and the list share (its first entry is the one the chip stands for). A session now links a pull request only when it opens it, comments on it, or is asked to work on it; reading one no longer links it (#37792).
What's new in 0.24.0
Dictation is transcribed by Athena. The composer mic now records with MediaRecorder and, on Stop or Send, posts the clip to agora's POST /api/transcription/upload, which runs Deepgram Nova-3 and returns the text; the strip shows "Transcribing…" until the words land. Accuracy is Deepgram's, every language Deepgram supports works (the session lang is forwarded), and the mic is available in Firefox. There are no live interim captions. A recording stops on its own after 5 minutes and is transcribed as if Stop had been pressed (createRecordedDictationEngine({ maxDurationMs }) to change it); agora caps a clip at 10 MB and rate-limits uploads per user (20 a minute by default). Pass dictation={{ engine: createWebSpeechDictationEngine() }} to keep the browser's live Web Speech engine. Needs an agora that serves /api/transcription/upload (staging from 2026-09-24; production with the next release train).
Breaking: useDictation().stop() resolves to { text, error } (DictationStopResult) instead of a string, so a custom composer can tell a failed transcription from silence and keep the draft instead of sending it; AthenaChat does exactly that. DictationEngine gains optional flushTimeoutMs (how long stop() waits for the flush; 1.5 s live, 90 s recorded) and stoppingLabel; the controller exposes engine; DictationStrip takes stoppingLabel. New exports: createRecordedDictationEngine, useAthenaDictationEngine, DictationTranscriber, DictationTranscribeContext, RecordedDictationEngineOptions, DictationStopResult.
What's new in 0.23.3
Dictation in the composer. <AthenaChat> now has a microphone left of Send. Click it and the editor row becomes a live waveform; Stop puts the spoken words into the draft for review, Send sends them (queued while a run is live). The default engine is the browser's Web Speech API (Chrome, Edge, Safari) — no backend, no extra dependency; the mic hides itself where the engine is missing (Firefox). Pass dictation={false} to remove it, or dictation={{ lang, engine }} to set the language or plug in another speech engine (DictationEngine interface, useDictation() hook, DictationStrip component are exported for custom composers).
Composer slots. New composer prop on <AthenaChat>: toolbarStart renders after the attach button, toolbarEnd before the mic and Send, and onBeforeSend runs host bookkeeping before every submit (dictated sends included). Slot components render inside the chat's thread scope, so useAthenaRunLive() works in them; the template's model-picker portal is no longer needed.
Frontend tools bind reliably. Two fixes to the frontendTools run config: (1) tools with an execute() no longer also ride enabled_tools — doing so switched on any backend toolkit owning a tool of the same name, and the backend won the collision, so the client tool was silently dropped (a frontend notify_user ran the backend notification cascade instead); (2) declaring executable frontend tools without a recognised appId now emits an sdk.config.warning error through diagnostics and onError, because the deep-agent runtime drops every client tool for an unknown app_id without telling the client.
What's new in 0.23.2
- Assistant turns no longer draw the grey vertical rule down their left edge.
AthenaChatrenders the lead column only when the host passes anAvatarComponent; without one the reply sits flush with the thread column and is told apart from the right-aligned user bubbles by alignment alone.
What's new in 0.23.1
- Hotfix: 0.23.0 crashed
AthenaChaton mount in published apps ("The chat hit an error",Cannot read properties of undefined (reading 'status')). The message-window hook shipped in 0.23.0 readsconnectionfrom the statewire extras, which exists in@assistant-ui/react-statewire0.29 /statewire0.19 — the versions the monorepo resolves and tests against — but the package still pinned 0.28.1 / 0.18.2 for consumers. The pins now match the tested versions, and a test fails the build if they drift again.
What's new in 0.23.0
0.23.0 is the Devin-style sessions release: a session list that says what
each session asks of you, live pull-request state, a "waiting on you" strip,
wake markers, hover actions (rename / pin / archive) with skeleton loading
and hover prefetch on that list, a provider whose host UI survives thread
switches, a paper composer with a staged welcome, a 46rem reading column with
one tool-card anatomy and grouped turns by default, assistant text that keeps
its source links and read_asset citations as chips, and the AI Chat computer
template rebuilt around them. No breaking changes to the API; <AthenaChat
groupToolCalls> now defaults to true (pass false for the previous inline
tool cards) and the AI Chat template's VITE_CONVERSATIONAL_VIEW defaults on.
Shipped in this release, by pull request:
- fix(react): AthenaProvider keys only the per-thread runtime so host UI survives thread switches; new export
useAthenaStatewireThreadId(#36402) - feat(react): SessionList with attention, next action and pull-request chips; waiting-on-you banner; wake markers (#36365)
- feat(react): SessionList pin toggle via
POST /api/conversations/threads/favorite(#36371) - fix(react): pin toggle serializes same-thread writes and scopes overrides per identity (#36373)
- feat(agora,react): admin-only repository code search route +
useAthenaRepoSearch(Devin-style Ask) (#36302) - feat(agora): live pull-request state on the session list from
agent_pr_threads(Devin-style sessions) (#36306) - feat(deep-agent): the message window per viewer, rebuilt on Statewire documents and per-attach interests (#36322)
- fix(athena-app): dedupe
@assistant-uiruntime packages so an SDK upgrade cannot mount two tap contexts (#36395) - feat(react): paper composer with clear send states, Enter-to-send hint and a staged welcome screen (#36412)
- feat(react): thread reading column, scannable turns, one tool-card anatomy, anchored scroll and a quiet thinking line (#36413) — see the thread's reading column under Styling
- feat(react): one tool-card anatomy for the SDK thread —
ToolFallbackthread appearance, grouped turns by default (#36411) — see Tool cards in the thread - feat(react): SessionList hover actions with inline rename, skeleton loading, hover prefetch and tighter rhythm; props
showRename/showArchive/prefetchOnHover, thread-list staterenameThread/prefetchThread/renameAvailable/archiveAvailable/hasResolvedFirstPage(#36415) — seeSessionList - feat(agora): REST rename for session threads (
POST /api/conversations/threads/rename), the backend of the inline rename (#36409) - fix(react): assistant text keeps source links and read_asset citations as clickable chips;
TiptapTextrenders throughChatMessageBody(#36414) - fix(react): message body re-parses only the trailing block and settles streamed words — block memoization, settled streamed words, deferred fence rendering while a reply streams (#36426)
Sources and citations (#36414)
- Sources and citations render as chips in every SDK chat surface. The
static message body (
MarkdownText/ChatMessageBody, andAthenaChat's defaultTiptapText, which now renders through it instead of a live Tiptap editor per message) turns the deep agent's citation anchors into chips: a web source (<a class="mention" data-type="url" data-url="…" data-excerpt="…">, or any<a href data-excerpt>) becomes a favicon chip whose tooltip is the excerpt and which opens the page at the excerpt (#:~:text=); a read_asset citation (<a class="mention" data-type="pdf" data-id="asset_…" data-page-number=… data-excerpt="…">) becomes an asset chip deep-linked to the cited page (anchor=page/page_range) with the excerpt as its tooltip; a citation the model left unlabelled readsPage Ninstead of an empty chip. Markdown links, bare URLs andmailto:stay plain links; onlyhttp(s):,mailto:,athena-point:and app-relative Spaces hrefs pass (javascript:,ftp:,data:,//hostare stripped to their label). Every anchor click goes throughuseAthenaLinkClickHandler(linkClicks, then the asset panel for an Athena citation); modified clicks stay with the browser. Favicons are requested only for public-looking hosts.
Composer and welcome (#36412)
- Paper composer.
<AthenaChat>renders the shared composer dock'sappearance="paper"chrome: a card-toned surface (bg-card, hairlineborder, 22px radius — still bound to thecomposerBorder/composerRadiustheme keys), a visiblefocus-withinring with a soft shadow, adata-drag-active="true"tint while files hover, and a bottom rail with attach +@mention on the left and send / stop on the right. Send is an ink circle with the arrow that reads inert at 30% while the draft is empty; stop is the same circle with a filled square. The draft grows to about eight lines (12rem) before it scrolls. The first time the composer is focused in a browser it shows "Enter to send · Shift+Enter for a new line" underneath, until the first send (localStorageathena-sdk:composer-hint-seen). Enter sends and Shift+Enter breaks the line on every surface, as before. - Staged welcome. The greeting (
h2) fades and rises in, the suggestion cards stagger in at 120 ms + 70 ms × index directly above the composer (equal-width in one row from a 40rem-wide thread, a two-column grid below), and the composer arrives last when the thread mounts empty — at 360 ms behind the default four cards, behind the last card when a host passes more.prefers-reduced-motionrenders all of it in place with no motion. @athenaintel/react/chat-ui:ChatComposerDockgainsappearance("default" | "paper", default unchanged),dragActiveandbelowSurface;TiptapComposergainsappearance,onSubmittedand the handle methodopenMentionMenu();ComposerDropZonegainsonDragActiveChange. The Chrome extension and Office add-in keep the default chrome and are unaffected.
The AI Chat computer template (docker/athena-app) pinned to this version
mounts SessionList in its sidebar (hover rename / pin / archive, skeleton
loading and hover prefetch at their defaults), shows the active session's title and
attention state in its header, keeps ?session_id in step with the active
thread (restored once per page load; one history entry per switch, so Back /
Forward walk sessions), scopes the thread-loading overlay to the chat column,
and adds a Cmd/Ctrl+K command palette with Cmd/Ctrl+Shift+O (new chat) and
Cmd/Ctrl+] / Cmd/Ctrl+[ (next / previous session).
What's new in 0.22.4
Message actions catch up with Olympus Spaces: assistant replies gain a Regenerate button and a branch picker; user bubbles gain a hover-revealed Copy / Edit bar (Edit hides on read-only threads, while a run is live, or when the runtime lacks the capability) plus a branch picker; code blocks in replies get a copy button; the composer placeholder now hints at @ mentions. New optional readOnly prop on AthenaChat hides Edit and Regenerate for transcripts whose server rejects dispatch.
What's new in 0.22.3
The assistant message action bar (copy / more) no longer shifts the message on hover: it stays mounted and reveals with opacity instead of unmounting, so older replies keep their footer height. No API changes.
What's new in 0.22.2
Public voice docs, JSDoc/.d.ts and the session-creation error message now use product-neutral wording (no internal service or transport names). No API changes.
What's new in 0.22.1
Tailwind Typography colors now follow theme tokens inside SDK roots; userBubbleForeground / assistantForeground apply to message text, headings, bold, inline code and blockquotes.
What's new in 0.22.0
0.22.0 adds a read-only workspace presence feed to @athenaintel/react/collab:
useWorkspacePresence(workspaceId) (and its vanilla core
connectWorkspacePresence) subscribes to the Keryx workspace awareness room —
the same per-recipient-filtered roster the Olympus People page renders — and
resolveWorkspacePresence / useWorkspacePresenceResolver turn the ids it
carries into titles. Admin-only. See
Workspace presence. No breaking changes.
What's new in 0.21.0
0.21.0 adds the @athenaintel/react/voice entry: useAthenaVoiceSession
(room lifecycle, mic, agent audio, live transcript turns, reconnect,
StrictMode-safe) and createAthenaVoiceSession. On Marathon it is
zero-config and attributes the session to the viewer (workspace members and
external SSO viewers); elsewhere pass createSession. See
Voice Sessions. No breaking changes.
What's new in 0.20.0
0.20.0 finishes the composer unification and retires the last chat-wire URL
shapes. It is a minor release with breaking changes for consumers of the
retired root-entry composer, the parent-bridge URL fields, and the URL
defaults; <AthenaProvider> and <AthenaChat> accept the same props.
- One composer, one mention stack.
<AthenaChat>renders the sharedchat-uiTiptap composer and@mention menu — the ones the Chrome extension and the Office add-in mount — so its menu offers the asset library (Favorites, Assets, Projects, Folders) beside the Tools / Toolkits list, with the same Enter-to-send / Shift-Enter behaviour on every surface. ThementionToolsprop keeps its shape (MentionTool[], still exported from the root entry; it defaults to the provider's enabled toolkits). Quote (Cmd+L) and attachment pills compose into the send as before; a draft that is only a quote or attachments sends from Enter and the send button alike, and a submit during an attachment upload waits for the upload to land.- Removed root exports:
TiptapComposer,TiptapComposerProps,useMentionSuggestions. Import the composer from@athenaintel/react/chat-ui(TiptapComposer,useMentionSuggestions,MentionTool,MentionRootCategory). - Outgoing text is Markdown on every surface. The shared composer
serializes the draft with
tiptap-markdownconfigured as the Olympus chat composer configures it, so<AthenaChat>keeps what 0.19 kept — a pasted hyperlink sends as[text](url), bold / italic / code marks and paragraph breaks survive, a Shift-Enter break is a backslash-newline — and the Chrome extension and Office add-in gain the same (they sent plain text before). A bare URL autolinks. Mentions serialize as the chat-ui stack does: tools are[@Name](mention://tool?name=<tool_id>&description=…)(was?tool=), toolkits[@Name](mention://toolkit?name=<toolkit_id>&description=…)(was?id=…&icon=…), and assets[@Title](/dashboard/spaces?asset_ids=<id>&assetType=<type>). A host that parses the outgoing text for the old query keys must readname. The draft mirrored into the composer runtime is that Markdown too, and a restore (a send refused during reconnect,useAppendToComposer) parses it back — mention links become chips again.serializeDraft(editor)is exported from@athenaintel/react/chat-ui;plainTextToTiptapContentis gone (the restore path parses Markdown now). @athenaintel/react/chat-ui:TiptapComposergainstools(a host's own Tools / Toolkits list in place of the server catalog),fileIntake({ onPaste, onDrop? }— a host's own intake for pasted and dropped files; withoutonDropthe composer leaves drops to an enclosing drop zone) andwaitForHostUploads(awaited before every submit, so a host-owned upload lands before the send);useMentionSuggestionsgainstools.interceptSubmitnow runs before the empty-draft check and may receive an empty string (a host composing extras of its own into the send) — an interceptor that only handles text returnsfalsefor it. A paste that carries meaningful text beside a bitmap (an Excel, Sheets or Word copy) now pastes the text on every surface instead of attaching the screenshot; a copied image file's own name (Finder, Explorer) is not such text.resolveComposerActionandComposerSendOrStopdrop thecanQueueoption — statewire always hosts a queue, so a mid-run draft always queues.@athenaintel/react/styles.cssstyles the@chips (.mention-chip) from the theme tokens, so they follow a dark theme and render in message text without a mounted composer.
- Removed root exports:
- Bare-origin URL defaults. The
environmentpresets are origins:apiUrlis the Iris host (https://iris.prd.athenaintel.com,https://iris.stg.athenaintel.com; its origin derives the/v2/threadsstatewire mount) andbackendUrlis the Agora REST base (https://api.athenaintel.com,https://staging-api.athenaintel.com).deriveAthenaApiUrlreturns the Iris origin without/api/chat;deriveAthenaAppUrlno longer recognizes the retiredsync.athenaintel.com/staging-sync.athenaintel.comhosts; anduseAthenaConfig().backendUrlis the bare base — the provider trims a historical…/api/assistant-uior…/api/v0value you pass explicitly before publishing it. - Parent bridge.
useParentBridge()/ParentBridgeStatedrop the chat-wireapiUrlandbackendUrl; a host mintsstatewireSyncUrl,backendBaseUrlandappUrl(Marathon has since 0.17). Those two fields in anathena-configmessage or a/_athena/authbody are ignored. The newhostAnsweredfield reports that a host answered the bridge at all, and<AthenaProvider>throws when a host answered without mintingstatewireSyncUrland neitherstatewireSyncUrlnorapiUrlis configured — a host's credential is never sent to the environment preset. Deploy order: a Marathon older than 2026-08-29 (#32053) mints no statewire mount; update it before an app on 0.20.0 is served from it. - Dependencies.
@assistant-ui/react-langgraphandzodare no longer dependencies (nothing in the SDK imported them after the retired composer and transport went).
What's new in 0.19.0
0.19.0 is a minor release with no breaking changes: every export that
shipped in 0.18.0 is still exported under the same name from the same entry,
and <AthenaProvider> / <AthenaChat> accept the same props. Bump the pin
and re-install; there is nothing to migrate.
- Windowed history on every surface. Each attach now declares a message
window (
ATHENA_SDK_MESSAGE_WINDOW, 40 messages, sent as thex-athena-deep-agent-windowhello), so a long thread's first snapshot carries the transcript's tail instead of every message.<AthenaChat>mounts the newStatewireHistoryEdgerow above the messages — "N earlier messages", paged in as you scroll near the top (or on click) while the reading position is kept — and cut tool outputs are fetched back for the cards that show them. Hosts composing their own thread mountStatewireHistoryEdgeinChatThreadShell'sbeforeMessagesslot (also exported from@athenaintel/react/chat-uiand/chat-ui/statewire) and read the tool-output store throughuseStatewireToolOutputs. Editing a message below the loaded window resubmits with an anchoredrun/edit. - One approval card, queue tray, and continue affordance.
StatewireApprovalCard(renderDetails,isHandledElsewhere,readOnly),StatewireQueuedMessages(renderContent,renderEditor,autoContinue,onEvent), and the continue / run-ended affordances are the single implementations Olympus, the Chrome extension, and the Office add-in mount; the headlessuseStatewireApprovalRequestandHITL_PENDING_RESUME_GRACE_MSare exported for hosts with their own decision UI. New lean entry:@athenaintel/react/chat-ui/statewire. - One tool display registry, result classifier, and fallback.
getToolDisplayMeta,classifyToolResult,isToolResultSuccess,describeToolArgs,resultMessage,toolActiveLabel,toolCompletedLabel(re-exported from@athenaintel/agent-runtime),TOOL_ICONS/toolIcon, theToolFallbackhost seams (ToolFallbackNotice,ToolFallbackOptions,ToolFallbackContext, …),CopyToolSpecButton,InstallPackageToolUI, andNestedPtcComponentsProvider/useNestedPtcComponents. New lean entry:@athenaintel/react/tools. - Grouped activity card and timeline.
SuperGroupingCard,SuperGroupingFinalText,SuperGroupingProvider,CollapsibleGroup,buildActivityTimeline,useActivityExpansion,createToolBasedGroupingFunction,TOOLKIT_THEMES, and their types are exported;<AthenaChat groupToolCalls>renders through them. New lean entry:@athenaintel/react/super-grouping. rejectedis a first-class tool-result state. A gated tool call the user refused classifies as{ state: 'rejected' }(ToolResultClassification) instead of an error, and reads as rejected on the tool card and in the grouped timeline.- Diagnostics instead of console noise. Composer, tool-UI, and mention
failures now emit
composer.attachment.error,composer.cancel.error,tools.schema.warning,tools.debug, andmentions.fetch.erroron the diagnostics bus (onDiagnostic/onError,__ATHENA_SDK__.diagnostics) rather than throughconsole.*. config.getTokenmay be called with{ force: true }(AthenaStatewireTokenGetter) right after the statewire host refused the previous credential; a getter that caches should mint a fresh token then. Existing zero-argument getters keep working unchanged.- Dependencies. The assistant-ui family is pinned exactly on one release
train (
@assistant-ui/react0.15.17,@assistant-ui/react-statewire0.24.0,assistant-stream0.3.41,statewire0.14.0); the SDK's internalzodmoved to v4 (it is not part of the public types).
Moved, not renamed: ToolCard, ExpandableSection, normalizeResult,
truncate, formatToolName, and ToolStatus now live in the tool
primitives module and are exported from both @athenaintel/react (as before)
and @athenaintel/react/tools.
Migrating from 0.17
0.18.0 deletes the legacy assistant-transport leg. Every Athena chat client
now speaks the native statewire wire, and the TAG:JSON transport over Iris
/api/chat is being removed server-side, so the SDK no longer offers it.
Removed from <AthenaProvider>:
transportprop andconfig.transport— statewire is the only lane.customToolConfigs— it only ever reached the legacy leg (the deep-agent runtime has nocustom_tool_configsreader; MCP configuration is the parsedmcp_server_configskey).agentno longer accepts named agents (athena_assist_agent, …). The only accepted shape iscollab_agent:<asset_id>; anything else throws with a message naming the prop and the accepted shape. Omit the prop to run the Athena deep agent. Validate ahead of time withresolveAgentProp({ agent }).
Removed exports:
useAthenaRuntime,AthenaRuntimeConfig,DEFAULT_API_URL,DEFAULT_BACKEND_URL— useuseAthenaStatewireRuntime/buildStatewireRunConfigfor a custom chat surface.ATHENA_TRANSPORTS,DEFAULT_ATHENA_TRANSPORT,resolveAthenaTransport,allowsMidRunSend,AthenaTransport,ResolvedAthenaTransport, and thetransportfield onAthenaConfig(useAthenaConfig()).convertLangChainToThreadMessages,autoCloseInFlightSubgraphMessages,PTC_SUBCALL_DID_NOT_COMPLETE_MESSAGE— the LangChain message converter existed only for the legacy wire.StatewireLegacyReadOnlyBanner(rendered nothing since 0.17) and the no-oponLegacyReadOnlyoption ofuseAthenaStatewireRuntime. The residuallegacy_thread_read_only409 still surfaces throughStatewireErrorBanner/useAthenaStatewireLifecycle.useAthenaThreadListAdapter(the assistant-ui remote-thread-list adapter).useRefreshThreadList,useAthenaThreadId, andAthenaThreadIdContextremain and read the statewire thread-list state.- Error codes
stream_failedandconfig_ignored; diagnostics eventsstream.connectedandstream.error.sdk.transportstill fires (alwaysstatewire).
Unchanged: apiUrl/backendUrl (explicit, host-minted, or from
environment) — apiUrl's origin still derives the statewire sync mount when
statewireSyncUrl is not set, and backendUrl is still the Agora REST base
for thread lists, uploads, and asset embeds.
Collab Agents
A collab agent is an agent configuration authored in Athena (prompt,
model, toolkits, behavior) and addressed as collab_agent:<asset_id>. Point
the provider at one and the chat runs as that agent:
<AthenaProvider
agent="collab_agent:asset_432af46f-293d-480b-a518-30b1f42a9ef7"
channel="askbob_web"
>
<AthenaChat />
</AthenaProvider>Copy the snippet with real ids from the agent's Channels tab in Athena.
channel is optional. It selects a channel override layer — a built-in
channel (email, sms, …) or a custom channel defined on that agent — so one
agent can present different prompts, models, and tools per surface. Omit it to
run the agent's base configuration.
Requirements
- The ref is the only accepted
agentshape.collab_agent:<asset_id>selects the agent; omittingagent(or a blank string) runs the Athena deep agent; any other value throws at render (resolveAgentPropis the same check, exported for hosts that validate configuration up front). - Publish the agent. Resolution reads the published snapshot, not the
live draft, so a channel that exists only in the editor is rejected with
collab_agent_channel_unknown. - The acting user needs VIEW access to the agent asset (admins bypass). Running as an agent exposes its prompt and tool policy, so the same gate that governs opening the asset governs running it.
Do not also pass model, systemPrompt, or tools
Request keys override the agent definition, so anything you pass here silently replaces what the agent's author configured — the run succeeds and returns a plausible answer using your config instead of theirs.
// ❌ the agent's prompt, model, and tools are all discarded
<AthenaProvider
agent="collab_agent:asset_1234"
model="claude-opus-4-6"
systemPrompt="You are a helpful assistant."
tools={['web_search_browse_toolkit']}
/>
// ✅ the agent's own configuration wins
<AthenaProvider agent="collab_agent:asset_1234" />Pass them only when you deliberately want to override the agent — an explicit
model is honoured as a caller override, with the agent's model as the default
beneath it.
Failure codes
A refused selection surfaces as an AthenaSdkError with
code: 'collab_agent_rejected'; the specific reason below rides in the error's
detail. Subscribe with onError (see
Debugging) rather than guessing from messages:
| Code | Meaning |
|---|---|
| collab_agent_not_found | No such asset, or it is not a collab_agent |
| collab_agent_forbidden | The acting user lacks VIEW on the asset |
| collab_agent_channel_unknown | No such channel (the message lists the known ones) |
| collab_agent_channel_disabled | The channel exists but is toggled off |
| collab_agent_channel_kind_mismatch | A voice channel was selected for a text run |
| collab_agent_runs_disabled | The deployment has the SDK seam turned off |
Authentication
Same-origin SSO defaults
import { AthenaSSOProvider } from '@athenaintel/react/auth';
import { AthenaChat, AthenaProvider } from '@athenaintel/react';
function App() {
return (
<AthenaSSOProvider>
<AthenaProvider enableThreadList>
<AthenaChat />
</AthenaProvider>
</AthenaSSOProvider>
);
}By default, AthenaSSOProvider uses same-origin SSO endpoints:
/api/sso/userinfo/api/sso/initiate/api/sso/logout
This makes the SSO URLs optional when your frontend is reverse-proxied with the backend.
Cross-origin SSO backend
import { AthenaSSOProvider } from '@athenaintel/react/auth';
<AthenaSSOProvider ssoBaseUrl="https://api.example.com">
<AthenaProvider
config={{
backendUrl: 'https://api.example.com',
apiUrl: 'https://iris.example.com',
trustedParentOrigins: ['https://app.example.com'],
}}
enableThreadList
>
