@athenaintel/react 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +225 -2
  2. package/dist/agent-runtime/src/client-tool-bridge.cjs +20 -2
  3. package/dist/agent-runtime/src/client-tool-bridge.cjs.map +1 -1
  4. package/dist/agent-runtime/src/client-tool-bridge.d.ts +9 -4
  5. package/dist/agent-runtime/src/client-tool-bridge.js +20 -2
  6. package/dist/agent-runtime/src/client-tool-bridge.js.map +1 -1
  7. package/dist/agent-runtime/src/client-tools.cjs.map +1 -1
  8. package/dist/agent-runtime/src/client-tools.d.ts +9 -0
  9. package/dist/agent-runtime/src/client-tools.js.map +1 -1
  10. package/dist/agent-ui/AgentTarget.cjs +11 -0
  11. package/dist/agent-ui/AgentTarget.cjs.map +1 -0
  12. package/dist/agent-ui/AgentTarget.d.ts +17 -0
  13. package/dist/agent-ui/AgentTarget.js +11 -0
  14. package/dist/agent-ui/AgentTarget.js.map +1 -0
  15. package/dist/agent-ui/AgentUiLayer.cjs +387 -0
  16. package/dist/agent-ui/AgentUiLayer.cjs.map +1 -0
  17. package/dist/agent-ui/AgentUiLayer.d.ts +40 -0
  18. package/dist/agent-ui/AgentUiLayer.js +387 -0
  19. package/dist/agent-ui/AgentUiLayer.js.map +1 -0
  20. package/dist/agent-ui/contract.cjs +104 -0
  21. package/dist/agent-ui/contract.cjs.map +1 -0
  22. package/dist/agent-ui/contract.d.ts +113 -0
  23. package/dist/agent-ui/contract.js +104 -0
  24. package/dist/agent-ui/contract.js.map +1 -0
  25. package/dist/agent-ui/driver.cjs +335 -0
  26. package/dist/agent-ui/driver.cjs.map +1 -0
  27. package/dist/agent-ui/driver.d.ts +66 -0
  28. package/dist/agent-ui/driver.js +335 -0
  29. package/dist/agent-ui/driver.js.map +1 -0
  30. package/dist/agent-ui/index.cjs +33 -0
  31. package/dist/agent-ui/index.cjs.map +1 -0
  32. package/dist/agent-ui/index.d.ts +19 -0
  33. package/dist/agent-ui/index.js +33 -0
  34. package/dist/agent-ui/index.js.map +1 -0
  35. package/dist/agent-ui/pace.cjs +121 -0
  36. package/dist/agent-ui/pace.cjs.map +1 -0
  37. package/dist/agent-ui/pace.d.ts +66 -0
  38. package/dist/agent-ui/pace.js +121 -0
  39. package/dist/agent-ui/pace.js.map +1 -0
  40. package/dist/agent-ui/screen.cjs +471 -0
  41. package/dist/agent-ui/screen.cjs.map +1 -0
  42. package/dist/agent-ui/screen.d.ts +97 -0
  43. package/dist/agent-ui/screen.js +471 -0
  44. package/dist/agent-ui/screen.js.map +1 -0
  45. package/dist/agent-ui/secret-fields.cjs +50 -0
  46. package/dist/agent-ui/secret-fields.cjs.map +1 -0
  47. package/dist/agent-ui/secret-fields.d.ts +15 -0
  48. package/dist/agent-ui/secret-fields.js +50 -0
  49. package/dist/agent-ui/secret-fields.js.map +1 -0
  50. package/dist/agent-ui/stage.cjs +196 -0
  51. package/dist/agent-ui/stage.cjs.map +1 -0
  52. package/dist/agent-ui/stage.d.ts +110 -0
  53. package/dist/agent-ui/stage.js +196 -0
  54. package/dist/agent-ui/stage.js.map +1 -0
  55. package/dist/agent-ui/tools.cjs +448 -0
  56. package/dist/agent-ui/tools.cjs.map +1 -0
  57. package/dist/agent-ui/tools.d.ts +103 -0
  58. package/dist/agent-ui/tools.js +448 -0
  59. package/dist/agent-ui/tools.js.map +1 -0
  60. package/dist/agent-ui/use-agent-target.cjs +63 -0
  61. package/dist/agent-ui/use-agent-target.cjs.map +1 -0
  62. package/dist/agent-ui/use-agent-target.d.ts +27 -0
  63. package/dist/agent-ui/use-agent-target.js +63 -0
  64. package/dist/agent-ui/use-agent-target.js.map +1 -0
  65. package/dist/agent-ui/use-agent-ui.cjs +15 -0
  66. package/dist/agent-ui/use-agent-ui.cjs.map +1 -0
  67. package/dist/agent-ui/use-agent-ui.d.ts +24 -0
  68. package/dist/agent-ui/use-agent-ui.js +15 -0
  69. package/dist/agent-ui/use-agent-ui.js.map +1 -0
  70. package/dist/chat/AthenaChat.cjs +19 -4
  71. package/dist/chat/AthenaChat.cjs.map +1 -1
  72. package/dist/chat/AthenaChat.d.ts +13 -0
  73. package/dist/chat/AthenaChat.js +19 -4
  74. package/dist/chat/AthenaChat.js.map +1 -1
  75. package/dist/chat/StatewireClientToolBridge.cjs +74 -1
  76. package/dist/chat/StatewireClientToolBridge.cjs.map +1 -1
  77. package/dist/chat/StatewireClientToolBridge.d.ts +16 -1
  78. package/dist/chat/StatewireClientToolBridge.js +75 -2
  79. package/dist/chat/StatewireClientToolBridge.js.map +1 -1
  80. package/dist/chat/StatewireClientToolConsentCard.cjs +66 -0
  81. package/dist/chat/StatewireClientToolConsentCard.cjs.map +1 -0
  82. package/dist/chat/StatewireClientToolConsentCard.d.ts +25 -0
  83. package/dist/chat/StatewireClientToolConsentCard.js +66 -0
  84. package/dist/chat/StatewireClientToolConsentCard.js.map +1 -0
  85. package/dist/chat/chat-voice-controls.cjs +5 -2
  86. package/dist/chat/chat-voice-controls.cjs.map +1 -1
  87. package/dist/chat/chat-voice-controls.d.ts +6 -1
  88. package/dist/chat/chat-voice-controls.js +6 -3
  89. package/dist/chat/chat-voice-controls.js.map +1 -1
  90. package/dist/chat/client-tool-write-consent.cjs +122 -0
  91. package/dist/chat/client-tool-write-consent.cjs.map +1 -0
  92. package/dist/chat/client-tool-write-consent.d.ts +79 -0
  93. package/dist/chat/client-tool-write-consent.js +122 -0
  94. package/dist/chat/client-tool-write-consent.js.map +1 -0
  95. package/dist/chat/statewire-client-tools.cjs +30 -5
  96. package/dist/chat/statewire-client-tools.cjs.map +1 -1
  97. package/dist/chat/statewire-client-tools.d.ts +34 -2
  98. package/dist/chat/statewire-client-tools.js +30 -5
  99. package/dist/chat/statewire-client-tools.js.map +1 -1
  100. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +13 -0
  101. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
  102. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.d.ts +12 -0
  103. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +13 -0
  104. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
  105. package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs +2 -1
  106. package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs.map +1 -1
  107. package/dist/chat-ui/mentions/suggestions/data/sources/index.js +3 -2
  108. package/dist/chat-ui/mentions/suggestions/data/sources/index.js.map +1 -1
  109. package/dist/chat-ui/mentions/use-asset-icon-app-url.cjs +2 -1
  110. package/dist/chat-ui/mentions/use-asset-icon-app-url.cjs.map +1 -1
  111. package/dist/chat-ui/mentions/use-asset-icon-app-url.d.ts +2 -1
  112. package/dist/chat-ui/mentions/use-asset-icon-app-url.js +2 -1
  113. package/dist/chat-ui/mentions/use-asset-icon-app-url.js.map +1 -1
  114. package/dist/chat-ui/messages/message-components.cjs +1 -1
  115. package/dist/chat-ui/messages/message-components.cjs.map +1 -1
  116. package/dist/chat-ui/messages/message-components.d.ts +2 -2
  117. package/dist/chat-ui/messages/message-components.js +1 -1
  118. package/dist/chat-ui/messages/message-components.js.map +1 -1
  119. package/dist/chat-ui/statewire.cjs +4 -0
  120. package/dist/chat-ui/statewire.cjs.map +1 -1
  121. package/dist/chat-ui/statewire.d.ts +2 -0
  122. package/dist/chat-ui/statewire.js +4 -0
  123. package/dist/chat-ui/statewire.js.map +1 -1
  124. package/dist/index.cjs +12 -0
  125. package/dist/index.cjs.map +1 -1
  126. package/dist/index.d.ts +12 -2
  127. package/dist/index.js +13 -1
  128. package/dist/index.js.map +1 -1
  129. package/dist/provider/AthenaProvider.cjs +59 -11
  130. package/dist/provider/AthenaProvider.cjs.map +1 -1
  131. package/dist/provider/AthenaProvider.d.ts +25 -1
  132. package/dist/provider/AthenaProvider.js +55 -7
  133. package/dist/provider/AthenaProvider.js.map +1 -1
  134. package/dist/react/package.json.cjs +1 -1
  135. package/dist/react/package.json.js +1 -1
  136. package/dist/styles.css +1 -1
  137. package/package.json +6 -1
