omnichatkit 0.0.23-b → 0.0.24-b

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.
package/README.md CHANGED
@@ -485,6 +485,37 @@ This proxy ensures unauthorized requests are immediately dropped (returning `401
485
485
  ### 9. Working with AI Reasoning (e.g. DeepSeek `<think>`)
486
486
  OmniChatKit automatically parses and extracts `<think>` tags from incoming model streams. It strips these out of the primary text response and renders them natively as a beautiful, collapsible "Reasoning" accordion inside the message block! No extra configuration is required.
487
487
 
488
+ ### 10. Frontend Action Tooling (AG-UI)
489
+
490
+ OmniChatKit provides a powerful way to define frontend tools that the AI agent can call using the `useAGUIAction` hook. This allows you to integrate application-specific behaviors like UI actions, user confirmations, or data fetching seamlessly into the agent's workflow.
491
+
492
+ ```tsx
493
+ import { useAGUIAction } from "omnichatkit";
494
+
495
+ function MyInteractiveComponent() {
496
+ useAGUIAction({
497
+ name: "confirmAction",
498
+ description: "Ask the user to confirm a specific action before proceeding",
499
+ parameters: {
500
+ type: "object",
501
+ properties: {
502
+ action: { type: "string", description: "The action that needs user confirmation" }
503
+ },
504
+ required: ["action"],
505
+ },
506
+ handler: async ({ action }) => {
507
+ // Execute your frontend logic, like showing a confirmation dialog
508
+ const confirmed = window.confirm(`Proceed with: ${action}?`);
509
+ return confirmed ? "approved" : "rejected";
510
+ },
511
+ });
512
+
513
+ return <div>My Component</div>;
514
+ }
515
+ ```
516
+
517
+ Registered tools are automatically injected into the `RunAgentInput.tools` payload during the AG-UI agent turn, and the chat hook natively awaits the `handler` execution and appends the result to the conversation automatically.
518
+
488
519
  ---
489
520
 
490
521
  ## Core Architecture
@@ -515,10 +546,11 @@ OmniChatKit exports all necessary hooks, components, and types to give you full
515
546
 
516
547
  **Hooks**
517
548
  - `useAIChatStore`
518
- - `useChatContext` — auto-selects the correct context based on the active provider
519
- - `useAIChatContext` — explicit classic (Vercel AI SDK) context accessor
520
- - `useAGUIChatContext` — explicit AG-UI context accessor
549
+ - `useChatContext` auto-selects the correct context based on the active provider
550
+ - `useAIChatContext` explicit classic (Vercel AI SDK) context accessor
551
+ - `useAGUIChatContext` explicit AG-UI context accessor
521
552
  - `useAGUIChat`
553
+ - `useAGUIAction`
522
554
  - `useHITL`
523
555
  - `useInterrupts`
524
556
 
package/dist/index.cjs CHANGED
@@ -151,6 +151,16 @@ const useAIChatStore = (0, zustand.create)()((0, zustand_middleware.persist)((se
151
151
  setPendingHITLAction: (action) => set({ pendingHITLAction: action }),
152
152
  resumeExecution: () => {},
153
153
  setResumeExecution: (fn) => set({ resumeExecution: fn }),
154
+ actions: {},
155
+ registerAction: (action) => set((state) => ({ actions: {
156
+ ...state.actions,
157
+ [action.name]: action
158
+ } })),
159
+ unregisterAction: (name) => set((state) => {
160
+ const newActions = { ...state.actions };
161
+ delete newActions[name];
162
+ return { actions: newActions };
163
+ }),
154
164
  sessions: [],
155
165
  activeSessionId: null,
156
166
  setSessions: (sessions) => {
@@ -570,7 +580,7 @@ function useAIChatContext() {
570
580
  * @param config.agentId - The specific agent identifier, appended to the API route.
571
581
  * @returns An object matching the Vercel AI SDK `UseChatHelpers` interface, plus custom event data.
572
582
  */
573
- function useAGUIChat({ api, body, agentId }) {
583
+ function useAGUIChat({ api, body, agentId, agentDescription }) {
574
584
  const [messages, setMessages] = (0, react.useState)([]);
575
585
  const [input, setInput] = (0, react.useState)("");
576
586
  const [status, setStatus] = (0, react.useState)("idle");
@@ -591,12 +601,17 @@ function useAGUIChat({ api, body, agentId }) {
591
601
  return new _ag_ui_client.HttpAgent({
592
602
  url: finalApiUrl,
593
603
  threadId: currentSessionId,
604
+ description: agentDescription,
594
605
  fetch: (fetchUrl, init) => fetch(fetchUrl, {
595
606
  ...init,
596
607
  signal: abortControllerRef.current?.signal || init?.signal
597
608
  })
598
609
  });
599
- }, [agentId, api]);
610
+ }, [
611
+ agentId,
612
+ api,
613
+ agentDescription
614
+ ]);
600
615
  (0, react.useEffect)(() => {
601
616
  const currentSessionId = body?.sessionId;
602
617
  agentRef.current = createAgent(currentSessionId);
@@ -641,12 +656,17 @@ function useAGUIChat({ api, body, agentId }) {
641
656
  setStatus("submitted");
642
657
  setError(void 0);
643
658
  setEvents([]);
644
- try {
645
- const aguiMessages = messagesRef.current.map((m) => ({ ...m }));
659
+ const runAgentTurn = async (currentMessages) => {
646
660
  setStatus("streaming");
647
- agentRef.current.setMessages(aguiMessages);
661
+ agentRef.current.setMessages(currentMessages);
648
662
  abortControllerRef.current = new AbortController();
649
- await agentRef.current.runAgent({}, {
663
+ const actions = useAIChatStore.getState().actions || {};
664
+ const tools = Object.values(actions).map((a) => ({
665
+ name: a.name,
666
+ description: a.description,
667
+ parameters: a.parameters
668
+ }));
669
+ await agentRef.current.runAgent({ tools: tools.length > 0 ? tools : void 0 }, {
650
670
  onEvent: (params) => {
651
671
  setEvents((prev) => [...prev, params.event]);
652
672
  },
@@ -701,8 +721,44 @@ function useAGUIChat({ api, body, agentId }) {
701
721
  }
702
722
  }
703
723
  });
724
+ const newMessages = messagesRef.current;
725
+ const latestMessage = newMessages[newMessages.length - 1];
726
+ if (latestMessage && latestMessage.toolInvocations && latestMessage.toolInvocations.length > 0) {
727
+ let hasToolResult = false;
728
+ const newToolResults = [];
729
+ for (const invocation of latestMessage.toolInvocations) {
730
+ const action = actions[invocation.toolName];
731
+ if (action && action.handler) try {
732
+ const res = await action.handler(invocation.args);
733
+ newToolResults.push({
734
+ id: Date.now().toString() + Math.random().toString().slice(2, 6),
735
+ role: "tool",
736
+ content: typeof res === "string" ? res : JSON.stringify(res),
737
+ toolCallId: invocation.toolCallId
738
+ });
739
+ hasToolResult = true;
740
+ } catch (err) {
741
+ newToolResults.push({
742
+ id: Date.now().toString() + Math.random().toString().slice(2, 6),
743
+ role: "tool",
744
+ content: String(err),
745
+ error: String(err),
746
+ toolCallId: invocation.toolCallId
747
+ });
748
+ hasToolResult = true;
749
+ }
750
+ }
751
+ if (hasToolResult) {
752
+ messagesRef.current = [...messagesRef.current, ...newToolResults];
753
+ setMessages(messagesRef.current);
754
+ return await runAgentTurn(messagesRef.current);
755
+ }
756
+ }
704
757
  setStatus("ready");
705
758
  return lastAssistantResponseRef.current;
759
+ };
760
+ try {
761
+ return await runAgentTurn(messagesRef.current);
706
762
  } catch (err) {
707
763
  if (String(err).toLowerCase().includes("aborted") || err instanceof Error && err.name === "AbortError") setMessages((prev) => {
708
764
  const lastMsg = prev[prev.length - 1];
@@ -770,7 +826,7 @@ const AGUIChatContext = (0, react.createContext)(null);
770
826
  * @param props - Provider configuration.
771
827
  * @returns A context provider wrapping the chat UI components.
772
828
  */
773
- function AGUIChatProvider({ children, theme = "standard", apiEndpoint = "/api/agent", agentId, sessionId, sessionStorageMode = "disabled", sessionRoute = "/session" }) {
829
+ function AGUIChatProvider({ children, theme = "standard", apiEndpoint = "/api/agent", agentId, agentDescription, sessionId, sessionStorageMode = "disabled", sessionRoute = "/session" }) {
774
830
  const setTheme = useAIChatStore((state) => state.setTheme);
775
831
  const setSessionStorageMode = useAIChatStore((state) => state.setSessionStorageMode);
776
832
  const setSessionRoute = useAIChatStore((state) => state.setSessionRoute);
@@ -801,12 +857,14 @@ function AGUIChatProvider({ children, theme = "standard", apiEndpoint = "/api/ag
801
857
  const chatHelpers = useAGUIChat({
802
858
  api: apiEndpoint,
803
859
  body: sessionsEnabled ? activeSessionId ? { sessionId: activeSessionId } : sessionId ? { sessionId } : void 0 : void 0,
804
- agentId
860
+ agentId,
861
+ agentDescription
805
862
  });
806
863
  const autoTitledSessionIdsRef = react.default.useRef(/* @__PURE__ */ new Set());
807
864
  const titleChatHelpers = useAGUIChat({
808
865
  api: apiEndpoint,
809
- agentId
866
+ agentId,
867
+ agentDescription
810
868
  });
811
869
  const generateInitialSessionTitle = react.default.useCallback(async (messages) => {
812
870
  if (!sessionsEnabled) throw new Error("Session titles require sessionStorageMode to be \"memory\" or \"api\".");
@@ -901,6 +959,32 @@ function useAGUIChatContext() {
901
959
  return context;
902
960
  }
903
961
  //#endregion
962
+ //#region src/hooks/useAGUIAction.ts
963
+ /**
964
+ * Registers an AG-UI action (tool) to be made available to the agent backend.
965
+ * The action is passed into the RunAgentInput.tools array when useAGUIChat runs.
966
+ * When the backend requests this tool, its handler will be executed.
967
+ *
968
+ * @param action - The action definition including name, description, parameters schema and a handler.
969
+ */
970
+ function useAGUIAction(action) {
971
+ const registerAction = useAIChatStore((state) => state.registerAction);
972
+ const unregisterAction = useAIChatStore((state) => state.unregisterAction);
973
+ (0, react.useEffect)(() => {
974
+ registerAction(action);
975
+ return () => {
976
+ unregisterAction(action.name);
977
+ };
978
+ }, [
979
+ action.name,
980
+ action.description,
981
+ action.handler,
982
+ action.parameters,
983
+ registerAction,
984
+ unregisterAction
985
+ ]);
986
+ }
987
+ //#endregion
904
988
  //#region src/hooks/useChatContext.ts
905
989
  /**
906
990
  * `useChatContext` is a convenience hook that automatically returns the correct
@@ -5205,7 +5289,7 @@ function ChatManager({ theme, className, style, chatManagerComponentStyles = {},
5205
5289
  className: "flex gap-2 w-full items-end relative",
5206
5290
  children: [
5207
5291
  inputTypeList && inputTypeList.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5208
- className: "relative shrink-0 flex items-center justify-center h-full mb-1",
5292
+ className: "relative z-50 shrink-0 flex items-center justify-center mb-1",
5209
5293
  ref: attachMenuRef,
5210
5294
  children: [
5211
5295
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -5219,7 +5303,7 @@ function ChatManager({ theme, className, style, chatManagerComponentStyles = {},
5219
5303
  type: "button",
5220
5304
  variant: "outline",
5221
5305
  size: "icon",
5222
- className: (0, cn.cn)("rounded-full w-10 h-10 border-dashed", typeof inputSectionStyle.attachmentMenuStyles?.plusButtonContainerStyles === "string" ? inputSectionStyle.attachmentMenuStyles.plusButtonContainerStyles : ""),
5306
+ className: (0, cn.cn)("rounded-full w-10 h-10 border-dashed relative z-50 bg-background", typeof inputSectionStyle.attachmentMenuStyles?.plusButtonContainerStyles === "string" ? inputSectionStyle.attachmentMenuStyles.plusButtonContainerStyles : ""),
5223
5307
  style: typeof inputSectionStyle.attachmentMenuStyles?.plusButtonContainerStyles === "object" ? inputSectionStyle.attachmentMenuStyles.plusButtonContainerStyles : void 0,
5224
5308
  onClick: () => setIsAttachMenuOpen(!isAttachMenuOpen),
5225
5309
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -5231,67 +5315,62 @@ function ChatManager({ theme, className, style, chatManagerComponentStyles = {},
5231
5315
  })
5232
5316
  })
5233
5317
  }),
5234
- isAttachMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5235
- className: (0, cn.cn)("absolute bottom-[calc(100%+0.5rem)] left-0 bg-background/80 backdrop-blur-md border border-border/50 shadow-lg rounded-xl p-1 z-50 flex flex-col min-w-[120px] animate-in fade-in zoom-in duration-200 origin-bottom-left", typeof inputSectionStyle.attachmentMenuStyles?.menuContainerStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuContainerStyles : ""),
5236
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuContainerStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuContainerStyles : void 0,
5318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5319
+ className: "absolute inset-0 flex items-center justify-center pointer-events-none z-40",
5237
5320
  children: [
5238
- inputTypeList.includes("image") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5239
- type: "button",
5240
- className: (0, cn.cn)("flex items-center gap-2.5 px-2.5 py-2 text-xs hover:bg-accent hover:text-accent-foreground rounded-lg text-left transition-colors text-foreground font-medium", typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : ""),
5241
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : void 0,
5242
- onClick: () => triggerFileInput("image/*"),
5243
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5244
- className: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : "",
5245
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : void 0,
5246
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Image, {
5247
- size: 14,
5248
- className: "text-zinc-500"
5249
- })
5250
- }), "Image"]
5251
- }),
5252
- inputTypeList.includes("document") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5253
- type: "button",
5254
- className: (0, cn.cn)("flex items-center gap-2.5 px-2.5 py-2 text-xs hover:bg-accent hover:text-accent-foreground rounded-lg text-left transition-colors text-foreground font-medium", typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : ""),
5255
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : void 0,
5256
- onClick: () => triggerFileInput(".pdf,.doc,.docx,.txt,application/pdf,text/plain"),
5257
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5258
- className: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : "",
5259
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : void 0,
5260
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FileText, {
5261
- size: 14,
5262
- className: "text-zinc-500"
5263
- })
5264
- }), "Document"]
5265
- }),
5266
- inputTypeList.includes("audio") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5267
- type: "button",
5268
- className: (0, cn.cn)("flex items-center gap-2.5 px-2.5 py-2 text-xs hover:bg-accent hover:text-accent-foreground rounded-lg text-left transition-colors text-foreground font-medium", typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : ""),
5269
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : void 0,
5270
- onClick: () => triggerFileInput("audio/*"),
5271
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5272
- className: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : "",
5273
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : void 0,
5274
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Mic, {
5275
- size: 14,
5276
- className: "text-zinc-500"
5277
- })
5278
- }), "Audio"]
5279
- }),
5280
- inputTypeList.includes("video") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5321
+ {
5322
+ type: "image",
5323
+ icon: lucide_react.Image,
5324
+ label: "Image",
5325
+ accept: "image/*"
5326
+ },
5327
+ {
5328
+ type: "document",
5329
+ icon: lucide_react.FileText,
5330
+ label: "Document",
5331
+ accept: ".pdf,.doc,.docx,.txt,application/pdf,text/plain"
5332
+ },
5333
+ {
5334
+ type: "audio",
5335
+ icon: lucide_react.Mic,
5336
+ label: "Audio",
5337
+ accept: "audio/*"
5338
+ },
5339
+ {
5340
+ type: "video",
5341
+ icon: lucide_react.Video,
5342
+ label: "Video",
5343
+ accept: "video/*"
5344
+ }
5345
+ ].filter((item) => inputTypeList.includes(item.type)).map((item, idx, arr) => {
5346
+ const isOpen = isAttachMenuOpen;
5347
+ const rad = ((arr.length === 1 ? 45 : 0 + 85 / (arr.length - 1) * idx) - 90) * (Math.PI / 180);
5348
+ const radius = 90;
5349
+ const x = isOpen ? Math.cos(rad) * radius : 0;
5350
+ const y = isOpen ? Math.sin(rad) * radius : 0;
5351
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5281
5352
  type: "button",
5282
- className: (0, cn.cn)("flex items-center gap-2.5 px-2.5 py-2 text-xs hover:bg-accent hover:text-accent-foreground rounded-lg text-left transition-colors text-foreground font-medium", typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : ""),
5283
- style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : void 0,
5284
- onClick: () => triggerFileInput("video/*"),
5285
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5353
+ title: item.label,
5354
+ onClick: () => {
5355
+ triggerFileInput(item.accept);
5356
+ setIsAttachMenuOpen(false);
5357
+ },
5358
+ className: (0, cn.cn)("absolute top-0 left-0 w-10 h-10 rounded-full bg-background border border-border shadow-md flex items-center justify-center text-foreground hover:bg-accent transition-all duration-300 pointer-events-auto", isOpen ? "opacity-100" : "opacity-0 pointer-events-none", typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : ""),
5359
+ style: {
5360
+ transform: `translate(${x}px, ${y}px) scale(${isOpen ? 1 : .5})`,
5361
+ transitionDelay: isOpen ? `${idx * 40}ms` : "0ms",
5362
+ ...typeof inputSectionStyle.attachmentMenuStyles?.menuItemStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemStyles : void 0
5363
+ },
5364
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5286
5365
  className: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "string" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : "",
5287
5366
  style: typeof inputSectionStyle.attachmentMenuStyles?.menuItemIconStyles === "object" ? inputSectionStyle.attachmentMenuStyles.menuItemIconStyles : void 0,
5288
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Video, {
5289
- size: 14,
5367
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(item.icon, {
5368
+ size: 16,
5290
5369
  className: "text-zinc-500"
5291
5370
  })
5292
- }), "Video"]
5293
- })
5294
- ]
5371
+ })
5372
+ }, item.type);
5373
+ })
5295
5374
  })
5296
5375
  ]
5297
5376
  }),
@@ -6366,12 +6445,14 @@ function OmniChat({ apiMode, theme, a2uiProps, apiEndpoint, chatApiSchema, chatM
6366
6445
  a2uiProps?.a2uiRenderingOption;
6367
6446
  const Provider = apiMode === "ag-ui" ? AGUIChatProvider : AIChatProvider;
6368
6447
  const agentId = chatManagerProps?.agentId;
6448
+ const agentDescription = chatManagerProps?.agentDescription;
6369
6449
  const sessionId = chatManagerProps?.sessionId;
6370
6450
  const normalizedSessionRoute = sessionRoute.startsWith("/") ? sessionRoute : `/${sessionRoute}`;
6371
6451
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Provider, {
6372
6452
  theme,
6373
6453
  apiEndpoint,
6374
6454
  agentId,
6455
+ agentDescription,
6375
6456
  sessionId,
6376
6457
  sessionStorageMode,
6377
6458
  sessionRoute: normalizedSessionRoute,
@@ -6551,6 +6632,7 @@ exports.catalog = catalog;
6551
6632
  exports.catalogSchema = catalogSchema;
6552
6633
  exports.definitions = definitions;
6553
6634
  exports.surfaceBus = surfaceBus;
6635
+ exports.useAGUIAction = useAGUIAction;
6554
6636
  exports.useAGUIChat = useAGUIChat;
6555
6637
  exports.useAGUIChatContext = useAGUIChatContext;
6556
6638
  exports.useAIChatContext = useAIChatContext;