@@ -15,9 +15,14 @@ export declare function useComposerChatVoice({ options }: {
15
15
  active: boolean;
16
16
  audioRef: import("react").RefObject<HTMLAudioElement | null>;
17
17
  };
18
- export declare function ChatVoiceButton({ voice, disabled, disabledReason, }: {
18
+ export declare function ChatVoiceButton({ voice, disabled, disabledReason, appearance, }: {
19
19
  voice: UseAthenaChatVoiceReturn;
20
20
  disabled: boolean;
21
+ /**
22
+ * `send`: drawn as the composer's Send (an ink circle with a waveform), where it stands in for Send
23
+ * while the draft is empty (`AthenaChatComposerSlots.voiceInSend`). `icon` (default): a plain icon.
24
+ */
25
+ appearance?: 'icon' | 'send';
21
26
  /**
22
27
  * Why a voice session can't start now (Athena can't be reached): shown as the tooltip of a
23
28
  * control that stays focusable and starts nothing. A session already connecting or live keeps its
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useAui } from "@assistant-ui/react";
3
- import { Loader2Icon, MicOffIcon, MicIcon, PhoneIcon, HeadphonesIcon, XIcon } from "lucide-react";
3
+ import { Loader2Icon, MicOffIcon, MicIcon, PhoneIcon, AudioLinesIcon, HeadphonesIcon, XIcon } from "lucide-react";
4
4
  import { useRef, useMemo, useEffect } from "react";
5
5
  import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
6
6
  import { createDeepAgentVoiceLiveKitAdapter } from "../voice/livekit.js";
@@ -71,7 +71,8 @@ function useComposerChatVoice({ options }) {
71
71
  function ChatVoiceButton({
72
72
  voice,
73
73
  disabled,
74
- disabledReason = null
74
+ disabledReason = null,
75
+ appearance = "icon"
75
76
  }) {
76
77
  const live = voice.phase === "live";
77
78
  const connecting = voice.phase === "connecting";
@@ -87,8 +88,10 @@ function ChatVoiceButton({
87
88
  "aria-pressed": live && !voice.micMuted,
88
89
  "aria-disabled": unavailable || void 0,
89
90
  "data-unavailable": unavailable || void 0,
91
+ ...appearance === "send" ? { variant: "default" } : {},
90
92
  className: cn(
91
93
  "aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30",
94
+ appearance === "send" && "aui-composer-voice-send",
92
95
  unavailable && "cursor-not-allowed opacity-30 hover:bg-transparent"
93
96
  ),
94
97
  disabled: !unavailable && !live && !connecting && disabled,
@@ -99,7 +102,7 @@ function ChatVoiceButton({
99
102
  else if (moveHere) void voice.transferHere();
100
103
  else void voice.start();
101
104
  },
102
- children: connecting ? /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }) : live ? voice.micMuted ? /* @__PURE__ */ jsx(MicOffIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(MicIcon, { className: "size-4" }) : moveHere ? /* @__PURE__ */ jsx(PhoneIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(HeadphonesIcon, { className: "size-4" })
105
+ children: connecting ? /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }) : live ? voice.micMuted ? /* @__PURE__ */ jsx(MicOffIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(MicIcon, { className: "size-4" }) : moveHere ? /* @__PURE__ */ jsx(PhoneIcon, { className: "size-4" }) : appearance === "send" ? /* @__PURE__ */ jsx(AudioLinesIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(HeadphonesIcon, { className: "size-4" })
103
106
  }
104
107
  );
105
108
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chat-voice-controls.js","sources":["../../src/chat/chat-voice-controls.tsx"],"sourcesContent":["import { useAui } from '@assistant-ui/react';\nimport {\n describeVoiceLiveElsewhere,\n playVoiceMoveEarcon,\n useVoiceElapsed,\n useVoiceMovedNotice,\n VOICE_CONTINUE_BY_VOICE_LABEL,\n VOICE_LIVE_TYPED_HINT,\n VOICE_MOVE_CALL_HERE_LABEL,\n VOICE_MOVE_HERE_ACTION,\n} from '@athenaintel/agent-runtime';\nimport { HeadphonesIcon, Loader2Icon, MicIcon, MicOffIcon, PhoneIcon, XIcon } from 'lucide-react';\nimport { useEffect, useMemo, useRef } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { createDeepAgentVoiceLiveKitAdapter } from '../voice/livekit';\nimport {\n useAthenaChatVoice,\n type UseAthenaChatVoiceOptions,\n type UseAthenaChatVoiceReturn,\n} from '../voice/chat';\nimport { useChatVoiceEnabled } from '../voice/use-chat-voice-enabled';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport { TooltipIconButton } from './tooltip-icon-button';\nimport { useMentionAuth } from '../chat-ui/mentions/suggestions/data/api-client';\nimport { credentialFingerprint, useCatalogPrincipal } from '../catalog/runtime-catalog';\n\n/**\n * `onCallMoved` replaces the move earcon a call plays when it moves to or\n * from this device (pass a no-op to keep it silent); by default the shared\n * one plays.\n */\nexport interface AthenaChatVoiceOptions\n extends Pick<UseAthenaChatVoiceOptions, 'model' | 'livekit' | 'onDiagnostic' | 'onCallMoved'> {}\n\n/** One controller for this composer's controls, text sends and audio sink. */\nexport function useComposerChatVoice({ options }: { options: boolean | AthenaChatVoiceOptions }) {\n const aui = useAui();\n const auth = useMentionAuth();\n const principal = useCatalogPrincipal();\n const enabled = useChatVoiceEnabled({ enabled: options !== false });\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const media = useMemo(\n () => createDeepAgentVoiceLiveKitAdapter({ audioElement: () => audioRef.current }),\n []\n );\n const config = typeof options === 'object' ? options : undefined;\n const voice = useAthenaChatVoice({\n transport: 'livekit',\n livekit: config?.livekit ?? media,\n model: config?.model,\n onDiagnostic:\n config?.onDiagnostic ??\n ((entry) => athenaDiagnostics.emit('voice.chat', { data: { ...entry } })),\n onCallMoved: config?.onCallMoved ?? playVoiceMoveEarcon,\n });\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n const { end, syncProfile } = voice;\n const identity = JSON.stringify([\n auth.backendUrl,\n principal,\n auth.apiKey ? credentialFingerprint({ credential: auth.apiKey }) : null,\n ]);\n const previousScope = useRef({ identity, workspaceId: auth.workspaceId });\n useEffect(() => {\n const previous = previousScope.current;\n const changed =\n previous.identity !== identity ||\n Boolean(\n previous.workspaceId && auth.workspaceId && previous.workspaceId !== auth.workspaceId\n );\n previousScope.current = {\n identity,\n workspaceId:\n previous.identity === identity\n ? auth.workspaceId ?? previous.workspaceId\n : auth.workspaceId,\n };\n if (active && (changed || !enabled)) end();\n }, [identity, auth.workspaceId, active, enabled, end]);\n\n // Model/agent/toolkit/mode picks live in the ambient model context. Read it\n // through the controller at send time, with the same debounce as other hosts.\n const live = voice.phase === 'live';\n useEffect(() => {\n if (!live) return;\n let timer: ReturnType<typeof setTimeout>;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(syncProfile, 800);\n };\n schedule();\n const unsubscribe = aui.modelContext.subscribe?.(schedule);\n return () => {\n clearTimeout(timer);\n unsubscribe?.();\n };\n }, [aui, live, syncProfile]);\n return { voice, enabled, active, audioRef };\n}\n\nexport function ChatVoiceButton({\n voice,\n disabled,\n disabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n disabled: boolean;\n /**\n * Why a voice session can't start now (Athena can't be reached): shown as the tooltip of a\n * control that stays focusable and starts nothing. A session already connecting or live keeps its\n * Cancel and Mute.\n */\n disabledReason?: string | null;\n}) {\n const live = voice.phase === 'live';\n const connecting = voice.phase === 'connecting';\n const unavailable = Boolean(disabledReason) && !live && !connecting;\n // The conversation's call is live on another of the person's devices.\n const moveHere = !live && !connecting && voice.liveElsewhere !== null;\n const label = connecting\n ? 'Cancel voice connection'\n : live\n ? voice.micMuted\n ? 'Unmute microphone'\n : 'Mute microphone'\n : moveHere\n ? VOICE_MOVE_CALL_HERE_LABEL\n : 'Start voice session';\n return (\n <TooltipIconButton\n type=\"button\"\n tooltip={unavailable && disabledReason ? disabledReason : label}\n aria-label={label}\n aria-pressed={live && !voice.micMuted}\n aria-disabled={unavailable || undefined}\n data-unavailable={unavailable || undefined}\n className={cn(\n 'aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30',\n unavailable && 'cursor-not-allowed opacity-30 hover:bg-transparent',\n )}\n disabled={!unavailable && !live && !connecting && disabled}\n onClick={() => {\n if (unavailable) return;\n if (connecting) voice.end();\n else if (live) voice.setMicMuted(!voice.micMuted);\n else if (moveHere) void voice.transferHere();\n else void voice.start();\n }}\n >\n {connecting ? (\n <Loader2Icon className=\"size-4 animate-spin\" />\n ) : live ? (\n voice.micMuted ? (\n <MicOffIcon className=\"size-4\" />\n ) : (\n <MicIcon className=\"size-4\" />\n )\n ) : moveHere ? (\n <PhoneIcon className=\"size-4\" />\n ) : (\n <HeadphonesIcon className=\"size-4\" />\n )}\n </TooltipIconButton>\n );\n}\n\n/**\n * A host's \"Continue by voice\" offer for the open conversation: a handover the\n * person sent in voice mode landed here. `onDone` runs once they answer it —\n * took it, dismissed it, or typed instead — so the host clears the offer.\n */\nexport interface AthenaChatContinueByVoice {\n onDone: () => void;\n}\n\n/**\n * The one-tap \"Continue by voice\" above the composer: it starts a call whose\n * model opens by saying where things stand. Shown while voice is idle here and\n * no call is live on another of the person's devices (\"Move here\" covers that).\n * Nothing starts the microphone without the tap.\n */\nexport function ChatVoiceContinueOffer({\n voice,\n offer,\n disabled = false,\n disabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n offer: AthenaChatContinueByVoice | null;\n /** Starting would capture the microphone now (dictation, an upload, no channel). */\n disabled?: boolean;\n /** Why voice can't start now (Athena can't be reached), as the action's tooltip. */\n disabledReason?: string | null;\n}) {\n if (!offer || voice.phase !== 'idle' || voice.liveElsewhere !== null) return null;\n return (\n <div className=\"aui-chat-voice-continue flex items-center gap-1 px-2 pb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"xs\"\n title={disabledReason ?? undefined}\n disabled={disabled || Boolean(disabledReason)}\n onClick={() => {\n offer.onDone();\n void voice.start({ opening: 'recap' });\n }}\n >\n <MicIcon aria-hidden=\"true\" />\n {VOICE_CONTINUE_BY_VOICE_LABEL}\n </Button>\n <TooltipIconButton\n type=\"button\"\n tooltip=\"Not now\"\n aria-label=\"Not now\"\n className=\"size-6 rounded-md p-0\"\n onClick={offer.onDone}\n >\n <XIcon className=\"size-3\" />\n </TooltipIconButton>\n </div>\n );\n}\n\nexport function ChatVoiceStatus({\n voice,\n offerMove = false,\n moveDisabled = false,\n moveDisabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n /** Whether this composer may take a call from another device (its voice is enabled). */\n offerMove?: boolean;\n /** Moving the call here would capture the microphone now (dictation, an upload, no channel). */\n moveDisabled?: boolean;\n /** Why the call can't move here now (Athena can't be reached), as the action's tooltip. */\n moveDisabledReason?: string | null;\n}) {\n const elapsed = useVoiceElapsed(voice.liveSince);\n const movedNotice = useVoiceMovedNotice(voice.movedTo);\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n // \"Live call on your phone · Move here\", or briefly after this composer's call moved away,\n // \"Call moved to your phone\".\n const elsewhere = offerMove && !active && movedNotice === null ? voice.liveElsewhere : null;\n if (!active && !voice.error && !voice.handoffTask && movedNotice === null && elsewhere === null)\n return null;\n const moveUnavailable = moveDisabled || Boolean(moveDisabledReason);\n return (\n <div className=\"aui-chat-voice-status flex flex-col gap-1 px-2 pb-2 text-xs text-muted-foreground\">\n {/* No end control of its own: the composer's Stop ends the session\n (and any run), so there is one way to stop everything. */}\n {active && (\n <span role=\"status\">\n {voice.phase === 'connecting'\n ? 'Connecting voice…'\n : `Voice ${voice.micMuted ? 'muted' : 'live'}${elapsed ? ` · ${elapsed}` : ''}`}\n </span>\n )}\n {voice.phase === 'live' && <span>{VOICE_LIVE_TYPED_HINT}</span>}\n {voice.activeTask && <span>Athena is working: {voice.activeTask.text}</span>}\n {voice.handoffTask && (\n <span>Voice ended. Athena is still working: {voice.handoffTask.text}</span>\n )}\n {voice.notice && <span role=\"status\">{voice.notice}</span>}\n {voice.error && (\n <span role=\"alert\" className=\"text-destructive\">\n {voice.error}\n </span>\n )}\n {movedNotice !== null && <span role=\"status\">{movedNotice}</span>}\n {elsewhere !== null && (\n <span className=\"aui-chat-voice-elsewhere flex items-center gap-1\">\n <span className=\"min-w-0 truncate\">{describeVoiceLiveElsewhere(elsewhere.surface)}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n title={moveDisabledReason ?? undefined}\n disabled={moveUnavailable}\n onClick={() => void voice.transferHere()}\n className=\"shrink-0 font-medium text-foreground underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {VOICE_MOVE_HERE_ACTION}\n </button>\n </span>\n )}\n </div>\n );\n}\n"],"names":["useAthenaChatVoice"],"mappings":"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"}
1
+ {"version":3,"file":"chat-voice-controls.js","sources":["../../src/chat/chat-voice-controls.tsx"],"sourcesContent":["import { useAui } from '@assistant-ui/react';\nimport {\n describeVoiceLiveElsewhere,\n playVoiceMoveEarcon,\n useVoiceElapsed,\n useVoiceMovedNotice,\n VOICE_CONTINUE_BY_VOICE_LABEL,\n VOICE_LIVE_TYPED_HINT,\n VOICE_MOVE_CALL_HERE_LABEL,\n VOICE_MOVE_HERE_ACTION,\n} from '@athenaintel/agent-runtime';\nimport {\n AudioLinesIcon,\n HeadphonesIcon,\n Loader2Icon,\n MicIcon,\n MicOffIcon,\n PhoneIcon,\n XIcon,\n} from 'lucide-react';\nimport { useEffect, useMemo, useRef } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { createDeepAgentVoiceLiveKitAdapter } from '../voice/livekit';\nimport {\n useAthenaChatVoice,\n type UseAthenaChatVoiceOptions,\n type UseAthenaChatVoiceReturn,\n} from '../voice/chat';\nimport { useChatVoiceEnabled } from '../voice/use-chat-voice-enabled';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport { TooltipIconButton } from './tooltip-icon-button';\nimport { useMentionAuth } from '../chat-ui/mentions/suggestions/data/api-client';\nimport { credentialFingerprint, useCatalogPrincipal } from '../catalog/runtime-catalog';\n\n/**\n * `onCallMoved` replaces the move earcon a call plays when it moves to or\n * from this device (pass a no-op to keep it silent); by default the shared\n * one plays.\n */\nexport interface AthenaChatVoiceOptions\n extends Pick<UseAthenaChatVoiceOptions, 'model' | 'livekit' | 'onDiagnostic' | 'onCallMoved'> {}\n\n/** One controller for this composer's controls, text sends and audio sink. */\nexport function useComposerChatVoice({ options }: { options: boolean | AthenaChatVoiceOptions }) {\n const aui = useAui();\n const auth = useMentionAuth();\n const principal = useCatalogPrincipal();\n const enabled = useChatVoiceEnabled({ enabled: options !== false });\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const media = useMemo(\n () => createDeepAgentVoiceLiveKitAdapter({ audioElement: () => audioRef.current }),\n []\n );\n const config = typeof options === 'object' ? options : undefined;\n const voice = useAthenaChatVoice({\n transport: 'livekit',\n livekit: config?.livekit ?? media,\n model: config?.model,\n onDiagnostic:\n config?.onDiagnostic ??\n ((entry) => athenaDiagnostics.emit('voice.chat', { data: { ...entry } })),\n onCallMoved: config?.onCallMoved ?? playVoiceMoveEarcon,\n });\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n const { end, syncProfile } = voice;\n const identity = JSON.stringify([\n auth.backendUrl,\n principal,\n auth.apiKey ? credentialFingerprint({ credential: auth.apiKey }) : null,\n ]);\n const previousScope = useRef({ identity, workspaceId: auth.workspaceId });\n useEffect(() => {\n const previous = previousScope.current;\n const changed =\n previous.identity !== identity ||\n Boolean(\n previous.workspaceId && auth.workspaceId && previous.workspaceId !== auth.workspaceId\n );\n previousScope.current = {\n identity,\n workspaceId:\n previous.identity === identity\n ? auth.workspaceId ?? previous.workspaceId\n : auth.workspaceId,\n };\n if (active && (changed || !enabled)) end();\n }, [identity, auth.workspaceId, active, enabled, end]);\n\n // Model/agent/toolkit/mode picks live in the ambient model context. Read it\n // through the controller at send time, with the same debounce as other hosts.\n const live = voice.phase === 'live';\n useEffect(() => {\n if (!live) return;\n let timer: ReturnType<typeof setTimeout>;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(syncProfile, 800);\n };\n schedule();\n const unsubscribe = aui.modelContext.subscribe?.(schedule);\n return () => {\n clearTimeout(timer);\n unsubscribe?.();\n };\n }, [aui, live, syncProfile]);\n return { voice, enabled, active, audioRef };\n}\n\nexport function ChatVoiceButton({\n voice,\n disabled,\n disabledReason = null,\n appearance = 'icon',\n}: {\n voice: UseAthenaChatVoiceReturn;\n disabled: boolean;\n /**\n * `send`: drawn as the composer's Send (an ink circle with a waveform), where it stands in for Send\n * while the draft is empty (`AthenaChatComposerSlots.voiceInSend`). `icon` (default): a plain icon.\n */\n appearance?: 'icon' | 'send';\n /**\n * Why a voice session can't start now (Athena can't be reached): shown as the tooltip of a\n * control that stays focusable and starts nothing. A session already connecting or live keeps its\n * Cancel and Mute.\n */\n disabledReason?: string | null;\n}) {\n const live = voice.phase === 'live';\n const connecting = voice.phase === 'connecting';\n const unavailable = Boolean(disabledReason) && !live && !connecting;\n // The conversation's call is live on another of the person's devices.\n const moveHere = !live && !connecting && voice.liveElsewhere !== null;\n const label = connecting\n ? 'Cancel voice connection'\n : live\n ? voice.micMuted\n ? 'Unmute microphone'\n : 'Mute microphone'\n : moveHere\n ? VOICE_MOVE_CALL_HERE_LABEL\n : 'Start voice session';\n return (\n <TooltipIconButton\n type=\"button\"\n tooltip={unavailable && disabledReason ? disabledReason : label}\n aria-label={label}\n aria-pressed={live && !voice.micMuted}\n aria-disabled={unavailable || undefined}\n data-unavailable={unavailable || undefined}\n {...(appearance === 'send' ? { variant: 'default' as const } : {})}\n className={cn(\n 'aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30',\n appearance === 'send' && 'aui-composer-voice-send',\n unavailable && 'cursor-not-allowed opacity-30 hover:bg-transparent',\n )}\n disabled={!unavailable && !live && !connecting && disabled}\n onClick={() => {\n if (unavailable) return;\n if (connecting) voice.end();\n else if (live) voice.setMicMuted(!voice.micMuted);\n else if (moveHere) void voice.transferHere();\n else void voice.start();\n }}\n >\n {connecting ? (\n <Loader2Icon className=\"size-4 animate-spin\" />\n ) : live ? (\n voice.micMuted ? (\n <MicOffIcon className=\"size-4\" />\n ) : (\n <MicIcon className=\"size-4\" />\n )\n ) : moveHere ? (\n <PhoneIcon className=\"size-4\" />\n ) : appearance === 'send' ? (\n <AudioLinesIcon className=\"size-4\" />\n ) : (\n <HeadphonesIcon className=\"size-4\" />\n )}\n </TooltipIconButton>\n );\n}\n\n/**\n * A host's \"Continue by voice\" offer for the open conversation: a handover the\n * person sent in voice mode landed here. `onDone` runs once they answer it —\n * took it, dismissed it, or typed instead — so the host clears the offer.\n */\nexport interface AthenaChatContinueByVoice {\n onDone: () => void;\n}\n\n/**\n * The one-tap \"Continue by voice\" above the composer: it starts a call whose\n * model opens by saying where things stand. Shown while voice is idle here and\n * no call is live on another of the person's devices (\"Move here\" covers that).\n * Nothing starts the microphone without the tap.\n */\nexport function ChatVoiceContinueOffer({\n voice,\n offer,\n disabled = false,\n disabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n offer: AthenaChatContinueByVoice | null;\n /** Starting would capture the microphone now (dictation, an upload, no channel). */\n disabled?: boolean;\n /** Why voice can't start now (Athena can't be reached), as the action's tooltip. */\n disabledReason?: string | null;\n}) {\n if (!offer || voice.phase !== 'idle' || voice.liveElsewhere !== null) return null;\n return (\n <div className=\"aui-chat-voice-continue flex items-center gap-1 px-2 pb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"xs\"\n title={disabledReason ?? undefined}\n disabled={disabled || Boolean(disabledReason)}\n onClick={() => {\n offer.onDone();\n void voice.start({ opening: 'recap' });\n }}\n >\n <MicIcon aria-hidden=\"true\" />\n {VOICE_CONTINUE_BY_VOICE_LABEL}\n </Button>\n <TooltipIconButton\n type=\"button\"\n tooltip=\"Not now\"\n aria-label=\"Not now\"\n className=\"size-6 rounded-md p-0\"\n onClick={offer.onDone}\n >\n <XIcon className=\"size-3\" />\n </TooltipIconButton>\n </div>\n );\n}\n\nexport function ChatVoiceStatus({\n voice,\n offerMove = false,\n moveDisabled = false,\n moveDisabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n /** Whether this composer may take a call from another device (its voice is enabled). */\n offerMove?: boolean;\n /** Moving the call here would capture the microphone now (dictation, an upload, no channel). */\n moveDisabled?: boolean;\n /** Why the call can't move here now (Athena can't be reached), as the action's tooltip. */\n moveDisabledReason?: string | null;\n}) {\n const elapsed = useVoiceElapsed(voice.liveSince);\n const movedNotice = useVoiceMovedNotice(voice.movedTo);\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n // \"Live call on your phone · Move here\", or briefly after this composer's call moved away,\n // \"Call moved to your phone\".\n const elsewhere = offerMove && !active && movedNotice === null ? voice.liveElsewhere : null;\n if (!active && !voice.error && !voice.handoffTask && movedNotice === null && elsewhere === null)\n return null;\n const moveUnavailable = moveDisabled || Boolean(moveDisabledReason);\n return (\n <div className=\"aui-chat-voice-status flex flex-col gap-1 px-2 pb-2 text-xs text-muted-foreground\">\n {/* No end control of its own: the composer's Stop ends the session\n (and any run), so there is one way to stop everything. */}\n {active && (\n <span role=\"status\">\n {voice.phase === 'connecting'\n ? 'Connecting voice…'\n : `Voice ${voice.micMuted ? 'muted' : 'live'}${elapsed ? ` · ${elapsed}` : ''}`}\n </span>\n )}\n {voice.phase === 'live' && <span>{VOICE_LIVE_TYPED_HINT}</span>}\n {voice.activeTask && <span>Athena is working: {voice.activeTask.text}</span>}\n {voice.handoffTask && (\n <span>Voice ended. Athena is still working: {voice.handoffTask.text}</span>\n )}\n {voice.notice && <span role=\"status\">{voice.notice}</span>}\n {voice.error && (\n <span role=\"alert\" className=\"text-destructive\">\n {voice.error}\n </span>\n )}\n {movedNotice !== null && <span role=\"status\">{movedNotice}</span>}\n {elsewhere !== null && (\n <span className=\"aui-chat-voice-elsewhere flex items-center gap-1\">\n <span className=\"min-w-0 truncate\">{describeVoiceLiveElsewhere(elsewhere.surface)}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n title={moveDisabledReason ?? undefined}\n disabled={moveUnavailable}\n onClick={() => void voice.transferHere()}\n className=\"shrink-0 font-medium text-foreground underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {VOICE_MOVE_HERE_ACTION}\n </button>\n </span>\n )}\n </div>\n );\n}\n"],"names":["useAthenaChatVoice"],"mappings":";;;;;;;;;;;;;;;AA4CO,SAAS,qBAAqB,EAAE,WAA0D;AAC/F,QAAM,MAAM,OAAA;AACZ,QAAM,OAAO,eAAA;AACb,QAAM,YAAY,oBAAA;AAClB,QAAM,UAAU,oBAAoB,EAAE,SAAS,YAAY,OAAO;AAClE,QAAM,WAAW,OAAgC,IAAI;AACrD,QAAM,QAAQ;AAAA,IACZ,MAAM,mCAAmC,EAAE,cAAc,MAAM,SAAS,SAAS;AAAA,IACjF,CAAA;AAAA,EAAC;AAEH,QAAM,SAAS,OAAO,YAAY,WAAW,UAAU;AACvD,QAAM,QAAQA,kBAAmB;AAAA,IAC/B,WAAW;AAAA,IACX,UAAS,iCAAQ,YAAW;AAAA,IAC5B,OAAO,iCAAQ;AAAA,IACf,eACE,iCAAQ,kBACP,CAAC,UAAU,kBAAkB,KAAK,cAAc,EAAE,MAAM,EAAE,GAAG,MAAA,GAAS;AAAA,IACzE,cAAa,iCAAQ,gBAAe;AAAA,EAAA,CACrC;AACD,QAAM,SAAS,MAAM,UAAU,gBAAgB,MAAM,UAAU;AAC/D,QAAM,EAAE,KAAK,YAAA,IAAgB;AAC7B,QAAM,WAAW,KAAK,UAAU;AAAA,IAC9B,KAAK;AAAA,IACL;AAAA,IACA,KAAK,SAAS,sBAAsB,EAAE,YAAY,KAAK,OAAA,CAAQ,IAAI;AAAA,EAAA,CACpE;AACD,QAAM,gBAAgB,OAAO,EAAE,UAAU,aAAa,KAAK,aAAa;AACxE,YAAU,MAAM;AACd,UAAM,WAAW,cAAc;AAC/B,UAAM,UACJ,SAAS,aAAa,YACtB;AAAA,MACE,SAAS,eAAe,KAAK,eAAe,SAAS,gBAAgB,KAAK;AAAA,IAAA;AAE9E,kBAAc,UAAU;AAAA,MACtB;AAAA,MACA,aACE,SAAS,aAAa,WAClB,KAAK,eAAe,SAAS,cAC7B,KAAK;AAAA,IAAA;AAEb,QAAI,WAAW,WAAW,CAAC,SAAU,KAAA;AAAA,EACvC,GAAG,CAAC,UAAU,KAAK,aAAa,QAAQ,SAAS,GAAG,CAAC;AAIrD,QAAM,OAAO,MAAM,UAAU;AAC7B,YAAU,MAAM;;AACd,QAAI,CAAC,KAAM;AACX,QAAI;AACJ,UAAM,WAAW,MAAM;AACrB,mBAAa,KAAK;AAClB,cAAQ,WAAW,aAAa,GAAG;AAAA,IACrC;AACA,aAAA;AACA,UAAM,eAAc,eAAI,cAAa,cAAjB,4BAA6B;AACjD,WAAO,MAAM;AACX,mBAAa,KAAK;AAClB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,KAAK,MAAM,WAAW,CAAC;AAC3B,SAAO,EAAE,OAAO,SAAS,QAAQ,SAAA;AACnC;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,aAAa;AACf,GAcG;AACD,QAAM,OAAO,MAAM,UAAU;AAC7B,QAAM,aAAa,MAAM,UAAU;AACnC,QAAM,cAAc,QAAQ,cAAc,KAAK,CAAC,QAAQ,CAAC;AAEzD,QAAM,WAAW,CAAC,QAAQ,CAAC,cAAc,MAAM,kBAAkB;AACjE,QAAM,QAAQ,aACV,4BACA,OACA,MAAM,WACJ,sBACA,oBACF,WACA,6BACA;AACJ,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,eAAe,iBAAiB,iBAAiB;AAAA,MAC1D,cAAY;AAAA,MACZ,gBAAc,QAAQ,CAAC,MAAM;AAAA,MAC7B,iBAAe,eAAe;AAAA,MAC9B,oBAAkB,eAAe;AAAA,MAChC,GAAI,eAAe,SAAS,EAAE,SAAS,UAAA,IAAuB,CAAA;AAAA,MAC/D,WAAW;AAAA,QACT;AAAA,QACA,eAAe,UAAU;AAAA,QACzB,eAAe;AAAA,MAAA;AAAA,MAEjB,UAAU,CAAC,eAAe,CAAC,QAAQ,CAAC,cAAc;AAAA,MAClD,SAAS,MAAM;AACb,YAAI,YAAa;AACjB,YAAI,kBAAkB,IAAA;AAAA,iBACb,KAAM,OAAM,YAAY,CAAC,MAAM,QAAQ;AAAA,iBACvC,SAAU,MAAK,MAAM,aAAA;AAAA,YACzB,MAAK,MAAM,MAAA;AAAA,MAClB;AAAA,MAEC,UAAA,aACC,oBAAC,aAAA,EAAY,WAAU,uBAAsB,IAC3C,OACF,MAAM,+BACH,YAAA,EAAW,WAAU,SAAA,CAAS,wBAE9B,SAAA,EAAQ,WAAU,SAAA,CAAS,IAE5B,WACF,oBAAC,WAAA,EAAU,WAAU,SAAA,CAAS,IAC5B,eAAe,SACjB,oBAAC,gBAAA,EAAe,WAAU,SAAA,CAAS,IAEnC,oBAAC,gBAAA,EAAe,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAI3C;AAiBO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,iBAAiB;AACnB,GAOG;AACD,MAAI,CAAC,SAAS,MAAM,UAAU,UAAU,MAAM,kBAAkB,KAAM,QAAO;AAC7E,SACE,qBAAC,OAAA,EAAI,WAAU,6DACb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,OAAO,kBAAkB;AAAA,QACzB,UAAU,YAAY,QAAQ,cAAc;AAAA,QAC5C,SAAS,MAAM;AACb,gBAAM,OAAA;AACN,eAAK,MAAM,MAAM,EAAE,SAAS,SAAS;AAAA,QACvC;AAAA,QAEA,UAAA;AAAA,UAAA,oBAAC,SAAA,EAAQ,eAAY,OAAA,CAAO;AAAA,UAC3B;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEH;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,cAAW;AAAA,QACX,WAAU;AAAA,QACV,SAAS,MAAM;AAAA,QAEf,UAAA,oBAAC,OAAA,EAAM,WAAU,SAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAC5B,GACF;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,qBAAqB;AACvB,GAQG;AACD,QAAM,UAAU,gBAAgB,MAAM,SAAS;AAC/C,QAAM,cAAc,oBAAoB,MAAM,OAAO;AACrD,QAAM,SAAS,MAAM,UAAU,gBAAgB,MAAM,UAAU;AAG/D,QAAM,YAAY,aAAa,CAAC,UAAU,gBAAgB,OAAO,MAAM,gBAAgB;AACvF,MAAI,CAAC,UAAU,CAAC,MAAM,SAAS,CAAC,MAAM,eAAe,gBAAgB,QAAQ,cAAc;AACzF,WAAO;AACT,QAAM,kBAAkB,gBAAgB,QAAQ,kBAAkB;AAClE,SACE,qBAAC,OAAA,EAAI,WAAU,qFAGZ,UAAA;AAAA,IAAA,8BACE,QAAA,EAAK,MAAK,UACR,UAAA,MAAM,UAAU,eACb,sBACA,SAAS,MAAM,WAAW,UAAU,MAAM,GAAG,UAAU,MAAM,OAAO,KAAK,EAAE,GAAA,CACjF;AAAA,IAED,MAAM,UAAU,UAAU,oBAAC,UAAM,UAAA,uBAAsB;AAAA,IACvD,MAAM,cAAc,qBAAC,QAAA,EAAK,UAAA;AAAA,MAAA;AAAA,MAAoB,MAAM,WAAW;AAAA,IAAA,GAAK;AAAA,IACpE,MAAM,eACL,qBAAC,QAAA,EAAK,UAAA;AAAA,MAAA;AAAA,MAAuC,MAAM,YAAY;AAAA,IAAA,GAAK;AAAA,IAErE,MAAM,UAAU,oBAAC,UAAK,MAAK,UAAU,gBAAM,QAAO;AAAA,IAClD,MAAM,SACL,oBAAC,QAAA,EAAK,MAAK,SAAQ,WAAU,oBAC1B,UAAA,MAAM,MAAA,CACT;AAAA,IAED,gBAAgB,QAAQ,oBAAC,QAAA,EAAK,MAAK,UAAU,UAAA,aAAY;AAAA,IACzD,cAAc,QACb,qBAAC,QAAA,EAAK,WAAU,oDACd,UAAA;AAAA,MAAA,oBAAC,UAAK,WAAU,oBAAoB,UAAA,2BAA2B,UAAU,OAAO,GAAE;AAAA,MAClF,oBAAC,QAAA,EAAK,eAAY,QAAO,UAAA,KAAC;AAAA,MAC1B;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO,sBAAsB;AAAA,UAC7B,UAAU;AAAA,UACV,SAAS,MAAM,KAAK,MAAM,aAAA;AAAA,UAC1B,WAAU;AAAA,UAET,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IACH,EAAA,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require$$0 = require("react");
4
+ const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
5
+ const errors = require("../diagnostics/errors.cjs");
6
+ function createClientToolWriteConsentStore() {
7
+ let pending = null;
8
+ let cards = 0;
9
+ const granted = /* @__PURE__ */ new Set();
10
+ const grantKey = (threadId, toolName) => `${threadId}\0${toolName}`;
11
+ const grantAll = (threadId, toolNames) => {
12
+ for (const toolName of toolNames) granted.add(grantKey(threadId, toolName));
13
+ };
14
+ const listeners = /* @__PURE__ */ new Set();
15
+ const notify = () => {
16
+ for (const listener of Array.from(listeners)) listener();
17
+ };
18
+ const settle = (decision) => {
19
+ const settled = pending;
20
+ pending = null;
21
+ settled == null ? void 0 : settled.resolve(decision);
22
+ notify();
23
+ };
24
+ return {
25
+ getPending: () => (pending == null ? void 0 : pending.request) ?? null,
26
+ subscribe(listener) {
27
+ listeners.add(listener);
28
+ return () => {
29
+ listeners.delete(listener);
30
+ };
31
+ },
32
+ isGranted: ({ threadId, toolName }) => granted.has(grantKey(threadId, toolName)),
33
+ ask(request) {
34
+ if (request.tools.every((tool) => granted.has(grantKey(request.threadId, tool.name)))) {
35
+ return Promise.resolve("proceed");
36
+ }
37
+ if (pending) settle("abandoned");
38
+ if (cards === 0) {
39
+ athenaDiagnostics.athenaDiagnostics.emit("sdk.config.warning", {
40
+ level: "warn",
41
+ error: new errors.AthenaSdkError({
42
+ code: "config_invalid",
43
+ message: "A frontend tool is waiting for the person to allow it, and nothing on screen asks them.",
44
+ hint: "Render <AthenaChat> (it draws the question), mount <StatewireClientToolConsentCard>, or pass confirmClientToolWrites on <AthenaProvider>. The run stays parked until the question is answered.",
45
+ context: { tools: request.tools.map((tool) => tool.name) }
46
+ }),
47
+ data: { tools: request.tools.map((tool) => tool.name) }
48
+ });
49
+ }
50
+ return new Promise((resolve) => {
51
+ pending = { request, resolve };
52
+ notify();
53
+ });
54
+ },
55
+ answer({ requestId, allow }) {
56
+ if ((pending == null ? void 0 : pending.request.requestId) !== requestId) return;
57
+ if (allow) {
58
+ grantAll(
59
+ pending.request.threadId,
60
+ pending.request.tools.map((tool) => tool.name)
61
+ );
62
+ }
63
+ settle(allow ? "proceed" : "declined");
64
+ },
65
+ abandon(requestId) {
66
+ if ((pending == null ? void 0 : pending.request.requestId) !== requestId) return;
67
+ settle("abandoned");
68
+ },
69
+ grant({ threadId, toolNames }) {
70
+ grantAll(threadId, toolNames);
71
+ },
72
+ registerCard() {
73
+ cards += 1;
74
+ return () => {
75
+ cards -= 1;
76
+ };
77
+ },
78
+ dispose() {
79
+ granted.clear();
80
+ if (pending) settle("abandoned");
81
+ }
82
+ };
83
+ }
84
+ function useClientToolWriteConsentStore(scope) {
85
+ const [held, setHeld] = require$$0.useState(() => ({ scope, store: createClientToolWriteConsentStore() }));
86
+ let current = held;
87
+ if (held.scope !== scope) {
88
+ current = { scope, store: createClientToolWriteConsentStore() };
89
+ setHeld(current);
90
+ }
91
+ const { store } = current;
92
+ require$$0.useEffect(() => () => store.dispose(), [store]);
93
+ return store;
94
+ }
95
+ const ClientToolWriteConsentContext = require$$0.createContext(
96
+ null
97
+ );
98
+ const NO_SUBSCRIPTION = () => () => {
99
+ };
100
+ const NO_PENDING = () => null;
101
+ function useClientToolWriteConsent() {
102
+ const store = require$$0.useContext(ClientToolWriteConsentContext);
103
+ const request = require$$0.useSyncExternalStore(
104
+ (store == null ? void 0 : store.subscribe) ?? NO_SUBSCRIPTION,
105
+ (store == null ? void 0 : store.getPending) ?? NO_PENDING,
106
+ NO_PENDING
107
+ );
108
+ require$$0.useEffect(() => store == null ? void 0 : store.registerCard(), [store]);
109
+ const requestId = request == null ? void 0 : request.requestId;
110
+ const allow = require$$0.useCallback(() => {
111
+ if (requestId !== void 0) store == null ? void 0 : store.answer({ requestId, allow: true });
112
+ }, [requestId, store]);
113
+ const decline = require$$0.useCallback(() => {
114
+ if (requestId !== void 0) store == null ? void 0 : store.answer({ requestId, allow: false });
115
+ }, [requestId, store]);
116
+ return { request, allow, decline };
117
+ }
118
+ exports.ClientToolWriteConsentContext = ClientToolWriteConsentContext;
119
+ exports.createClientToolWriteConsentStore = createClientToolWriteConsentStore;
120
+ exports.useClientToolWriteConsent = useClientToolWriteConsent;
121
+ exports.useClientToolWriteConsentStore = useClientToolWriteConsentStore;
122
+ //# sourceMappingURL=client-tool-write-consent.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tool-write-consent.cjs","sources":["../../src/chat/client-tool-write-consent.ts"],"sourcesContent":["'use client';\n\nimport type { ClientToolWriteConsentDecision } from '@athenaintel/agent-runtime';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { AthenaSdkError } from '../diagnostics/errors';\n\n/*\n * Write consent for the app's own frontend tools. A tool marked with\n * `requiresWriteConsent` parks its first call in a conversation until the\n * person answers; this store holds that question between the client-tool\n * bridge (which asks) and whatever draws it (`StatewireClientToolConsentCard`\n * in `AthenaChat`, or a host's own card on `useClientToolWriteConsent`).\n *\n * Allowing covers the tools the question named, for the rest of the\n * conversation, for the person who allowed it: a yes to one tool is never a\n * yes to another. A store belongs to one signed-in person on one\n * Athena (`useClientToolWriteConsentStore`), so someone else signing in to the\n * same mounted chat is asked for themselves. Nothing is remembered across a\n * reload: the next call asks again.\n */\n\n/** One write tool of a batch waiting on the person. */\nexport interface ClientToolWriteConsentTool {\n /** The tool's name, as the model calls it. */\n name: string;\n /** What it does, when the tool said (`requiresWriteConsent(…, { summary })`). */\n summary?: string;\n}\n\n/** The question a parked batch of write calls puts to the person. */\nexport interface ClientToolWriteConsentRequest {\n /** The parked request the answer resumes. */\n requestId: string;\n /** The conversation the answer covers. */\n threadId: string;\n /** The batch's write tools, each once. */\n tools: readonly ClientToolWriteConsentTool[];\n /** Who is asking, when the host named its agent (`agentUi.agentName`). */\n agentName?: string;\n}\n\n/**\n * A host's own consent UI (`AthenaProviderProps.confirmClientToolWrites`).\n * Resolve `true` to allow the conversation's writes, `false` to decline this\n * batch. `signal` aborts when the question no longer has a call to answer\n * (the run was stopped): close the dialog then; the value is ignored.\n */\nexport type ClientToolWriteConfirmer = (\n request: ClientToolWriteConsentRequest & { signal: AbortSignal },\n) => Promise<boolean>;\n\nexport interface ClientToolWriteConsentStore {\n /** The question on screen, or `null`. */\n getPending(): ClientToolWriteConsentRequest | null;\n subscribe(listener: () => void): () => void;\n /** Whether the person already allowed this tool in this conversation. */\n isGranted(args: { threadId: string; toolName: string }): boolean;\n /** Put the question to the person; resolves with their answer, or `abandoned`. */\n ask(request: ClientToolWriteConsentRequest): Promise<ClientToolWriteConsentDecision>;\n /** The person's answer to the pending question. */\n answer(args: { requestId: string; allow: boolean }): void;\n /** The pending question lost its call (stop, another conversation): close it. */\n abandon(requestId: string): void;\n /** Record a consent the host's own UI collected, for the tools it asked about. */\n grant(args: { threadId: string; toolNames: readonly string[] }): void;\n /** A mounted card registers itself, so a question nobody can see is reported. */\n registerCard(): () => void;\n /** The store's person is gone: close the pending question and forget every yes. */\n dispose(): void;\n}\n\ninterface PendingConsent {\n request: ClientToolWriteConsentRequest;\n resolve: (decision: ClientToolWriteConsentDecision) => void;\n}\n\nexport function createClientToolWriteConsentStore(): ClientToolWriteConsentStore {\n let pending: PendingConsent | null = null;\n let cards = 0;\n const granted = new Set<string>();\n const grantKey = (threadId: string, toolName: string): string => `${threadId}\\u0000${toolName}`;\n const grantAll = (threadId: string, toolNames: readonly string[]) => {\n for (const toolName of toolNames) granted.add(grantKey(threadId, toolName));\n };\n const listeners = new Set<() => void>();\n const notify = () => {\n for (const listener of Array.from(listeners)) listener();\n };\n const settle = (decision: ClientToolWriteConsentDecision) => {\n const settled = pending;\n pending = null;\n settled?.resolve(decision);\n notify();\n };\n\n return {\n getPending: () => pending?.request ?? null,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n isGranted: ({ threadId, toolName }) => granted.has(grantKey(threadId, toolName)),\n ask(request) {\n if (request.tools.every((tool) => granted.has(grantKey(request.threadId, tool.name)))) {\n return Promise.resolve('proceed');\n }\n // One question at a time: a newer batch replaces a question whose call\n // the bridge has already given up on.\n if (pending) settle('abandoned');\n if (cards === 0) {\n athenaDiagnostics.emit('sdk.config.warning', {\n level: 'warn',\n error: new AthenaSdkError({\n code: 'config_invalid',\n message:\n 'A frontend tool is waiting for the person to allow it, and nothing on screen asks them.',\n hint: 'Render <AthenaChat> (it draws the question), mount <StatewireClientToolConsentCard>, or pass confirmClientToolWrites on <AthenaProvider>. The run stays parked until the question is answered.',\n context: { tools: request.tools.map((tool) => tool.name) },\n }),\n data: { tools: request.tools.map((tool) => tool.name) },\n });\n }\n return new Promise<ClientToolWriteConsentDecision>((resolve) => {\n pending = { request, resolve };\n notify();\n });\n },\n answer({ requestId, allow }) {\n if (pending?.request.requestId !== requestId) return;\n if (allow) {\n grantAll(\n pending.request.threadId,\n pending.request.tools.map((tool) => tool.name),\n );\n }\n settle(allow ? 'proceed' : 'declined');\n },\n abandon(requestId) {\n if (pending?.request.requestId !== requestId) return;\n settle('abandoned');\n },\n grant({ threadId, toolNames }) {\n grantAll(threadId, toolNames);\n },\n registerCard() {\n cards += 1;\n return () => {\n cards -= 1;\n };\n },\n dispose() {\n granted.clear();\n if (pending) settle('abandoned');\n },\n };\n}\n\n/**\n * The consent store for one signed-in person on one Athena. `scope` names\n * both (the provider passes the session principal and the sync host): when it\n * changes, the previous person's store is disposed (its question closed, its\n * yeses forgotten) and the next person starts with none.\n */\nexport function useClientToolWriteConsentStore(scope: string): ClientToolWriteConsentStore {\n const [held, setHeld] = useState(() => ({ scope, store: createClientToolWriteConsentStore() }));\n let current = held;\n if (held.scope !== scope) {\n // Replaced during render, so no commit ever pairs the new person with the\n // previous person's store.\n current = { scope, store: createClientToolWriteConsentStore() };\n setHeld(current);\n }\n const { store } = current;\n useEffect(() => () => store.dispose(), [store]);\n return store;\n}\n\n/** Provided by `AthenaProvider`; `null` outside one. */\nexport const ClientToolWriteConsentContext = createContext<ClientToolWriteConsentStore | null>(\n null,\n);\n\nconst NO_SUBSCRIPTION = () => () => {};\nconst NO_PENDING = () => null;\n\nexport interface ClientToolWriteConsent {\n /** The question waiting on the person, or `null`. */\n request: ClientToolWriteConsentRequest | null;\n /** Allow the conversation's writes and run the waiting calls. */\n allow: () => void;\n /** Decline the waiting calls; the agent is told, and a later call asks again. */\n decline: () => void;\n}\n\n/**\n * The write-consent question for the chat the nearest `AthenaProvider` shows,\n * for a host that draws its own card. Calling it marks the question as shown.\n */\nexport function useClientToolWriteConsent(): ClientToolWriteConsent {\n const store = useContext(ClientToolWriteConsentContext);\n const request = useSyncExternalStore(\n store?.subscribe ?? NO_SUBSCRIPTION,\n store?.getPending ?? NO_PENDING,\n NO_PENDING,\n );\n useEffect(() => store?.registerCard(), [store]);\n const requestId = request?.requestId;\n const allow = useCallback(() => {\n if (requestId !== undefined) store?.answer({ requestId, allow: true });\n }, [requestId, store]);\n const decline = useCallback(() => {\n if (requestId !== undefined) store?.answer({ requestId, allow: false });\n }, [requestId, store]);\n return { request, allow, decline };\n}\n"],"names":["athenaDiagnostics","AthenaSdkError","useState","useEffect","createContext","useContext","useSyncExternalStore","useCallback"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import type { ClientToolWriteConsentDecision } from '../agent-runtime/src/index';
2
+ /** One write tool of a batch waiting on the person. */
3
+ export interface ClientToolWriteConsentTool {
4
+ /** The tool's name, as the model calls it. */
5
+ name: string;
6
+ /** What it does, when the tool said (`requiresWriteConsent(…, { summary })`). */
7
+ summary?: string;
8
+ }
9
+ /** The question a parked batch of write calls puts to the person. */
10
+ export interface ClientToolWriteConsentRequest {
11
+ /** The parked request the answer resumes. */
12
+ requestId: string;
13
+ /** The conversation the answer covers. */
14
+ threadId: string;
15
+ /** The batch's write tools, each once. */
16
+ tools: readonly ClientToolWriteConsentTool[];
17
+ /** Who is asking, when the host named its agent (`agentUi.agentName`). */
18
+ agentName?: string;
19
+ }
20
+ /**
21
+ * A host's own consent UI (`AthenaProviderProps.confirmClientToolWrites`).
22
+ * Resolve `true` to allow the conversation's writes, `false` to decline this
23
+ * batch. `signal` aborts when the question no longer has a call to answer
24
+ * (the run was stopped): close the dialog then; the value is ignored.
25
+ */
26
+ export type ClientToolWriteConfirmer = (request: ClientToolWriteConsentRequest & {
27
+ signal: AbortSignal;
28
+ }) => Promise<boolean>;
29
+ export interface ClientToolWriteConsentStore {
30
+ /** The question on screen, or `null`. */
31
+ getPending(): ClientToolWriteConsentRequest | null;
32
+ subscribe(listener: () => void): () => void;
33
+ /** Whether the person already allowed this tool in this conversation. */
34
+ isGranted(args: {
35
+ threadId: string;
36
+ toolName: string;
37
+ }): boolean;
38
+ /** Put the question to the person; resolves with their answer, or `abandoned`. */
39
+ ask(request: ClientToolWriteConsentRequest): Promise<ClientToolWriteConsentDecision>;
40
+ /** The person's answer to the pending question. */
41
+ answer(args: {
42
+ requestId: string;
43
+ allow: boolean;
44
+ }): void;
45
+ /** The pending question lost its call (stop, another conversation): close it. */
46
+ abandon(requestId: string): void;
47
+ /** Record a consent the host's own UI collected, for the tools it asked about. */
48
+ grant(args: {
49
+ threadId: string;
50
+ toolNames: readonly string[];
51
+ }): void;
52
+ /** A mounted card registers itself, so a question nobody can see is reported. */
53
+ registerCard(): () => void;
54
+ /** The store's person is gone: close the pending question and forget every yes. */
55
+ dispose(): void;
56
+ }
57
+ export declare function createClientToolWriteConsentStore(): ClientToolWriteConsentStore;
58
+ /**
59
+ * The consent store for one signed-in person on one Athena. `scope` names
60
+ * both (the provider passes the session principal and the sync host): when it
61
+ * changes, the previous person's store is disposed (its question closed, its
62
+ * yeses forgotten) and the next person starts with none.
63
+ */
64
+ export declare function useClientToolWriteConsentStore(scope: string): ClientToolWriteConsentStore;
65
+ /** Provided by `AthenaProvider`; `null` outside one. */
66
+ export declare const ClientToolWriteConsentContext: import("react").Context<ClientToolWriteConsentStore | null>;
67
+ export interface ClientToolWriteConsent {
68
+ /** The question waiting on the person, or `null`. */
69
+ request: ClientToolWriteConsentRequest | null;
70
+ /** Allow the conversation's writes and run the waiting calls. */
71
+ allow: () => void;
72
+ /** Decline the waiting calls; the agent is told, and a later call asks again. */
73
+ decline: () => void;
74
+ }
75
+ /**
76
+ * The write-consent question for the chat the nearest `AthenaProvider` shows,
77
+ * for a host that draws its own card. Calling it marks the question as shown.
78
+ */
79
+ export declare function useClientToolWriteConsent(): ClientToolWriteConsent;
@@ -0,0 +1,122 @@
1
+ import { useContext, useSyncExternalStore, useEffect, useCallback, createContext, useState } from "react";
2
+ import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
3
+ import { AthenaSdkError } from "../diagnostics/errors.js";
4
+ function createClientToolWriteConsentStore() {
5
+ let pending = null;
6
+ let cards = 0;
7
+ const granted = /* @__PURE__ */ new Set();
8
+ const grantKey = (threadId, toolName) => `${threadId}\0${toolName}`;
9
+ const grantAll = (threadId, toolNames) => {
10
+ for (const toolName of toolNames) granted.add(grantKey(threadId, toolName));
11
+ };
12
+ const listeners = /* @__PURE__ */ new Set();
13
+ const notify = () => {
14
+ for (const listener of Array.from(listeners)) listener();
15
+ };
16
+ const settle = (decision) => {
17
+ const settled = pending;
18
+ pending = null;
19
+ settled == null ? void 0 : settled.resolve(decision);
20
+ notify();
21
+ };
22
+ return {
23
+ getPending: () => (pending == null ? void 0 : pending.request) ?? null,
24
+ subscribe(listener) {
25
+ listeners.add(listener);
26
+ return () => {
27
+ listeners.delete(listener);
28
+ };
29
+ },
30
+ isGranted: ({ threadId, toolName }) => granted.has(grantKey(threadId, toolName)),
31
+ ask(request) {
32
+ if (request.tools.every((tool) => granted.has(grantKey(request.threadId, tool.name)))) {
33
+ return Promise.resolve("proceed");
34
+ }
35
+ if (pending) settle("abandoned");
36
+ if (cards === 0) {
37
+ athenaDiagnostics.emit("sdk.config.warning", {
38
+ level: "warn",
39
+ error: new AthenaSdkError({
40
+ code: "config_invalid",
41
+ message: "A frontend tool is waiting for the person to allow it, and nothing on screen asks them.",
42
+ hint: "Render <AthenaChat> (it draws the question), mount <StatewireClientToolConsentCard>, or pass confirmClientToolWrites on <AthenaProvider>. The run stays parked until the question is answered.",
43
+ context: { tools: request.tools.map((tool) => tool.name) }
44
+ }),
45
+ data: { tools: request.tools.map((tool) => tool.name) }
46
+ });
47
+ }
48
+ return new Promise((resolve) => {
49
+ pending = { request, resolve };
50
+ notify();
51
+ });
52
+ },
53
+ answer({ requestId, allow }) {
54
+ if ((pending == null ? void 0 : pending.request.requestId) !== requestId) return;
55
+ if (allow) {
56
+ grantAll(
57
+ pending.request.threadId,
58
+ pending.request.tools.map((tool) => tool.name)
59
+ );
60
+ }
61
+ settle(allow ? "proceed" : "declined");
62
+ },
63
+ abandon(requestId) {
64
+ if ((pending == null ? void 0 : pending.request.requestId) !== requestId) return;
65
+ settle("abandoned");
66
+ },
67
+ grant({ threadId, toolNames }) {
68
+ grantAll(threadId, toolNames);
69
+ },
70
+ registerCard() {
71
+ cards += 1;
72
+ return () => {
73
+ cards -= 1;
74
+ };
75
+ },
76
+ dispose() {
77
+ granted.clear();
78
+ if (pending) settle("abandoned");
79
+ }
80
+ };
81
+ }
82
+ function useClientToolWriteConsentStore(scope) {
83
+ const [held, setHeld] = useState(() => ({ scope, store: createClientToolWriteConsentStore() }));
84
+ let current = held;
85
+ if (held.scope !== scope) {
86
+ current = { scope, store: createClientToolWriteConsentStore() };
87
+ setHeld(current);
88
+ }
89
+ const { store } = current;
90
+ useEffect(() => () => store.dispose(), [store]);
91
+ return store;
92
+ }
93
+ const ClientToolWriteConsentContext = createContext(
94
+ null
95
+ );
96
+ const NO_SUBSCRIPTION = () => () => {
97
+ };
98
+ const NO_PENDING = () => null;
99
+ function useClientToolWriteConsent() {
100
+ const store = useContext(ClientToolWriteConsentContext);
101
+ const request = useSyncExternalStore(
102
+ (store == null ? void 0 : store.subscribe) ?? NO_SUBSCRIPTION,
103
+ (store == null ? void 0 : store.getPending) ?? NO_PENDING,
104
+ NO_PENDING
105
+ );
106
+ useEffect(() => store == null ? void 0 : store.registerCard(), [store]);
107
+ const requestId = request == null ? void 0 : request.requestId;
108
+ const allow = useCallback(() => {
109
+ if (requestId !== void 0) store == null ? void 0 : store.answer({ requestId, allow: true });
110
+ }, [requestId, store]);
111
+ const decline = useCallback(() => {
112
+ if (requestId !== void 0) store == null ? void 0 : store.answer({ requestId, allow: false });
113
+ }, [requestId, store]);
114
+ return { request, allow, decline };
115
+ }
116
+ export {
117
+ ClientToolWriteConsentContext,
118
+ createClientToolWriteConsentStore,
119
+ useClientToolWriteConsent,
120
+ useClientToolWriteConsentStore
121
+ };
122
+ //# sourceMappingURL=client-tool-write-consent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tool-write-consent.js","sources":["../../src/chat/client-tool-write-consent.ts"],"sourcesContent":["'use client';\n\nimport type { ClientToolWriteConsentDecision } from '@athenaintel/agent-runtime';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { AthenaSdkError } from '../diagnostics/errors';\n\n/*\n * Write consent for the app's own frontend tools. A tool marked with\n * `requiresWriteConsent` parks its first call in a conversation until the\n * person answers; this store holds that question between the client-tool\n * bridge (which asks) and whatever draws it (`StatewireClientToolConsentCard`\n * in `AthenaChat`, or a host's own card on `useClientToolWriteConsent`).\n *\n * Allowing covers the tools the question named, for the rest of the\n * conversation, for the person who allowed it: a yes to one tool is never a\n * yes to another. A store belongs to one signed-in person on one\n * Athena (`useClientToolWriteConsentStore`), so someone else signing in to the\n * same mounted chat is asked for themselves. Nothing is remembered across a\n * reload: the next call asks again.\n */\n\n/** One write tool of a batch waiting on the person. */\nexport interface ClientToolWriteConsentTool {\n /** The tool's name, as the model calls it. */\n name: string;\n /** What it does, when the tool said (`requiresWriteConsent(…, { summary })`). */\n summary?: string;\n}\n\n/** The question a parked batch of write calls puts to the person. */\nexport interface ClientToolWriteConsentRequest {\n /** The parked request the answer resumes. */\n requestId: string;\n /** The conversation the answer covers. */\n threadId: string;\n /** The batch's write tools, each once. */\n tools: readonly ClientToolWriteConsentTool[];\n /** Who is asking, when the host named its agent (`agentUi.agentName`). */\n agentName?: string;\n}\n\n/**\n * A host's own consent UI (`AthenaProviderProps.confirmClientToolWrites`).\n * Resolve `true` to allow the conversation's writes, `false` to decline this\n * batch. `signal` aborts when the question no longer has a call to answer\n * (the run was stopped): close the dialog then; the value is ignored.\n */\nexport type ClientToolWriteConfirmer = (\n request: ClientToolWriteConsentRequest & { signal: AbortSignal },\n) => Promise<boolean>;\n\nexport interface ClientToolWriteConsentStore {\n /** The question on screen, or `null`. */\n getPending(): ClientToolWriteConsentRequest | null;\n subscribe(listener: () => void): () => void;\n /** Whether the person already allowed this tool in this conversation. */\n isGranted(args: { threadId: string; toolName: string }): boolean;\n /** Put the question to the person; resolves with their answer, or `abandoned`. */\n ask(request: ClientToolWriteConsentRequest): Promise<ClientToolWriteConsentDecision>;\n /** The person's answer to the pending question. */\n answer(args: { requestId: string; allow: boolean }): void;\n /** The pending question lost its call (stop, another conversation): close it. */\n abandon(requestId: string): void;\n /** Record a consent the host's own UI collected, for the tools it asked about. */\n grant(args: { threadId: string; toolNames: readonly string[] }): void;\n /** A mounted card registers itself, so a question nobody can see is reported. */\n registerCard(): () => void;\n /** The store's person is gone: close the pending question and forget every yes. */\n dispose(): void;\n}\n\ninterface PendingConsent {\n request: ClientToolWriteConsentRequest;\n resolve: (decision: ClientToolWriteConsentDecision) => void;\n}\n\nexport function createClientToolWriteConsentStore(): ClientToolWriteConsentStore {\n let pending: PendingConsent | null = null;\n let cards = 0;\n const granted = new Set<string>();\n const grantKey = (threadId: string, toolName: string): string => `${threadId}\\u0000${toolName}`;\n const grantAll = (threadId: string, toolNames: readonly string[]) => {\n for (const toolName of toolNames) granted.add(grantKey(threadId, toolName));\n };\n const listeners = new Set<() => void>();\n const notify = () => {\n for (const listener of Array.from(listeners)) listener();\n };\n const settle = (decision: ClientToolWriteConsentDecision) => {\n const settled = pending;\n pending = null;\n settled?.resolve(decision);\n notify();\n };\n\n return {\n getPending: () => pending?.request ?? null,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n isGranted: ({ threadId, toolName }) => granted.has(grantKey(threadId, toolName)),\n ask(request) {\n if (request.tools.every((tool) => granted.has(grantKey(request.threadId, tool.name)))) {\n return Promise.resolve('proceed');\n }\n // One question at a time: a newer batch replaces a question whose call\n // the bridge has already given up on.\n if (pending) settle('abandoned');\n if (cards === 0) {\n athenaDiagnostics.emit('sdk.config.warning', {\n level: 'warn',\n error: new AthenaSdkError({\n code: 'config_invalid',\n message:\n 'A frontend tool is waiting for the person to allow it, and nothing on screen asks them.',\n hint: 'Render <AthenaChat> (it draws the question), mount <StatewireClientToolConsentCard>, or pass confirmClientToolWrites on <AthenaProvider>. The run stays parked until the question is answered.',\n context: { tools: request.tools.map((tool) => tool.name) },\n }),\n data: { tools: request.tools.map((tool) => tool.name) },\n });\n }\n return new Promise<ClientToolWriteConsentDecision>((resolve) => {\n pending = { request, resolve };\n notify();\n });\n },\n answer({ requestId, allow }) {\n if (pending?.request.requestId !== requestId) return;\n if (allow) {\n grantAll(\n pending.request.threadId,\n pending.request.tools.map((tool) => tool.name),\n );\n }\n settle(allow ? 'proceed' : 'declined');\n },\n abandon(requestId) {\n if (pending?.request.requestId !== requestId) return;\n settle('abandoned');\n },\n grant({ threadId, toolNames }) {\n grantAll(threadId, toolNames);\n },\n registerCard() {\n cards += 1;\n return () => {\n cards -= 1;\n };\n },\n dispose() {\n granted.clear();\n if (pending) settle('abandoned');\n },\n };\n}\n\n/**\n * The consent store for one signed-in person on one Athena. `scope` names\n * both (the provider passes the session principal and the sync host): when it\n * changes, the previous person's store is disposed (its question closed, its\n * yeses forgotten) and the next person starts with none.\n */\nexport function useClientToolWriteConsentStore(scope: string): ClientToolWriteConsentStore {\n const [held, setHeld] = useState(() => ({ scope, store: createClientToolWriteConsentStore() }));\n let current = held;\n if (held.scope !== scope) {\n // Replaced during render, so no commit ever pairs the new person with the\n // previous person's store.\n current = { scope, store: createClientToolWriteConsentStore() };\n setHeld(current);\n }\n const { store } = current;\n useEffect(() => () => store.dispose(), [store]);\n return store;\n}\n\n/** Provided by `AthenaProvider`; `null` outside one. */\nexport const ClientToolWriteConsentContext = createContext<ClientToolWriteConsentStore | null>(\n null,\n);\n\nconst NO_SUBSCRIPTION = () => () => {};\nconst NO_PENDING = () => null;\n\nexport interface ClientToolWriteConsent {\n /** The question waiting on the person, or `null`. */\n request: ClientToolWriteConsentRequest | null;\n /** Allow the conversation's writes and run the waiting calls. */\n allow: () => void;\n /** Decline the waiting calls; the agent is told, and a later call asks again. */\n decline: () => void;\n}\n\n/**\n * The write-consent question for the chat the nearest `AthenaProvider` shows,\n * for a host that draws its own card. Calling it marks the question as shown.\n */\nexport function useClientToolWriteConsent(): ClientToolWriteConsent {\n const store = useContext(ClientToolWriteConsentContext);\n const request = useSyncExternalStore(\n store?.subscribe ?? NO_SUBSCRIPTION,\n store?.getPending ?? NO_PENDING,\n NO_PENDING,\n );\n useEffect(() => store?.registerCard(), [store]);\n const requestId = request?.requestId;\n const allow = useCallback(() => {\n if (requestId !== undefined) store?.answer({ requestId, allow: true });\n }, [requestId, store]);\n const decline = useCallback(() => {\n if (requestId !== undefined) store?.answer({ requestId, allow: false });\n }, [requestId, store]);\n return { request, allow, decline };\n}\n"],"names":[],"mappings":"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"}
@@ -3,6 +3,28 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
4
4
  const assistantStream = require("assistant-stream");
5
5
  const clientTools = require("../agent-runtime/src/client-tools.cjs");
6
+ const CLIENT_TOOL_WRITE_CONSENT = Symbol.for("athena.clientTool.writeConsent");
7
+ function requiresWriteConsent(tool, options = {}) {
8
+ var _a;
9
+ const summary = (_a = options.summary) == null ? void 0 : _a.trim();
10
+ return { ...tool, [CLIENT_TOOL_WRITE_CONSENT]: summary ? { summary } : {} };
11
+ }
12
+ function clientToolWriteConsent(entry) {
13
+ if (!entry || typeof entry !== "object") return null;
14
+ const mark = entry[CLIENT_TOOL_WRITE_CONSENT];
15
+ if (!mark || typeof mark !== "object") return null;
16
+ const summary = mark.summary;
17
+ return typeof summary === "string" && summary ? { summary } : {};
18
+ }
19
+ function clientToolWriteConsentSummaries(toolkit) {
20
+ var _a;
21
+ const summaries = {};
22
+ for (const [name, entry] of Object.entries(toolkit ?? {})) {
23
+ const summary = (_a = clientToolWriteConsent(entry)) == null ? void 0 : _a.summary;
24
+ if (summary) summaries[name] = summary;
25
+ }
26
+ return summaries;
27
+ }
6
28
  function hasCallableExecute(entry) {
7
29
  return !!entry && typeof entry === "object" && typeof entry.execute === "function";
8
30
  }
@@ -32,15 +54,15 @@ function collectStatewireClientTools(toolkit) {
32
54
  name,
33
55
  description: entry.description ?? "",
34
56
  parameters,
35
- requiresWrite: false
57
+ requiresWrite: clientToolWriteConsent(entry) !== null
36
58
  },
37
59
  handler: async (args, context) => {
38
- const controller = new AbortController();
39
60
  const output = await execute(args, {
40
- // The bridge always supplies the parked call's id; a host invoking
41
- // the handler directly has no call to correlate.
61
+ // The bridge always supplies the parked call's id and its signal,
62
+ // which aborts when the call's request goes away (the run was
63
+ // stopped); a host invoking the handler directly has neither.
42
64
  toolCallId: (context == null ? void 0 : context.toolCallId) ?? "",
43
- abortSignal: controller.signal,
65
+ abortSignal: (context == null ? void 0 : context.signal) ?? new AbortController().signal,
44
66
  human: () => Promise.reject(
45
67
  new Error("Human input is not supported for statewire client tools.")
46
68
  )
@@ -73,7 +95,10 @@ function resolveClientToolSurface(appId) {
73
95
  }
74
96
  exports.CLIENT_TOOL_SURFACE_APP_IDS = CLIENT_TOOL_SURFACE_APP_IDS;
75
97
  exports.backendDeclaredFrontendToolIds = backendDeclaredFrontendToolIds;
98
+ exports.clientToolWriteConsent = clientToolWriteConsent;
99
+ exports.clientToolWriteConsentSummaries = clientToolWriteConsentSummaries;
76
100
  exports.collectStatewireClientTools = collectStatewireClientTools;
101
+ exports.requiresWriteConsent = requiresWriteConsent;
77
102
  exports.resolveClientToolSurface = resolveClientToolSurface;
78
103
  exports.statewireClientToolWireEntries = statewireClientToolWireEntries;
79
104
  //# sourceMappingURL=statewire-client-tools.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-client-tools.cjs","sources":["../../src/chat/statewire-client-tools.ts"],"sourcesContent":["import { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport type { Toolkit } from '@assistant-ui/react';\nimport {\n type ClientToolHandlerContext,\n clientToolRunConfigEntries,\n type RegisteredClientTool,\n} from '@athenaintel/agent-runtime';\nimport { ToolResponse, toJSONSchema } from 'assistant-stream';\n\n/**\n * @deprecated The SDK's client tools ARE the shared `RegisteredClientTool`\n * shape from `@athenaintel/agent-runtime` (`definition` + `handler`); import\n * that type instead. Retained for one release so existing imports type-check.\n */\nexport type StatewireClientTool = RegisteredClientTool;\n\nfunction hasCallableExecute(\n entry: unknown,\n): entry is { execute: (args: Record<string, unknown>, context: unknown) => unknown } {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as { execute?: unknown }).execute === 'function'\n );\n}\n\n/**\n * Collect the client-executable tools from a frontend toolkit, in the shared\n * `RegisteredClientTool` shape every statewire surface declares and executes.\n *\n * The deep-agent runtime drops frontend (`ui_*`) tool definitions, so tools\n * with a local `execute` must instead be declared through\n * `runConfig.custom.client_tools` and answered by the statewire client-tool\n * bridge. Schema conversion reuses `toJSONSchema` — the same conversion\n * primitive as the legacy assistant-transport — and a tool whose schema cannot\n * be converted is skipped with a warning rather than failing the whole set.\n * SDK tools never gate on write consent (`requiresWrite: false`): a toolkit\n * `execute` is the host app's own code.\n */\nexport function collectStatewireClientTools(toolkit: Toolkit): RegisteredClientTool[] {\n const executable = Object.entries(toolkit).filter(([, entry]) => hasCallableExecute(entry));\n if (executable.length === 0) return [];\n\n return executable.flatMap(([name, entry]) => {\n if (entry.parameters === undefined) return [];\n\n let parameters: Record<string, unknown>;\n try {\n parameters = { ...toJSONSchema(entry.parameters) };\n } catch (error) {\n athenaDiagnostics.emit('tools.schema.warning', {\n level: 'warn',\n data: {\n tool: name,\n message: 'Frontend tool schema could not be converted for statewire; the tool is not registered',\n error: error instanceof Error ? error.message : String(error),\n },\n });\n return [];\n }\n\n const execute = (entry as {\n execute: (args: Record<string, unknown>, context: unknown) => unknown;\n }).execute;\n return [\n {\n definition: {\n name,\n description: entry.description ?? '',\n parameters,\n requiresWrite: false,\n },\n handler: async (args, context?: ClientToolHandlerContext) => {\n const controller = new AbortController();\n const output = await execute(args, {\n // The bridge always supplies the parked call's id; a host invoking\n // the handler directly has no call to correlate.\n toolCallId: context?.toolCallId ?? '',\n abortSignal: controller.signal,\n human: () =>\n Promise.reject(\n new Error('Human input is not supported for statewire client tools.'),\n ),\n });\n return output instanceof ToolResponse ? output.result : output;\n },\n },\n ];\n });\n}\n\n/** Wire entries for `runConfig.custom.client_tools` — the shared contract from\n * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */\nexport function statewireClientToolWireEntries(\n tools: readonly RegisteredClientTool[],\n): Record<string, unknown>[] {\n return clientToolRunConfigEntries(tools);\n}\n\n/**\n * The frontend-toolkit entries that must ride `enabled_tools`: declarations\n * WITHOUT a local `execute`, which name registry tools the backend already\n * knows how to bind (Olympus's frontend-executed tool ids).\n *\n * Executable entries are deliberately left out. They are declared through\n * `client_tools`, and the run binds them by name; putting the same name into\n * `enabled_tools` would also switch on any backend toolkit that happens to\n * own a tool of that name, and the backend wins the resulting collision —\n * the client tool is dropped and the model calls the backend one instead.\n */\nexport function backendDeclaredFrontendToolIds(toolkit: Toolkit): string[] {\n return Object.entries(toolkit)\n .filter(([, entry]) => !hasCallableExecute(entry))\n .map(([name]) => name);\n}\n\n/**\n * The `app_id` values the deep-agent runtime accepts as a client-tool surface.\n * Mirrors `agora/agora/graphs/athena_deep_agent/client_tools.py`: the fixed\n * first-party ids plus the `computer_asset_<uuid>` shape Athena's provisioning\n * injects into a computer-asset app. Any other `app_id` (or none) gets no\n * surface, and the runtime drops every `client_tools` declaration for the run.\n */\nexport const CLIENT_TOOL_SURFACE_APP_IDS = {\n olympus: 'olympus',\n officeAddin: 'athena-office-addin',\n mobile: 'athena-mobile',\n} as const;\n\nconst SDK_APP_ID_PATTERN = /^computer_asset_[0-9a-f-]{8,64}$/;\n\nexport type ClientToolSurface = 'web' | 'office' | 'mobile' | 'sdk';\n\n/** The surface an `app_id` selects, or null when the runtime would refuse it. */\nexport function resolveClientToolSurface(appId: string | undefined | null): ClientToolSurface | null {\n if (typeof appId !== 'string' || appId.length === 0) return null;\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.olympus) return 'web';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.officeAddin) return 'office';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.mobile) return 'mobile';\n if (SDK_APP_ID_PATTERN.test(appId)) return 'sdk';\n return null;\n}\n"],"names":["toJSONSchema","athenaDiagnostics","ToolResponse","clientToolRunConfigEntries"],"mappings":";;;;;AAgBA,SAAS,mBACP,OACoF;AACpF,SACE,CAAC,CAAC,SACF,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY;AAExD;AAeO,SAAS,4BAA4B,SAA0C;AACpF,QAAM,aAAa,OAAO,QAAQ,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,MAAM,mBAAmB,KAAK,CAAC;AAC1F,MAAI,WAAW,WAAW,EAAG,QAAO,CAAA;AAEpC,SAAO,WAAW,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM;AAC3C,QAAI,MAAM,eAAe,OAAW,QAAO,CAAA;AAE3C,QAAI;AACJ,QAAI;AACF,mBAAa,EAAE,GAAGA,gBAAAA,aAAa,MAAM,UAAU,EAAA;AAAA,IACjD,SAAS,OAAO;AACdC,wBAAAA,kBAAkB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,QACP,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAAA;AAAA,MAC9D,CACD;AACD,aAAO,CAAA;AAAA,IACT;AAEA,UAAM,UAAW,MAEd;AACH,WAAO;AAAA,MACL;AAAA,QACE,YAAY;AAAA,UACV;AAAA,UACA,aAAa,MAAM,eAAe;AAAA,UAClC;AAAA,UACA,eAAe;AAAA,QAAA;AAAA,QAEjB,SAAS,OAAO,MAAM,YAAuC;AAC3D,gBAAM,aAAa,IAAI,gBAAA;AACvB,gBAAM,SAAS,MAAM,QAAQ,MAAM;AAAA;AAAA;AAAA,YAGjC,aAAY,mCAAS,eAAc;AAAA,YACnC,aAAa,WAAW;AAAA,YACxB,OAAO,MACL,QAAQ;AAAA,cACN,IAAI,MAAM,0DAA0D;AAAA,YAAA;AAAA,UACtE,CACH;AACD,iBAAO,kBAAkBC,gBAAAA,eAAe,OAAO,SAAS;AAAA,QAC1D;AAAA,MAAA;AAAA,IACF;AAAA,EAEJ,CAAC;AACH;AAIO,SAAS,+BACd,OAC2B;AAC3B,SAAOC,YAAAA,2BAA2B,KAAK;AACzC;AAaO,SAAS,+BAA+B,SAA4B;AACzE,SAAO,OAAO,QAAQ,OAAO,EAC1B,OAAO,CAAC,CAAA,EAAG,KAAK,MAAM,CAAC,mBAAmB,KAAK,CAAC,EAChD,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AACzB;AASO,MAAM,8BAA8B;AAAA,EACzC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AACV;AAEA,MAAM,qBAAqB;AAKpB,SAAS,yBAAyB,OAA4D;AACnG,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI,UAAU,4BAA4B,QAAS,QAAO;AAC1D,MAAI,UAAU,4BAA4B,YAAa,QAAO;AAC9D,MAAI,UAAU,4BAA4B,OAAQ,QAAO;AACzD,MAAI,mBAAmB,KAAK,KAAK,EAAG,QAAO;AAC3C,SAAO;AACT;;;;;;"}
1
+ {"version":3,"file":"statewire-client-tools.cjs","sources":["../../src/chat/statewire-client-tools.ts"],"sourcesContent":["import { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport type { Toolkit } from '@assistant-ui/react';\nimport {\n type ClientToolHandlerContext,\n clientToolRunConfigEntries,\n type RegisteredClientTool,\n} from '@athenaintel/agent-runtime';\nimport { ToolResponse, toJSONSchema } from 'assistant-stream';\n\n/**\n * @deprecated The SDK's client tools ARE the shared `RegisteredClientTool`\n * shape from `@athenaintel/agent-runtime` (`definition` + `handler`); import\n * that type instead. Retained for one release so existing imports type-check.\n */\nexport type StatewireClientTool = RegisteredClientTool;\n\n/**\n * Marks a frontend tool as one that changes something for the person, so its\n * calls wait for their consent. Set by {@link requiresWriteConsent}. A\n * registered symbol (not a module-local one), so the mark is read by whichever\n * copy of the SDK collects the toolkit.\n */\nconst CLIENT_TOOL_WRITE_CONSENT = Symbol.for('athena.clientTool.writeConsent');\n\nexport interface ClientToolWriteConsentOptions {\n /**\n * What the tool does, as the rest of the sentence \"… wants to ___\" on the\n * consent card (e.g. `\"click and type on this page\"`). The card names the\n * tool when this is absent.\n */\n summary?: string;\n}\n\n/**\n * Mark a `frontendTools` entry as a write: the person is asked before its\n * first call in a conversation runs, the answer covers the rest of that\n * conversation, and a declined call answers the model with an error instead\n * of running. Unmarked tools run without asking, as before.\n *\n * ```ts\n * const frontendTools = {\n * save_note: requiresWriteConsent(\n * { description: '…', parameters, execute: saveNote },\n * { summary: 'save a note on this record' },\n * ),\n * };\n * ```\n *\n * The mark also travels to Athena (`requires_write`), which then never\n * answers the call on the person's behalf while their tab is away.\n */\nexport function requiresWriteConsent<T extends object>(\n tool: T,\n options: ClientToolWriteConsentOptions = {},\n): T {\n const summary = options.summary?.trim();\n return { ...tool, [CLIENT_TOOL_WRITE_CONSENT]: summary ? { summary } : {} };\n}\n\n/** The consent mark on a toolkit entry, or `null` for a tool that never asks. */\nexport function clientToolWriteConsent(entry: unknown): ClientToolWriteConsentOptions | null {\n if (!entry || typeof entry !== 'object') return null;\n const mark = (entry as Record<symbol, unknown>)[CLIENT_TOOL_WRITE_CONSENT];\n if (!mark || typeof mark !== 'object') return null;\n const summary = (mark as { summary?: unknown }).summary;\n return typeof summary === 'string' && summary ? { summary } : {};\n}\n\n/** Each consent-marked tool's own words for what it does, by tool name. */\nexport function clientToolWriteConsentSummaries(\n toolkit: Toolkit | undefined,\n): Readonly<Record<string, string>> {\n const summaries: Record<string, string> = {};\n for (const [name, entry] of Object.entries(toolkit ?? {})) {\n const summary = clientToolWriteConsent(entry)?.summary;\n if (summary) summaries[name] = summary;\n }\n return summaries;\n}\n\nfunction hasCallableExecute(\n entry: unknown,\n): entry is { execute: (args: Record<string, unknown>, context: unknown) => unknown } {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as { execute?: unknown }).execute === 'function'\n );\n}\n\n/**\n * Collect the client-executable tools from a frontend toolkit, in the shared\n * `RegisteredClientTool` shape every statewire surface declares and executes.\n *\n * The deep-agent runtime drops frontend (`ui_*`) tool definitions, so tools\n * with a local `execute` must instead be declared through\n * `runConfig.custom.client_tools` and answered by the statewire client-tool\n * bridge. Schema conversion reuses `toJSONSchema` — the same conversion\n * primitive as the legacy assistant-transport — and a tool whose schema cannot\n * be converted is skipped with a warning rather than failing the whole set.\n * A tool gates on write consent only when its entry is marked with\n * `requiresWriteConsent`: an unmarked toolkit `execute` is the host app's own\n * code and runs without asking.\n */\nexport function collectStatewireClientTools(toolkit: Toolkit): RegisteredClientTool[] {\n const executable = Object.entries(toolkit).filter(([, entry]) => hasCallableExecute(entry));\n if (executable.length === 0) return [];\n\n return executable.flatMap(([name, entry]) => {\n if (entry.parameters === undefined) return [];\n\n let parameters: Record<string, unknown>;\n try {\n parameters = { ...toJSONSchema(entry.parameters) };\n } catch (error) {\n athenaDiagnostics.emit('tools.schema.warning', {\n level: 'warn',\n data: {\n tool: name,\n message: 'Frontend tool schema could not be converted for statewire; the tool is not registered',\n error: error instanceof Error ? error.message : String(error),\n },\n });\n return [];\n }\n\n const execute = (entry as {\n execute: (args: Record<string, unknown>, context: unknown) => unknown;\n }).execute;\n return [\n {\n definition: {\n name,\n description: entry.description ?? '',\n parameters,\n requiresWrite: clientToolWriteConsent(entry) !== null,\n },\n handler: async (args, context?: ClientToolHandlerContext) => {\n const output = await execute(args, {\n // The bridge always supplies the parked call's id and its signal,\n // which aborts when the call's request goes away (the run was\n // stopped); a host invoking the handler directly has neither.\n toolCallId: context?.toolCallId ?? '',\n abortSignal: context?.signal ?? new AbortController().signal,\n human: () =>\n Promise.reject(\n new Error('Human input is not supported for statewire client tools.'),\n ),\n });\n return output instanceof ToolResponse ? output.result : output;\n },\n },\n ];\n });\n}\n\n/** Wire entries for `runConfig.custom.client_tools` — the shared contract from\n * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */\nexport function statewireClientToolWireEntries(\n tools: readonly RegisteredClientTool[],\n): Record<string, unknown>[] {\n return clientToolRunConfigEntries(tools);\n}\n\n/**\n * The frontend-toolkit entries that must ride `enabled_tools`: declarations\n * WITHOUT a local `execute`, which name registry tools the backend already\n * knows how to bind (Olympus's frontend-executed tool ids).\n *\n * Executable entries are deliberately left out. They are declared through\n * `client_tools`, and the run binds them by name; putting the same name into\n * `enabled_tools` would also switch on any backend toolkit that happens to\n * own a tool of that name, and the backend wins the resulting collision —\n * the client tool is dropped and the model calls the backend one instead.\n */\nexport function backendDeclaredFrontendToolIds(toolkit: Toolkit): string[] {\n return Object.entries(toolkit)\n .filter(([, entry]) => !hasCallableExecute(entry))\n .map(([name]) => name);\n}\n\n/**\n * The `app_id` values the deep-agent runtime accepts as a client-tool surface.\n * Mirrors `agora/agora/graphs/athena_deep_agent/client_tools.py`: the fixed\n * first-party ids plus the `computer_asset_<uuid>` shape Athena's provisioning\n * injects into a computer-asset app. Any other `app_id` (or none) gets no\n * surface, and the runtime drops every `client_tools` declaration for the run.\n */\nexport const CLIENT_TOOL_SURFACE_APP_IDS = {\n olympus: 'olympus',\n officeAddin: 'athena-office-addin',\n mobile: 'athena-mobile',\n} as const;\n\nconst SDK_APP_ID_PATTERN = /^computer_asset_[0-9a-f-]{8,64}$/;\n\nexport type ClientToolSurface = 'web' | 'office' | 'mobile' | 'sdk';\n\n/** The surface an `app_id` selects, or null when the runtime would refuse it. */\nexport function resolveClientToolSurface(appId: string | undefined | null): ClientToolSurface | null {\n if (typeof appId !== 'string' || appId.length === 0) return null;\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.olympus) return 'web';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.officeAddin) return 'office';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.mobile) return 'mobile';\n if (SDK_APP_ID_PATTERN.test(appId)) return 'sdk';\n return null;\n}\n"],"names":["toJSONSchema","athenaDiagnostics","ToolResponse","clientToolRunConfigEntries"],"mappings":"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"}