├── card-issue-agent
├── src
│ ├── client
│ │ ├── vite-env.d.ts
│ │ ├── main.tsx
│ │ ├── App.tsx
│ │ ├── components
│ │ │ └── ui
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── code-block.tsx
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── message.tsx
│ │ │ │ ├── prompt-suggestion.tsx
│ │ │ │ ├── prompt-input.tsx
│ │ │ │ └── chat-container.tsx
│ │ ├── index.css
│ │ └── Chat.tsx
│ ├── lib
│ │ └── utils.ts
│ └── agent
│ │ ├── api.ts
│ │ ├── helper.ts
│ │ ├── app.ts
│ │ ├── constants.ts
│ │ ├── tools.ts
│ │ └── index.ts
├── public
│ ├── knock-logo.png
│ ├── slope-logo.png
│ └── vite.svg
├── tsconfig.worker.json
├── tsconfig.json
├── index.html
├── .gitignore
├── components.json
├── vite.config.ts
├── tsconfig.node.json
├── tsconfig.app.json
├── eslint.config.js
├── README.md
├── wrangler.json
└── package.json
├── agent-sdk-chat-with-approval
├── .env.example
├── src
│ ├── client
│ │ ├── vite-env.d.ts
│ │ ├── main.tsx
│ │ ├── Notifications.tsx
│ │ ├── index.css
│ │ └── App.tsx
│ ├── lib
│ │ └── utils.ts
│ ├── components
│ │ └── ui
│ │ │ ├── textarea.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── button.tsx
│ │ │ ├── code-block.tsx
│ │ │ ├── markdown.tsx
│ │ │ ├── message.tsx
│ │ │ ├── prompt-suggestion.tsx
│ │ │ ├── prompt-input.tsx
│ │ │ └── chat-container.tsx
│ └── agent
│ │ ├── tools.ts
│ │ ├── helper.ts
│ │ └── index.ts
├── tsconfig.worker.json
├── tsconfig.json
├── index.html
├── .gitignore
├── components.json
├── vite.config.ts
├── tsconfig.node.json
├── tsconfig.app.json
├── eslint.config.js
├── public
│ └── vite.svg
├── README.md
├── wrangler.json
└── package.json
├── README.md
└── LICENSE
/card-issue-agent/src/client/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/.env.example:
--------------------------------------------------------------------------------
1 | VITE_KNOCK_PUBLIC_KEY=
2 | VITE_KNOCK_FEED_ID=
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/client/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/card-issue-agent/public/knock-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knocklabs/ai-agent-examples/HEAD/card-issue-agent/public/knock-logo.png
--------------------------------------------------------------------------------
/card-issue-agent/public/slope-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knocklabs/ai-agent-examples/HEAD/card-issue-agent/public/slope-logo.png
--------------------------------------------------------------------------------
/card-issue-agent/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/card-issue-agent/tsconfig.worker.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.node.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
5 | "types": ["vite/client", "./worker-configuration.d.ts"]
6 | },
7 | "include": ["src/worker"]
8 | }
9 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/tsconfig.worker.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.node.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
5 | "types": ["vite/client", "./worker-configuration.d.ts"]
6 | },
7 | "include": ["src/worker"]
8 | }
9 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App.tsx";
5 |
6 | createRoot(document.getElementById("root")!).render(
7 |
8 |
9 | ,
10 | );
11 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/client/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App.tsx";
5 |
6 | createRoot(document.getElementById("root")!).render(
7 |
8 |
9 | ,
10 | );
11 |
--------------------------------------------------------------------------------
/card-issue-agent/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" },
6 | { "path": "./tsconfig.worker.json" }
7 | ],
8 | "compilerOptions": {
9 | "baseUrl": ".",
10 | "paths": {
11 | "@/*": ["./src/*"]
12 | },
13 | "types": ["@cloudflare/workers-types/2023-07-01"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" },
6 | { "path": "./tsconfig.worker.json" }
7 | ],
8 | "compilerOptions": {
9 | "baseUrl": ".",
10 | "paths": {
11 | "@/*": ["./src/*"]
12 | },
13 | "types": ["@cloudflare/workers-types/2023-07-01"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/card-issue-agent/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Knock + Cloudflare Agent HITL example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Knock + Cloudflare Agent HITL example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/card-issue-agent/src/agent/api.ts:
--------------------------------------------------------------------------------
1 | export type IssuedCard = {
2 | cardId: string;
3 | last4: string;
4 | expiry: string;
5 | userId: string;
6 | name: string;
7 | };
8 |
9 | export async function issueCard(userId: string, name: string) {
10 | return {
11 | cardId: `card_${Math.random().toString(36).substring(2, 15)}`,
12 | userId,
13 | name,
14 | last4: "4242",
15 | expiry: "12/2028",
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/card-issue-agent/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # wrangler files
27 | .wrangler
28 | .dev.vars*
29 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # wrangler files
27 | .wrangler
28 | .dev.vars*
29 |
--------------------------------------------------------------------------------
/card-issue-agent/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/client/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/client/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/card-issue-agent/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import tailwindcss from "@tailwindcss/vite";
3 | import { defineConfig } from "vite";
4 | import react from "@vitejs/plugin-react";
5 | import { cloudflare } from "@cloudflare/vite-plugin";
6 |
7 | export default defineConfig({
8 | plugins: [react(), cloudflare(), tailwindcss()],
9 | server: {
10 | allowedHosts: ["26e9-47-185-68-94.ngrok-free.app", "localhost"],
11 | },
12 | resolve: {
13 | alias: {
14 | "@": path.resolve(__dirname, "./src"),
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import tailwindcss from "@tailwindcss/vite";
3 | import { defineConfig } from "vite";
4 | import react from "@vitejs/plugin-react";
5 | import { cloudflare } from "@cloudflare/vite-plugin";
6 |
7 | export default defineConfig({
8 | plugins: [react(), cloudflare(), tailwindcss()],
9 | server: {
10 | allowedHosts: ["a8b8-141-157-241-19.ngrok-free.app", "localhost"],
11 | },
12 | resolve: {
13 | alias: {
14 | "@": path.resolve(__dirname, "./src"),
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Knock Agent Toolkit examples
2 |
3 | A repository of AI Agent examples integrating Knock's [Agent Toolkit](https://docs.knock.app/developer-tools/agent-toolkit) across various Agent frameworks and platforms.
4 |
5 | You can use the [Knock Agent Toolkit](https://github.com/knocklabs/agent-toolkit) to integrate Knock into Agent applications, giving your Agent the ability to send cross-channel messaging and easily power deferred tool executions for human-in-the-loop interactions.
6 |
7 | ## Examples
8 |
9 | - [Cloudflare Agent SDK + Vercel AI SDK + HITL approval](/agent-sdk-chat-with-approval/README.md)
10 |
--------------------------------------------------------------------------------
/card-issue-agent/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/App.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { generateId } from "ai";
4 | import { useLocalStorage } from "usehooks-ts";
5 | import { useCallback, useEffect } from "react";
6 | import Chat from "./Chat";
7 |
8 | function App() {
9 | const [userId, setUserId] = useLocalStorage(
10 | "userId",
11 | undefined
12 | );
13 |
14 | const resetUserId = useCallback(() => {
15 | setUserId(generateId());
16 | }, [setUserId]);
17 |
18 | useEffect(() => {
19 | // Set the initial userId if it's not set
20 | if (!userId) resetUserId();
21 | }, [resetUserId, userId]);
22 |
23 | return userId ? (
24 |
25 | ) : null;
26 | }
27 |
28 | export default App;
29 |
--------------------------------------------------------------------------------
/card-issue-agent/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src/client"]
26 | }
27 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src/client"]
26 | }
27 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/card-issue-agent/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import reactHooks from "eslint-plugin-react-hooks";
4 | import reactRefresh from "eslint-plugin-react-refresh";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | { ignores: ["dist"] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ["**/*.{ts,tsx}"],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | "react-hooks": reactHooks,
18 | "react-refresh": reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | "react-refresh/only-export-components": [
23 | "warn",
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | );
29 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import reactHooks from "eslint-plugin-react-hooks";
4 | import reactRefresh from "eslint-plugin-react-refresh";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | { ignores: ["dist"] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ["**/*.{ts,tsx}"],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | "react-hooks": reactHooks,
18 | "react-refresh": reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | "react-refresh/only-export-components": [
23 | "warn",
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | );
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Knock Labs, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/client/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef } from "react";
2 | import {
3 | NotificationIconButton,
4 | NotificationFeedPopover,
5 | useKnockFeed,
6 | } from "@knocklabs/react";
7 |
8 | // Required CSS import, unless you're overriding the styling
9 | import "@knocklabs/react/dist/index.css";
10 |
11 | const Notifications = () => {
12 | const [isVisible, setIsVisible] = useState(false);
13 | const notifButtonRef = useRef(null);
14 |
15 | const { feedClient } = useKnockFeed();
16 |
17 | return (
18 |
19 |
20 | setIsVisible(!isVisible)}
23 | />
24 |
25 | {notifButtonRef.current && (
26 |
}
28 | isVisible={isVisible}
29 | onClose={() => setIsVisible(false)}
30 | onNotificationButtonClick={(item) => {
31 | feedClient.markAsArchived(item);
32 | return false;
33 | }}
34 | />
35 | )}
36 |
37 | );
38 | };
39 |
40 | export default Notifications;
41 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/agent/tools.ts:
--------------------------------------------------------------------------------
1 | import { createKnockToolkit } from "@knocklabs/agent/ai-sdk";
2 | import { tool } from "ai";
3 | import { z } from "zod";
4 |
5 | type Env = {
6 | KNOCK_SERVICE_TOKEN: string;
7 | };
8 |
9 | const addTool = tool({
10 | description: "Add two numbers together. ALWAYS use this tool when you are asked to do addition DO NOT assume the result.",
11 | parameters: z.object({
12 | a: z.number(),
13 | b: z.number(),
14 | }),
15 | execute: async ({ a, b }) => {
16 | return a + b;
17 | },
18 | });
19 |
20 | const subtractTool = tool({
21 | description: "Subtract two numbers together. ALWAYS use this tool when you are asked to do subtraction DO NOT assume the result.",
22 | parameters: z.object({
23 | a: z.number(),
24 | b: z.number(),
25 | }),
26 | execute: async ({ a, b }) => {
27 | return a - b;
28 | },
29 | });
30 |
31 | async function initializeToolkit(env: Env) {
32 | const toolkit = await createKnockToolkit({
33 | serviceToken: env.KNOCK_SERVICE_TOKEN,
34 | permissions: {},
35 | });
36 |
37 | const tools = toolkit.requireHumanInput({ add: addTool }, {
38 | workflow: "approve-tool-call",
39 | recipients: ["admin_user_1"],
40 | });
41 |
42 | return { toolkit, tools: { ...tools, subtract: subtractTool } };
43 | }
44 |
45 | export { initializeToolkit };
46 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "../../../lib/utils";
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | );
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | );
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | );
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback };
54 |
--------------------------------------------------------------------------------
/card-issue-agent/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/agent/helper.ts:
--------------------------------------------------------------------------------
1 | import { CoreAssistantMessage, CoreToolMessage, Message } from "ai";
2 | import { deferredToolCallToToolInvocation } from "@knocklabs/agent/ai-sdk";
3 | import { type DeferredToolCall } from "@knocklabs/agent/types";
4 |
5 | type ResponseMessage = (CoreAssistantMessage | CoreToolMessage) & {
6 | id: string;
7 | };
8 |
9 | /**
10 | * Returns a new assistant message that includes the deferred tool call and the result so it can be
11 | * shown in the UI.
12 | *
13 | * @param toolCall
14 | */
15 | export function responseToAssistantMessage(
16 | responseMessage: ResponseMessage,
17 | deferredToolCall: DeferredToolCall,
18 | toolResult: any
19 | ): Message {
20 | const toolInvocation = deferredToolCallToToolInvocation(
21 | deferredToolCall,
22 | toolResult
23 | );
24 |
25 | let textContent = "";
26 |
27 | for (const part of responseMessage.content) {
28 | if (typeof part === "string") {
29 | textContent += part;
30 | } else if (part.type === "text") {
31 | textContent += part.text;
32 | }
33 | }
34 |
35 | return {
36 | role: "assistant",
37 | id: responseMessage.id,
38 | createdAt: new Date(),
39 | content: textContent,
40 | parts: [
41 | { type: "step-start" },
42 | { type: "tool-invocation", toolInvocation },
43 | { type: "step-start" },
44 | { type: "text", text: textContent },
45 | ],
46 | toolInvocations: [toolInvocation],
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/card-issue-agent/src/agent/helper.ts:
--------------------------------------------------------------------------------
1 | import { CoreAssistantMessage, CoreToolMessage, Message } from "ai";
2 | import { deferredToolCallToToolInvocation } from "@knocklabs/agent-toolkit/ai-sdk";
3 | // import { type DeferredToolCall } from "@knocklabs/agent-toolkit/human-in-the-loop";
4 |
5 | type ResponseMessage = (CoreAssistantMessage | CoreToolMessage) & {
6 | id: string;
7 | };
8 |
9 | /**
10 | * Returns a new assistant message that includes the deferred tool call and the result so it can be
11 | * shown in the UI.
12 | *
13 | * @param toolCall
14 | */
15 | export function responseToAssistantMessage(
16 | responseMessage: ResponseMessage,
17 | deferredToolCall: any,
18 | toolResult: any
19 | ): Message {
20 | const toolInvocation = deferredToolCallToToolInvocation(
21 | deferredToolCall,
22 | toolResult
23 | );
24 |
25 | let textContent = "";
26 |
27 | for (const part of responseMessage.content) {
28 | if (typeof part === "string") {
29 | textContent += part;
30 | } else if (part.type === "text") {
31 | textContent += part.text;
32 | }
33 | }
34 |
35 | return {
36 | role: "assistant",
37 | id: responseMessage.id,
38 | createdAt: new Date(),
39 | content: textContent,
40 | parts: [
41 | { type: "step-start" },
42 | { type: "tool-invocation", toolInvocation },
43 | { type: "step-start" },
44 | { type: "text", text: textContent },
45 | ],
46 | toolInvocations: [toolInvocation],
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/card-issue-agent/src/agent/app.ts:
--------------------------------------------------------------------------------
1 | import { getAgentByName, routeAgentRequest } from "agents";
2 | import { Hono } from "hono";
3 | import { Knock } from "@knocklabs/node";
4 | import { Env } from ".";
5 |
6 | const app = new Hono();
7 |
8 | const knock = new Knock(process.env.KNOCK_API_KEY);
9 |
10 | app.get("/card-issued/approve", async (c) => {
11 | const { messageId } = c.req.query();
12 |
13 | await knock.put(`/v1/messages/${messageId}/interacted`, {
14 | metadata: {
15 | status: "approved",
16 | },
17 | });
18 |
19 | return c.text("Approved");
20 | });
21 |
22 | app.post("/incoming/knock/webhook", async (c) => {
23 | const body = await c.req.json();
24 | const env = c.env as Env;
25 |
26 | // Find the user ID from the tool call for the original issuer.
27 | // TODO: move this to the `body.data.actors[0]` on the Message object.
28 | // this requires us using the `actor` on the workflow.
29 | const userId = body?.data?.actors[0];
30 |
31 | if (!userId) {
32 | return c.text("No user ID found", { status: 400 });
33 | }
34 |
35 | const existingAgent = await getAgentByName(env.AIAgent, userId);
36 |
37 | if (existingAgent) {
38 | const result = await existingAgent.handleIncomingWebhook(body);
39 |
40 | return c.json(result);
41 | } else {
42 | return c.text("Not found", { status: 404 });
43 | }
44 | });
45 |
46 | app.get("*", async (c) => {
47 | return (
48 | // Route the request to our agent or return 404 if not found
49 | (await routeAgentRequest(c.req.raw, c.env)) ||
50 | c.text("Not found", { status: 404 })
51 | );
52 | });
53 |
54 | export default app;
55 |
--------------------------------------------------------------------------------
/card-issue-agent/README.md:
--------------------------------------------------------------------------------
1 | # React Chat with human-in-the-loop approval
2 |
3 | This application demonstrates using Knock to power a deferred tool call as part of a human-in-the-loop interaction in an Agent chat.
4 |
5 | ## Powered by
6 |
7 | - [Cloudflare Workers](https://developers.cloudflare.com/workers/)
8 | - [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/api-reference/)
9 | - [Vercel AI SDK](https://sdk.vercel.ai/)
10 | - [Knock Agent Toolkit](https://docs.knock.app/developer-tools/agent-toolkit/overview)
11 |
12 | ## Prerequisites
13 |
14 | - A [Knock account](https://dashboard.knock.app/signup)
15 | - A service token configured
16 | - A `approve-tool-call` workflow configured with a single in-app feed channel step
17 | - A `message.interacted` webhook configured that sends a request back to `http://localhost:5173/incoming/webhook`
18 | - A [Cloudflare developer account](https://dash.cloudflare.com/sign-up/workers?_gl=1*s0kyay*_gcl_au*MTg1MTMyOTk5NS4xNzQ0ODEyNzA3*_ga*MTc1NTAwNzc3NS4xNzQ0ODEyNzA3*_ga_SQCRB0TXZW*MTc0NTQzNjU1NC41LjEuMTc0NTQzNjU1OS41NS4wLjA.)
19 |
20 | ## Development
21 |
22 | Install dependencies:
23 |
24 | ```bash
25 | npm install
26 | ```
27 |
28 | Start the development server with:
29 |
30 | ```bash
31 | npm run dev
32 | ```
33 |
34 | Your application will be available at [http://localhost:5173](http://localhost:5173).
35 |
36 | ## Production
37 |
38 | Build your project for production:
39 |
40 | ```bash
41 | npm run build
42 | ```
43 |
44 | Preview your build locally:
45 |
46 | ```bash
47 | npm run preview
48 | ```
49 |
50 | Deploy your project to Cloudflare Workers:
51 |
52 | ```bash
53 | npx wrangler deploy
54 | ```
55 |
56 | ## Additional Resources
57 |
58 | - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/)
59 | - [Vite Documentation](https://vitejs.dev/guide/)
60 | - [React Documentation](https://reactjs.org/)
61 | - [Hono Documentation](https://hono.dev/)
62 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/README.md:
--------------------------------------------------------------------------------
1 | # React Chat with human-in-the-loop approval
2 |
3 | This application demonstrates using Knock to power a deferred tool call as part of a human-in-the-loop interaction in an Agent chat.
4 |
5 | ## Powered by
6 |
7 | - [Cloudflare Workers](https://developers.cloudflare.com/workers/)
8 | - [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/api-reference/)
9 | - [Vercel AI SDK](https://sdk.vercel.ai/)
10 | - [Knock Agent Toolkit](https://docs.knock.app/developer-tools/agent-toolkit/overview)
11 |
12 | ## Prerequisites
13 |
14 | - A [Knock account](https://dashboard.knock.app/signup)
15 | - A service token configured
16 | - A `approve-tool-call` workflow configured with a single in-app feed channel step
17 | - A `message.interacted` webhook configured that sends a request back to `http://localhost:5173/incoming/webhook`
18 | - A [Cloudflare developer account](https://dash.cloudflare.com/sign-up/workers?_gl=1*s0kyay*_gcl_au*MTg1MTMyOTk5NS4xNzQ0ODEyNzA3*_ga*MTc1NTAwNzc3NS4xNzQ0ODEyNzA3*_ga_SQCRB0TXZW*MTc0NTQzNjU1NC41LjEuMTc0NTQzNjU1OS41NS4wLjA.)
19 |
20 | ## Development
21 |
22 | Install dependencies:
23 |
24 | ```bash
25 | npm install
26 | ```
27 |
28 | Start the development server with:
29 |
30 | ```bash
31 | npm run dev
32 | ```
33 |
34 | Your application will be available at [http://localhost:5173](http://localhost:5173).
35 |
36 | ## Production
37 |
38 | Build your project for production:
39 |
40 | ```bash
41 | npm run build
42 | ```
43 |
44 | Preview your build locally:
45 |
46 | ```bash
47 | npm run preview
48 | ```
49 |
50 | Deploy your project to Cloudflare Workers:
51 |
52 | ```bash
53 | npx wrangler deploy
54 | ```
55 |
56 | ## Additional Resources
57 |
58 | - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/)
59 | - [Vite Documentation](https://vitejs.dev/guide/)
60 | - [React Documentation](https://reactjs.org/)
61 | - [Hono Documentation](https://hono.dev/)
62 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
60 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
60 |
--------------------------------------------------------------------------------
/card-issue-agent/wrangler.json:
--------------------------------------------------------------------------------
1 | /**
2 | * For more details on how to configure Wrangler, refer to:
3 | * https://developers.cloudflare.com/workers/wrangler/configuration/
4 | */
5 | {
6 | "$schema": "node_modules/wrangler/config-schema.json",
7 | "name": "knock-card-issue-agent",
8 | "main": "./src/agent/index.ts",
9 | "compatibility_date": "2025-04-01",
10 | "compatibility_flags": ["nodejs_compat"],
11 | "observability": {
12 | "enabled": true
13 | },
14 | "upload_source_maps": true,
15 | "assets": {
16 | "not_found_handling": "none"
17 | },
18 | "durable_objects": {
19 | "bindings": [
20 | {
21 | "name": "AIAgent",
22 | "class_name": "AIAgent"
23 | }
24 | ]
25 | },
26 | "migrations": [
27 | {
28 | "tag": "v1",
29 | // Mandatory for the Agent to store state
30 | "new_sqlite_classes": ["AIAgent"]
31 | }
32 | ]
33 | /**
34 | * Smart Placement
35 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
36 | */
37 | // "placement": { "mode": "smart" },
38 |
39 | /**
40 | * Bindings
41 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
42 | * databases, object storage, AI inference, real-time communication and more.
43 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/
44 | */
45 |
46 | /**
47 | * Environment Variables
48 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
49 | */
50 | // "vars": { "MY_VARIABLE": "production_value" },
51 | /**
52 | * Note: Use secrets to store sensitive data.
53 | * https://developers.cloudflare.com/workers/configuration/secrets/
54 | */
55 |
56 | /**
57 | * Static Assets
58 | * https://developers.cloudflare.com/workers/static-assets/binding/
59 | */
60 | // "assets": { "directory": "./public/", "binding": "ASSETS" },
61 |
62 | /**
63 | * Service Bindings (communicate between multiple Workers)
64 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
65 | */
66 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
67 | }
68 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/wrangler.json:
--------------------------------------------------------------------------------
1 | /**
2 | * For more details on how to configure Wrangler, refer to:
3 | * https://developers.cloudflare.com/workers/wrangler/configuration/
4 | */
5 | {
6 | "$schema": "node_modules/wrangler/config-schema.json",
7 | "name": "agent-sdk-chat-with-approval",
8 | "main": "./src/agent/index.ts",
9 | "compatibility_date": "2025-04-01",
10 | "compatibility_flags": [
11 | "nodejs_compat"
12 | ],
13 | "observability": {
14 | "enabled": true
15 | },
16 | "upload_source_maps": true,
17 | "assets": {
18 | "not_found_handling": "single-page-application"
19 | },
20 | "durable_objects": {
21 | "bindings": [
22 | {
23 | "name": "AIAgent",
24 | "class_name": "AIAgent",
25 | },
26 | ],
27 | },
28 | "migrations": [
29 | {
30 | "tag": "v1",
31 | // Mandatory for the Agent to store state
32 | "new_sqlite_classes": ["AIAgent"],
33 | },
34 | ],
35 | /**
36 | * Smart Placement
37 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
38 | */
39 | // "placement": { "mode": "smart" },
40 |
41 | /**
42 | * Bindings
43 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
44 | * databases, object storage, AI inference, real-time communication and more.
45 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/
46 | */
47 |
48 | /**
49 | * Environment Variables
50 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
51 | */
52 | // "vars": { "MY_VARIABLE": "production_value" },
53 | /**
54 | * Note: Use secrets to store sensitive data.
55 | * https://developers.cloudflare.com/workers/configuration/secrets/
56 | */
57 |
58 | /**
59 | * Static Assets
60 | * https://developers.cloudflare.com/workers/static-assets/binding/
61 | */
62 | // "assets": { "directory": "./public/", "binding": "ASSETS" },
63 |
64 | /**
65 | * Service Bindings (communicate between multiple Workers)
66 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
67 | */
68 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
69 | }
70 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-react-template",
3 | "description": "A template for building a React application with Vite, Hono, and Cloudflare Workers",
4 | "version": "0.0.0",
5 | "cloudflare": {
6 | "label": "Vite React Template",
7 | "products": [
8 | "Workers"
9 | ],
10 | "categories": [],
11 | "icon_urls": [
12 | "https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/5ca0ca32-e897-4699-d4c1-6b680512f000/public"
13 | ],
14 | "preview_image_url": "https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/fc7b4b62-442b-4769-641b-ad4422d74300/public",
15 | "dash": true
16 | },
17 | "dependencies": {
18 | "@ai-sdk/openai": "^1.3.16",
19 | "@ai-sdk/react": "^1.2.9",
20 | "@ai-sdk/ui-utils": "^1.2.8",
21 | "@knocklabs/agent": "file:../../agent-toolkit",
22 | "@knocklabs/react": "^0.7.3",
23 | "@radix-ui/react-avatar": "^1.1.7",
24 | "@radix-ui/react-slot": "^1.2.0",
25 | "@radix-ui/react-tooltip": "^1.2.4",
26 | "@tailwindcss/vite": "^4.1.4",
27 | "agents": "^0.0.62",
28 | "ai": "^4.3.9",
29 | "class-variance-authority": "^0.7.1",
30 | "clsx": "^2.1.1",
31 | "hono": "4.7.5",
32 | "lucide-react": "^0.503.0",
33 | "marked": "^15.0.11",
34 | "react": "19.0.0",
35 | "react-dom": "19.0.0",
36 | "react-markdown": "^10.1.0",
37 | "remark-gfm": "^4.0.1",
38 | "shiki": "^3.3.0",
39 | "tailwind-merge": "^3.2.0",
40 | "tailwindcss": "^4.1.4"
41 | },
42 | "devDependencies": {
43 | "@cloudflare/vite-plugin": "1.0.2",
44 | "@cloudflare/workers-types": "^4.20250420.0",
45 | "@eslint/js": "9.23.0",
46 | "@types/node": "^22.15.2",
47 | "@types/react": "19.0.10",
48 | "@types/react-dom": "19.0.4",
49 | "@vitejs/plugin-react": "4.3.4",
50 | "eslint": "9.23.0",
51 | "eslint-plugin-react-hooks": "5.2.0",
52 | "eslint-plugin-react-refresh": "0.4.19",
53 | "globals": "15.15.0",
54 | "tw-animate-css": "^1.2.8",
55 | "typescript": "5.8.2",
56 | "typescript-eslint": "8.29.0",
57 | "vite": "6.2.5",
58 | "wrangler": "^4.12.0"
59 | },
60 | "scripts": {
61 | "build": "tsc -b && vite build",
62 | "deploy": "npm run build && wrangler deploy",
63 | "dev": "vite",
64 | "lint": "eslint .",
65 | "preview": "npm run build && vite preview",
66 | "types": "wrangler types"
67 | },
68 | "type": "module"
69 | }
70 |
--------------------------------------------------------------------------------
/card-issue-agent/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "knock-card-issue-agent",
3 | "description": "A simple AI agent that can assist with issuing new credit cards.",
4 | "version": "0.1.0",
5 | "cloudflare": {
6 | "products": [
7 | "Workers"
8 | ],
9 | "categories": [],
10 | "icon_urls": [
11 | "https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/5ca0ca32-e897-4699-d4c1-6b680512f000/public"
12 | ],
13 | "preview_image_url": "https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/fc7b4b62-442b-4769-641b-ad4422d74300/public",
14 | "dash": true
15 | },
16 | "dependencies": {
17 | "@ai-sdk/openai": "^1.3.16",
18 | "@ai-sdk/react": "^1.2.9",
19 | "@ai-sdk/ui-utils": "^1.2.8",
20 | "@knocklabs/agent-toolkit": "file:../../agent-toolkit",
21 | "@knocklabs/react": "^0.7.3",
22 | "@radix-ui/react-avatar": "^1.1.7",
23 | "@radix-ui/react-slot": "^1.2.0",
24 | "@radix-ui/react-tooltip": "^1.2.4",
25 | "@tailwindcss/vite": "^4.1.4",
26 | "agents": "^0.0.62",
27 | "ai": "^4.3.9",
28 | "class-variance-authority": "^0.7.1",
29 | "clsx": "^2.1.1",
30 | "hono": "4.7.5",
31 | "lucide-react": "^0.503.0",
32 | "marked": "^15.0.11",
33 | "react": "19.0.0",
34 | "react-dom": "19.0.0",
35 | "react-markdown": "^10.1.0",
36 | "remark-gfm": "^4.0.1",
37 | "shiki": "^3.3.0",
38 | "tailwind-merge": "^3.2.0",
39 | "tailwindcss": "^4.1.4",
40 | "usehooks-ts": "^3.1.1",
41 | "uuid": "^11.1.0"
42 | },
43 | "devDependencies": {
44 | "@cloudflare/vite-plugin": "1.0.2",
45 | "@cloudflare/workers-types": "^4.20250420.0",
46 | "@eslint/js": "9.23.0",
47 | "@types/node": "^22.15.2",
48 | "@types/react": "19.0.10",
49 | "@types/react-dom": "19.0.4",
50 | "@vitejs/plugin-react": "4.3.4",
51 | "eslint": "9.23.0",
52 | "eslint-plugin-react-hooks": "5.2.0",
53 | "eslint-plugin-react-refresh": "0.4.19",
54 | "globals": "15.15.0",
55 | "tw-animate-css": "^1.2.8",
56 | "typescript": "5.8.2",
57 | "typescript-eslint": "8.29.0",
58 | "vite": "6.2.5",
59 | "wrangler": "^4.12.0"
60 | },
61 | "scripts": {
62 | "build": "tsc -b && vite build",
63 | "deploy": "npm run build && wrangler deploy",
64 | "dev": "vite",
65 | "lint": "eslint .",
66 | "preview": "npm run build && vite preview",
67 | "types": "wrangler types"
68 | },
69 | "type": "module"
70 | }
71 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "../../../lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | );
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean;
47 | }) {
48 | const Comp = asChild ? Slot : "button";
49 |
50 | return (
51 |
56 | );
57 | }
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import React, { useEffect, useState } from "react"
3 | import { codeToHtml } from "shiki"
4 |
5 | export type CodeBlockProps = {
6 | children?: React.ReactNode
7 | className?: string
8 | } & React.HTMLProps
9 |
10 | function CodeBlock({ children, className, ...props }: CodeBlockProps) {
11 | return (
12 |
20 | {children}
21 |
22 | )
23 | }
24 |
25 | export type CodeBlockCodeProps = {
26 | code: string
27 | language?: string
28 | theme?: string
29 | className?: string
30 | } & React.HTMLProps
31 |
32 | function CodeBlockCode({
33 | code,
34 | language = "tsx",
35 | theme = "github-light",
36 | className,
37 | ...props
38 | }: CodeBlockCodeProps) {
39 | const [highlightedHtml, setHighlightedHtml] = useState(null)
40 |
41 | useEffect(() => {
42 | async function highlight() {
43 | if (!code) {
44 | setHighlightedHtml(" ")
45 | return
46 | }
47 |
48 | const html = await codeToHtml(code, { lang: language, theme })
49 | setHighlightedHtml(html)
50 | }
51 | highlight()
52 | }, [code, language, theme])
53 |
54 | const classNames = cn(
55 | "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4",
56 | className
57 | )
58 |
59 | // SSR fallback: render plain code if not hydrated yet
60 | return highlightedHtml ? (
61 |
66 | ) : (
67 |
68 |
69 | {code}
70 |
71 |
72 | )
73 | }
74 |
75 | export type CodeBlockGroupProps = React.HTMLAttributes
76 |
77 | function CodeBlockGroup({
78 | children,
79 | className,
80 | ...props
81 | }: CodeBlockGroupProps) {
82 | return (
83 |
87 | {children}
88 |
89 | )
90 | }
91 |
92 | export { CodeBlockGroup, CodeBlockCode, CodeBlock }
93 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "../../../lib/utils";
2 | import React, { useEffect, useState } from "react";
3 | import { codeToHtml } from "shiki";
4 |
5 | export type CodeBlockProps = {
6 | children?: React.ReactNode;
7 | className?: string;
8 | } & React.HTMLProps;
9 |
10 | function CodeBlock({ children, className, ...props }: CodeBlockProps) {
11 | return (
12 |
20 | {children}
21 |
22 | );
23 | }
24 |
25 | export type CodeBlockCodeProps = {
26 | code: string;
27 | language?: string;
28 | theme?: string;
29 | className?: string;
30 | } & React.HTMLProps;
31 |
32 | function CodeBlockCode({
33 | code,
34 | language = "tsx",
35 | theme = "github-light",
36 | className,
37 | ...props
38 | }: CodeBlockCodeProps) {
39 | const [highlightedHtml, setHighlightedHtml] = useState(null);
40 |
41 | useEffect(() => {
42 | async function highlight() {
43 | if (!code) {
44 | setHighlightedHtml(" ");
45 | return;
46 | }
47 |
48 | const html = await codeToHtml(code, { lang: language, theme });
49 | setHighlightedHtml(html);
50 | }
51 | highlight();
52 | }, [code, language, theme]);
53 |
54 | const classNames = cn(
55 | "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4",
56 | className
57 | );
58 |
59 | // SSR fallback: render plain code if not hydrated yet
60 | return highlightedHtml ? (
61 |
66 | ) : (
67 |
68 |
69 | {code}
70 |
71 |
72 | );
73 | }
74 |
75 | export type CodeBlockGroupProps = React.HTMLAttributes;
76 |
77 | function CodeBlockGroup({
78 | children,
79 | className,
80 | ...props
81 | }: CodeBlockGroupProps) {
82 | return (
83 |
87 | {children}
88 |
89 | );
90 | }
91 |
92 | export { CodeBlockGroup, CodeBlockCode, CodeBlock };
93 |
--------------------------------------------------------------------------------
/card-issue-agent/src/agent/constants.ts:
--------------------------------------------------------------------------------
1 | export const ABOUT = `
2 | This is a demo AI Agent chat that shows how to use Knock to power multi-channel human-in-the-loop messaging for your AI Agents. In this demo, we use Knock to power the approval process for a credit card issuing agent.
3 |
4 | It's built with [Knock](https://knock.app/agent-toolkit), [Cloudflare Agents](https://developers.cloudflare.com/agents/), and the [Vercel AI SDK](https://sdk.vercel.ai/).
5 | `;
6 |
7 | export const WHAT_IS_KNOCK = `
8 | [Knock](https://knock.app) is a developer tool for powering customer messaging. It's built as a set of primitives that you can use to power any kind of customer cross-channel messaging flow for your product or AI agent.
9 |
10 | You can sign up for a free Knock account at [knock.app](https://knock.app).
11 | `;
12 |
13 | export const HOW_DOES_THIS_WORK = `
14 | When asked to explain how this demo works, here's the context. You do not have to use this verbatim, but you can use it as a guide to explain how the application works.
15 |
16 |
17 | This demo shows how to use Knock to power a cross-channel approval process for a credit card issuing agent. The [Knock workflow](https://docs.knock.app/concepts/workflows) will be invoked when you execute the "Issue card" tool, deferring the original tool call to wait for a human to approve or reject the card.
18 |
19 | The Knock workflow will send an email and then the SMS with a link to approve or reject the card. Clicking the approve button in either message will route a request back to the worker with the approval context. The message approval will then be tracked _back_ to Knock as an "interaction" with the message ([docs](https://docs.knock.app/send-notifications/message-statuses)), sending through the approval result as metadata.
20 |
21 | Knock will then send a "message.interacted" event as a webhook to the Cloudflare worker. The worker forwards the approval result back to the agent so that the agent can resume the tool call. At this point the card is issued as the deferred tool call is processed, and a message is sent to the originating user via the Agent process.
22 |
23 | We use the [Knock Agent Toolkit](https://docs.knock.app/developer-tools/agent-toolkit/overview) to power the agent, and the [Knock Outbound Webhooks](https://docs.knock.app/developer-tools/outbound-webhooks/overview) to power the webhook integration. The Agent process keeps the state of which tool calls have been deferred and the cards that have been issued.
24 |
25 | `;
26 |
27 | export const BASE_URL = "http://localhost:5173";
28 |
--------------------------------------------------------------------------------
/card-issue-agent/src/agent/tools.ts:
--------------------------------------------------------------------------------
1 | import { createKnockToolkit } from "@knocklabs/agent-toolkit/ai-sdk";
2 | import { tool } from "ai";
3 | import { z } from "zod";
4 |
5 | import { AIAgent } from "./index";
6 | import { issueCard } from "./api";
7 | import { BASE_URL, HOW_DOES_THIS_WORK } from "./constants";
8 |
9 | async function initializeToolkit(agent: AIAgent) {
10 | const toolkit = await createKnockToolkit({
11 | serviceToken: agent.env.KNOCK_SERVICE_TOKEN,
12 | permissions: {
13 | users: {
14 | manage: true,
15 | },
16 | },
17 | userId: agent.name,
18 | });
19 |
20 | const issueCardTool = tool({
21 | description:
22 | "Issue a new credit card to a customer. Use this tool when you're requested to issue a new card. When this tool is used, the card will only be issued once an admin has approved the card.",
23 | parameters: z.object({
24 | customerId: z.string(),
25 | name: z.string().describe("The name of the card. This is REQUIRED."),
26 | }),
27 | execute: async ({ customerId, name }) => {
28 | const card = await issueCard(customerId, name);
29 | agent.addIssuedCard(card);
30 | return card;
31 | },
32 | });
33 |
34 | const listCardsTool = tool({
35 | description:
36 | "List all of the cards issued to the user. Use this tool when you're requested to list cards.",
37 | parameters: z.object({
38 | customerId: z.string(),
39 | }),
40 | execute: async ({ customerId }) => {
41 | return agent.getIssuedCards();
42 | },
43 | });
44 |
45 | const explainTool = tool({
46 | description:
47 | "Explain how this demo works. Use this tool if you're asked to explain how this demo application works.",
48 | parameters: z.object({}),
49 | execute: async () => {
50 | return HOW_DOES_THIS_WORK;
51 | },
52 | });
53 |
54 | const tools = toolkit.requireHumanInput(
55 | { issueCard: issueCardTool },
56 | {
57 | onBeforeCallKnock: async (toolCall) => {
58 | agent.setToolCallStatus(toolCall.id, "pending");
59 | },
60 | onAfterCallKnock: async (toolCall) => {
61 | agent.setToolCallStatus(toolCall.id, "requested");
62 | },
63 | workflow: "approve-issued-card",
64 | actor: {
65 | id: agent.name,
66 | email: `user_${agent.name}@example.com`,
67 | name: "Jane Doe",
68 | },
69 | recipients: ["admin_user_1"],
70 | metadata: {
71 | approve_url: `${BASE_URL}/card-issued/approve`,
72 | reject_url: `${BASE_URL}/card-issued/reject`,
73 | },
74 | }
75 | );
76 |
77 | return {
78 | toolkit,
79 | tools: {
80 | ...tools,
81 | listCards: listCardsTool,
82 | explainTool,
83 | ...toolkit.getTools("users"),
84 | },
85 | };
86 | }
87 |
88 | export { initializeToolkit };
89 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { marked } from "marked"
3 | import { memo, useId, useMemo } from "react"
4 | import ReactMarkdown, { Components } from "react-markdown"
5 | import remarkGfm from "remark-gfm"
6 | import { CodeBlock, CodeBlockCode } from "./code-block"
7 |
8 | export type MarkdownProps = {
9 | children: string
10 | id?: string
11 | className?: string
12 | components?: Partial
13 | }
14 |
15 | function parseMarkdownIntoBlocks(markdown: string): string[] {
16 | const tokens = marked.lexer(markdown)
17 | return tokens.map((token) => token.raw)
18 | }
19 |
20 | function extractLanguage(className?: string): string {
21 | if (!className) return "plaintext"
22 | const match = className.match(/language-(\w+)/)
23 | return match ? match[1] : "plaintext"
24 | }
25 |
26 | const INITIAL_COMPONENTS: Partial = {
27 | code: function CodeComponent({ className, children, ...props }) {
28 | const isInline =
29 | !props.node?.position?.start.line ||
30 | props.node?.position?.start.line === props.node?.position?.end.line
31 |
32 | if (isInline) {
33 | return (
34 |
41 | {children}
42 |
43 | )
44 | }
45 |
46 | const language = extractLanguage(className)
47 |
48 | return (
49 |
50 |
51 |
52 | )
53 | },
54 | pre: function PreComponent({ children }) {
55 | return <>{children}>
56 | },
57 | }
58 |
59 | const MemoizedMarkdownBlock = memo(
60 | function MarkdownBlock({
61 | content,
62 | components = INITIAL_COMPONENTS,
63 | }: {
64 | content: string
65 | components?: Partial
66 | }) {
67 | return (
68 |
69 | {content}
70 |
71 | )
72 | },
73 | function propsAreEqual(prevProps, nextProps) {
74 | return prevProps.content === nextProps.content
75 | }
76 | )
77 |
78 | MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock"
79 |
80 | function MarkdownComponent({
81 | children,
82 | id,
83 | className,
84 | components = INITIAL_COMPONENTS,
85 | }: MarkdownProps) {
86 | const generatedId = useId()
87 | const blockId = id ?? generatedId
88 | const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])
89 |
90 | return (
91 |
92 | {blocks.map((block, index) => (
93 |
98 | ))}
99 |
100 | )
101 | }
102 |
103 | const Markdown = memo(MarkdownComponent)
104 | Markdown.displayName = "Markdown"
105 |
106 | export { Markdown }
107 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "../../../lib/utils";
2 | import { marked } from "marked";
3 | import { memo, useId, useMemo } from "react";
4 | import ReactMarkdown, { Components } from "react-markdown";
5 | import remarkGfm from "remark-gfm";
6 | import { CodeBlock, CodeBlockCode } from "./code-block";
7 |
8 | export type MarkdownProps = {
9 | children: string;
10 | id?: string;
11 | className?: string;
12 | components?: Partial;
13 | };
14 |
15 | function parseMarkdownIntoBlocks(markdown: string): string[] {
16 | const tokens = marked.lexer(markdown);
17 | return tokens.map((token) => token.raw);
18 | }
19 |
20 | function extractLanguage(className?: string): string {
21 | if (!className) return "plaintext";
22 | const match = className.match(/language-(\w+)/);
23 | return match ? match[1] : "plaintext";
24 | }
25 |
26 | const INITIAL_COMPONENTS: Partial = {
27 | code: function CodeComponent({ className, children, ...props }) {
28 | const isInline =
29 | !props.node?.position?.start.line ||
30 | props.node?.position?.start.line === props.node?.position?.end.line;
31 |
32 | if (isInline) {
33 | return (
34 |
41 | {children}
42 |
43 | );
44 | }
45 |
46 | const language = extractLanguage(className);
47 |
48 | return (
49 |
50 |
51 |
52 | );
53 | },
54 | pre: function PreComponent({ children }) {
55 | return <>{children}>;
56 | },
57 | };
58 |
59 | const MemoizedMarkdownBlock = memo(
60 | function MarkdownBlock({
61 | content,
62 | components = INITIAL_COMPONENTS,
63 | }: {
64 | content: string;
65 | components?: Partial;
66 | }) {
67 | return (
68 |
69 | {content}
70 |
71 | );
72 | },
73 | function propsAreEqual(prevProps, nextProps) {
74 | return prevProps.content === nextProps.content;
75 | }
76 | );
77 |
78 | MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
79 |
80 | function MarkdownComponent({
81 | children,
82 | id,
83 | className,
84 | components = INITIAL_COMPONENTS,
85 | }: MarkdownProps) {
86 | const generatedId = useId();
87 | const blockId = id ?? generatedId;
88 | const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);
89 |
90 | return (
91 |
92 | {blocks.map((block, index) => (
93 |
98 | ))}
99 |
100 | );
101 | }
102 |
103 | const Markdown = memo(MarkdownComponent);
104 | Markdown.displayName = "Markdown";
105 |
106 | export { Markdown };
107 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/message.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip"
8 | import { cn } from "@/lib/utils"
9 | import { Markdown } from "./markdown"
10 |
11 | export type MessageProps = {
12 | children: React.ReactNode
13 | className?: string
14 | } & React.HTMLProps
15 |
16 | const Message = ({ children, className, ...props }: MessageProps) => (
17 |
18 | {children}
19 |
20 | )
21 |
22 | export type MessageAvatarProps = {
23 | src: string
24 | alt: string
25 | fallback?: string
26 | delayMs?: number
27 | className?: string
28 | }
29 |
30 | const MessageAvatar = ({
31 | src,
32 | alt,
33 | fallback,
34 | delayMs,
35 | className,
36 | }: MessageAvatarProps) => {
37 | return (
38 |
39 |
40 | {fallback && (
41 | {fallback}
42 | )}
43 |
44 | )
45 | }
46 |
47 | export type MessageContentProps = {
48 | children: React.ReactNode
49 | markdown?: boolean
50 | className?: string
51 | } & React.ComponentProps &
52 | React.HTMLProps
53 |
54 | const MessageContent = ({
55 | children,
56 | markdown = false,
57 | className,
58 | ...props
59 | }: MessageContentProps) => {
60 | const classNames = cn(
61 | "rounded-lg p-2 text-foreground bg-secondary prose break-words whitespace-normal",
62 | className
63 | )
64 |
65 | return markdown ? (
66 |
67 | {children as string}
68 |
69 | ) : (
70 |
71 | {children}
72 |
73 | )
74 | }
75 |
76 | export type MessageActionsProps = {
77 | children: React.ReactNode
78 | className?: string
79 | } & React.HTMLProps
80 |
81 | const MessageActions = ({
82 | children,
83 | className,
84 | ...props
85 | }: MessageActionsProps) => (
86 |
90 | {children}
91 |
92 | )
93 |
94 | export type MessageActionProps = {
95 | className?: string
96 | tooltip: React.ReactNode
97 | children: React.ReactNode
98 | side?: "top" | "bottom" | "left" | "right"
99 | } & React.ComponentProps
100 |
101 | const MessageAction = ({
102 | tooltip,
103 | children,
104 | className,
105 | side = "top",
106 | ...props
107 | }: MessageActionProps) => {
108 | return (
109 |
110 |
111 | {children}
112 |
113 | {tooltip}
114 |
115 |
116 |
117 | )
118 | }
119 |
120 | export { Message, MessageAvatar, MessageContent, MessageActions, MessageAction }
121 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/message.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "./tooltip";
8 | import { cn } from "../../../lib/utils";
9 | import { Markdown } from "./markdown";
10 |
11 | export type MessageProps = {
12 | children: React.ReactNode;
13 | className?: string;
14 | } & React.HTMLProps;
15 |
16 | const Message = ({ children, className, ...props }: MessageProps) => (
17 |
18 | {children}
19 |
20 | );
21 |
22 | export type MessageAvatarProps = {
23 | src: string;
24 | alt: string;
25 | fallback?: string;
26 | delayMs?: number;
27 | className?: string;
28 | };
29 |
30 | const MessageAvatar = ({
31 | src,
32 | alt,
33 | fallback,
34 | delayMs,
35 | className,
36 | }: MessageAvatarProps) => {
37 | return (
38 |
39 |
40 | {fallback && (
41 | {fallback}
42 | )}
43 |
44 | );
45 | };
46 |
47 | export type MessageContentProps = {
48 | children: React.ReactNode;
49 | markdown?: boolean;
50 | className?: string;
51 | } & React.ComponentProps &
52 | React.HTMLProps;
53 |
54 | const MessageContent = ({
55 | children,
56 | markdown = false,
57 | className,
58 | ...props
59 | }: MessageContentProps) => {
60 | const classNames = cn(
61 | "rounded-lg p-2 text-foreground bg-secondary prose break-words whitespace-normal",
62 | className
63 | );
64 |
65 | return markdown ? (
66 |
67 | {children as string}
68 |
69 | ) : (
70 |
71 | {children}
72 |
73 | );
74 | };
75 |
76 | export type MessageActionsProps = {
77 | children: React.ReactNode;
78 | className?: string;
79 | } & React.HTMLProps;
80 |
81 | const MessageActions = ({
82 | children,
83 | className,
84 | ...props
85 | }: MessageActionsProps) => (
86 |
90 | {children}
91 |
92 | );
93 |
94 | export type MessageActionProps = {
95 | className?: string;
96 | tooltip: React.ReactNode;
97 | children: React.ReactNode;
98 | side?: "top" | "bottom" | "left" | "right";
99 | } & React.ComponentProps;
100 |
101 | const MessageAction = ({
102 | tooltip,
103 | children,
104 | className,
105 | side = "top",
106 | ...props
107 | }: MessageActionProps) => {
108 | return (
109 |
110 |
111 | {children}
112 |
113 | {tooltip}
114 |
115 |
116 |
117 | );
118 | };
119 |
120 | export {
121 | Message,
122 | MessageAvatar,
123 | MessageContent,
124 | MessageActions,
125 | MessageAction,
126 | };
127 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/agent/index.ts:
--------------------------------------------------------------------------------
1 | import { getAgentByName, routeAgentRequest } from "agents";
2 |
3 | import { AIChatAgent } from "agents/ai-chat-agent";
4 | import { openai } from "@ai-sdk/openai";
5 | import { createDataStreamResponse, generateText, streamText } from "ai";
6 | import { ExecutionContext } from "hono";
7 | import { initializeToolkit } from "./tools";
8 | import { responseToAssistantMessage } from "./helper";
9 |
10 | type Env = {
11 | KNOCK_SERVICE_TOKEN: string;
12 | AIAgent: AIAgent;
13 | };
14 |
15 | export class AIAgent extends AIChatAgent {
16 | env: Env;
17 |
18 | async onChatMessage(onFinish) {
19 | const { tools } = await initializeToolkit(this.env);
20 |
21 | return createDataStreamResponse({
22 | execute: async (dataStream) => {
23 | try {
24 | const stream = streamText({
25 | model: openai("gpt-4o-mini"),
26 | system:
27 | "You are a helpful assistant that can answer questions and help with tasks.",
28 | messages: this.messages,
29 | onFinish, // call onFinish so that messages get saved
30 | tools: tools,
31 | maxSteps: 5,
32 | });
33 |
34 | stream.mergeIntoDataStream(dataStream);
35 | } catch (error) {
36 | console.error(error);
37 | }
38 | },
39 | });
40 | }
41 |
42 | async handleIncomingWebhook(request: Request) {
43 | const body = await request.json();
44 |
45 | const { toolkit } = await initializeToolkit(this.env);
46 | const result = toolkit.handleMessageInteraction(body);
47 |
48 | if (!result) {
49 | return new Response("Not found", { status: 404 });
50 | }
51 |
52 | // This is one example on how to handle the result of the deferred tool execution by running a new LLM call with the result.
53 | // You could also find the past tool call using the `toolCallResult.toolCallId` and append the result to the existing messages.
54 | if (result.interaction.action === "#approve") {
55 | const toolCallResult = await toolkit.resumeToolExecution(result.toolCall);
56 |
57 | const { response } = await generateText({
58 | model: openai("gpt-4o-mini"),
59 | prompt: `You are a helpful assistant that was asked to defer a tool execution to a human. The response from the tool execution is: ${JSON.stringify(
60 | toolCallResult
61 | )}.`,
62 | });
63 |
64 | const message = responseToAssistantMessage(
65 | response.messages[0],
66 | result.toolCall,
67 | toolCallResult
68 | );
69 |
70 | this.persistMessages([...this.messages, message]);
71 | }
72 |
73 | return Response.json({ status: "success" });
74 | }
75 | }
76 |
77 | export default {
78 | async fetch(request: Request, env: Env, ctx: ExecutionContext) {
79 | const url = new URL(request.url);
80 |
81 | if (url.pathname.startsWith("/incoming/webhook")) {
82 | const existingAgent = await getAgentByName(env.AIAgent, "default");
83 |
84 | if (existingAgent) {
85 | return await existingAgent.handleIncomingWebhook(request);
86 | } else {
87 | return new Response("Not found", { status: 404 });
88 | }
89 | }
90 |
91 | return (
92 | // Route the request to our agent or return 404 if not found
93 | (await routeAgentRequest(request, env)) ||
94 | new Response("Not found", { status: 404 })
95 | );
96 | },
97 | };
98 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/prompt-suggestion.tsx:
--------------------------------------------------------------------------------
1 | import { Button, buttonVariants } from "@/components/ui/button"
2 | import { cn } from "@/lib/utils"
3 | import { VariantProps } from "class-variance-authority"
4 |
5 | export type PromptSuggestionProps = {
6 | children: React.ReactNode
7 | variant?: VariantProps["variant"]
8 | size?: VariantProps["size"]
9 | className?: string
10 | highlight?: string
11 | } & React.ButtonHTMLAttributes
12 |
13 | function PromptSuggestion({
14 | children,
15 | variant,
16 | size,
17 | className,
18 | highlight,
19 | ...props
20 | }: PromptSuggestionProps) {
21 | const isHighlightMode = highlight !== undefined && highlight.trim() !== ""
22 | const content = typeof children === "string" ? children : ""
23 |
24 | if (!isHighlightMode) {
25 | return (
26 |
32 | {children}
33 |
34 | )
35 | }
36 |
37 | if (!content) {
38 | return (
39 |
49 | {children}
50 |
51 | )
52 | }
53 |
54 | const trimmedHighlight = highlight.trim()
55 | const contentLower = content.toLowerCase()
56 | const highlightLower = trimmedHighlight.toLowerCase()
57 | const shouldHighlight = contentLower.includes(highlightLower)
58 |
59 | return (
60 |
70 | {shouldHighlight ? (
71 | (() => {
72 | const index = contentLower.indexOf(highlightLower)
73 | if (index === -1)
74 | return (
75 |
76 | {content}
77 |
78 | )
79 |
80 | const actualHighlightedText = content.substring(
81 | index,
82 | index + highlightLower.length
83 | )
84 |
85 | const before = content.substring(0, index)
86 | const after = content.substring(index + actualHighlightedText.length)
87 |
88 | return (
89 | <>
90 | {before && (
91 |
92 | {before}
93 |
94 | )}
95 |
96 | {actualHighlightedText}
97 |
98 | {after && (
99 |
100 | {after}
101 |
102 | )}
103 | >
104 | )
105 | })()
106 | ) : (
107 |
108 | {content}
109 |
110 | )}
111 |
112 | )
113 | }
114 |
115 | export { PromptSuggestion }
116 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/components/ui/prompt-suggestion.tsx:
--------------------------------------------------------------------------------
1 | import { Button, buttonVariants } from "./button";
2 | import { cn } from "../../../lib/utils";
3 | import { VariantProps } from "class-variance-authority";
4 |
5 | export type PromptSuggestionProps = {
6 | children: React.ReactNode;
7 | variant?: VariantProps["variant"];
8 | size?: VariantProps["size"];
9 | className?: string;
10 | highlight?: string;
11 | } & React.ButtonHTMLAttributes;
12 |
13 | function PromptSuggestion({
14 | children,
15 | variant,
16 | size,
17 | className,
18 | highlight,
19 | ...props
20 | }: PromptSuggestionProps) {
21 | const isHighlightMode = highlight !== undefined && highlight.trim() !== "";
22 | const content = typeof children === "string" ? children : "";
23 |
24 | if (!isHighlightMode) {
25 | return (
26 |
32 | {children}
33 |
34 | );
35 | }
36 |
37 | if (!content) {
38 | return (
39 |
49 | {children}
50 |
51 | );
52 | }
53 |
54 | const trimmedHighlight = highlight.trim();
55 | const contentLower = content.toLowerCase();
56 | const highlightLower = trimmedHighlight.toLowerCase();
57 | const shouldHighlight = contentLower.includes(highlightLower);
58 |
59 | return (
60 |
70 | {shouldHighlight ? (
71 | (() => {
72 | const index = contentLower.indexOf(highlightLower);
73 | if (index === -1)
74 | return (
75 |
76 | {content}
77 |
78 | );
79 |
80 | const actualHighlightedText = content.substring(
81 | index,
82 | index + highlightLower.length
83 | );
84 |
85 | const before = content.substring(0, index);
86 | const after = content.substring(index + actualHighlightedText.length);
87 |
88 | return (
89 | <>
90 | {before && (
91 |
92 | {before}
93 |
94 | )}
95 |
96 | {actualHighlightedText}
97 |
98 | {after && (
99 |
100 | {after}
101 |
102 | )}
103 | >
104 | );
105 | })()
106 | ) : (
107 |
108 | {content}
109 |
110 | )}
111 |
112 | );
113 | }
114 |
115 | export { PromptSuggestion };
116 |
--------------------------------------------------------------------------------
/card-issue-agent/src/agent/index.ts:
--------------------------------------------------------------------------------
1 | import { AIChatAgent } from "agents/ai-chat-agent";
2 | import { openai } from "@ai-sdk/openai";
3 | import { createDataStreamResponse, generateText, streamText } from "ai";
4 |
5 | import { initializeToolkit } from "./tools";
6 | import { responseToAssistantMessage } from "./helper";
7 | import app from "./app";
8 | import { IssuedCard } from "./api";
9 | import { ABOUT, WHAT_IS_KNOCK } from "./constants";
10 |
11 | export type Env = {
12 | KNOCK_SERVICE_TOKEN: string;
13 | KNOCK_API_KEY: string;
14 | AIAgent: AIAgent;
15 | };
16 |
17 | export interface AgentState {
18 | issuedCards: IssuedCard[];
19 | toolCalls: Record;
20 | }
21 |
22 | export class AIAgent extends AIChatAgent {
23 | env: Env;
24 |
25 | initialState: AgentState = {
26 | issuedCards: [],
27 | toolCalls: {},
28 | };
29 |
30 | async onChatMessage(onFinish) {
31 | const { tools } = await initializeToolkit(this);
32 |
33 | return createDataStreamResponse({
34 | execute: async (dataStream) => {
35 | try {
36 | const stream = streamText({
37 | model: openai("gpt-4o-mini"),
38 | system: `You are a helpful assistant for a financial services company. You help customers with credit card issuing. The current user is ${this.name}.
39 |
40 | When you're asked about what this application is, you should respond with: ${ABOUT}.
41 |
42 | When you're asked about what Knock is, you should respond with: ${WHAT_IS_KNOCK}.`,
43 | messages: this.messages,
44 | onFinish, // call onFinish so that messages get saved
45 | tools: tools,
46 | maxSteps: 5,
47 | });
48 |
49 | stream.mergeIntoDataStream(dataStream);
50 | } catch (error) {
51 | console.error(error);
52 | }
53 | },
54 | });
55 | }
56 |
57 | // API for storing state on this agent process
58 |
59 | resetState() {
60 | this.setState(this.initialState);
61 | }
62 |
63 | getIssuedCards() {
64 | return this.state.issuedCards;
65 | }
66 |
67 | setToolCallStatus(
68 | toolCallId: string,
69 | status: "pending" | "requested" | "approved" | "rejected"
70 | ) {
71 | this.setState({
72 | ...this.state,
73 | toolCalls: { ...this.state.toolCalls, [toolCallId]: status },
74 | });
75 | }
76 |
77 | addIssuedCard(card: IssuedCard) {
78 | this.setState({
79 | ...this.state,
80 | issuedCards: [...this.state.issuedCards, card],
81 | });
82 | }
83 |
84 | async handleIncomingWebhook(body: any) {
85 | const { toolkit } = await initializeToolkit(this);
86 |
87 | const result = toolkit.handleMessageInteraction(body);
88 |
89 | if (!result) {
90 | return { error: "No result" };
91 | }
92 |
93 | const toolCallId = result.toolCall.id;
94 |
95 | // Guard against duplicate tool call requests
96 | if (this.state.toolCalls[toolCallId] !== "requested") {
97 | return { error: "Tool call is not requested" };
98 | }
99 |
100 | if (result.interaction.status === "approved") {
101 | const toolCallResult = await toolkit.resumeToolExecution(result.toolCall);
102 |
103 | this.setToolCallStatus(toolCallId, "approved");
104 |
105 | const { response } = await generateText({
106 | model: openai("gpt-4o-mini"),
107 | prompt: `You were asked to issue a card for a customer. The card is now approved. The result was: ${JSON.stringify(
108 | toolCallResult
109 | )}.`,
110 | });
111 |
112 | const message = responseToAssistantMessage(
113 | response.messages[0],
114 | result.toolCall,
115 | toolCallResult
116 | );
117 |
118 | this.persistMessages([...this.messages, message]);
119 | }
120 |
121 | return { status: "success" };
122 | }
123 | }
124 |
125 | export default app;
126 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/client/index.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | :root {
7 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
8 | line-height: 1.5;
9 | font-weight: 400;
10 | --radius: 0.625rem;
11 | --background: oklch(1 0 0);
12 | --foreground: oklch(0.145 0 0);
13 | --card: oklch(1 0 0);
14 | --card-foreground: oklch(0.145 0 0);
15 | --popover: oklch(1 0 0);
16 | --popover-foreground: oklch(0.145 0 0);
17 | --primary: oklch(0.205 0 0);
18 | --primary-foreground: oklch(0.985 0 0);
19 | --secondary: oklch(0.97 0 0);
20 | --secondary-foreground: oklch(0.205 0 0);
21 | --muted: oklch(0.97 0 0);
22 | --muted-foreground: oklch(0.556 0 0);
23 | --accent: oklch(0.97 0 0);
24 | --accent-foreground: oklch(0.205 0 0);
25 | --destructive: oklch(0.577 0.245 27.325);
26 | --border: oklch(0.922 0 0);
27 | --input: oklch(0.922 0 0);
28 | --ring: oklch(0.708 0 0);
29 | --chart-1: oklch(0.646 0.222 41.116);
30 | --chart-2: oklch(0.6 0.118 184.704);
31 | --chart-3: oklch(0.398 0.07 227.392);
32 | --chart-4: oklch(0.828 0.189 84.429);
33 | --chart-5: oklch(0.769 0.188 70.08);
34 | --sidebar: oklch(0.985 0 0);
35 | --sidebar-foreground: oklch(0.145 0 0);
36 | --sidebar-primary: oklch(0.205 0 0);
37 | --sidebar-primary-foreground: oklch(0.985 0 0);
38 | --sidebar-accent: oklch(0.97 0 0);
39 | --sidebar-accent-foreground: oklch(0.205 0 0);
40 | --sidebar-border: oklch(0.922 0 0);
41 | --sidebar-ring: oklch(0.708 0 0);
42 | }
43 |
44 | @theme inline {
45 | --radius-sm: calc(var(--radius) - 4px);
46 | --radius-md: calc(var(--radius) - 2px);
47 | --radius-lg: var(--radius);
48 | --radius-xl: calc(var(--radius) + 4px);
49 | --color-background: var(--background);
50 | --color-foreground: var(--foreground);
51 | --color-card: var(--card);
52 | --color-card-foreground: var(--card-foreground);
53 | --color-popover: var(--popover);
54 | --color-popover-foreground: var(--popover-foreground);
55 | --color-primary: var(--primary);
56 | --color-primary-foreground: var(--primary-foreground);
57 | --color-secondary: var(--secondary);
58 | --color-secondary-foreground: var(--secondary-foreground);
59 | --color-muted: var(--muted);
60 | --color-muted-foreground: var(--muted-foreground);
61 | --color-accent: var(--accent);
62 | --color-accent-foreground: var(--accent-foreground);
63 | --color-destructive: var(--destructive);
64 | --color-border: var(--border);
65 | --color-input: var(--input);
66 | --color-ring: var(--ring);
67 | --color-chart-1: var(--chart-1);
68 | --color-chart-2: var(--chart-2);
69 | --color-chart-3: var(--chart-3);
70 | --color-chart-4: var(--chart-4);
71 | --color-chart-5: var(--chart-5);
72 | --color-sidebar: var(--sidebar);
73 | --color-sidebar-foreground: var(--sidebar-foreground);
74 | --color-sidebar-primary: var(--sidebar-primary);
75 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
76 | --color-sidebar-accent: var(--sidebar-accent);
77 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
78 | --color-sidebar-border: var(--sidebar-border);
79 | --color-sidebar-ring: var(--sidebar-ring);
80 | }
81 |
82 | .dark {
83 | --background: oklch(0.145 0 0);
84 | --foreground: oklch(0.985 0 0);
85 | --card: oklch(0.205 0 0);
86 | --card-foreground: oklch(0.985 0 0);
87 | --popover: oklch(0.205 0 0);
88 | --popover-foreground: oklch(0.985 0 0);
89 | --primary: oklch(0.922 0 0);
90 | --primary-foreground: oklch(0.205 0 0);
91 | --secondary: oklch(0.269 0 0);
92 | --secondary-foreground: oklch(0.985 0 0);
93 | --muted: oklch(0.269 0 0);
94 | --muted-foreground: oklch(0.708 0 0);
95 | --accent: oklch(0.269 0 0);
96 | --accent-foreground: oklch(0.985 0 0);
97 | --destructive: oklch(0.704 0.191 22.216);
98 | --border: oklch(1 0 0 / 10%);
99 | --input: oklch(1 0 0 / 15%);
100 | --ring: oklch(0.556 0 0);
101 | --chart-1: oklch(0.488 0.243 264.376);
102 | --chart-2: oklch(0.696 0.17 162.48);
103 | --chart-3: oklch(0.769 0.188 70.08);
104 | --chart-4: oklch(0.627 0.265 303.9);
105 | --chart-5: oklch(0.645 0.246 16.439);
106 | --sidebar: oklch(0.205 0 0);
107 | --sidebar-foreground: oklch(0.985 0 0);
108 | --sidebar-primary: oklch(0.488 0.243 264.376);
109 | --sidebar-primary-foreground: oklch(0.985 0 0);
110 | --sidebar-accent: oklch(0.269 0 0);
111 | --sidebar-accent-foreground: oklch(0.985 0 0);
112 | --sidebar-border: oklch(1 0 0 / 10%);
113 | --sidebar-ring: oklch(0.556 0 0);
114 | }
115 |
116 | @layer base {
117 | * {
118 | @apply border-border outline-ring/50;
119 | }
120 | body {
121 | @apply bg-background text-foreground;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/card-issue-agent/src/client/index.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | :root {
7 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
8 | line-height: 1.5;
9 | font-weight: 400;
10 | --radius: 0.625rem;
11 | --background: oklch(1 0 0);
12 | --foreground: oklch(0.145 0 0);
13 | --card: oklch(1 0 0);
14 | --card-foreground: oklch(0.145 0 0);
15 | --popover: oklch(1 0 0);
16 | --popover-foreground: oklch(0.145 0 0);
17 | --primary: oklch(0.205 0 0);
18 | --primary-foreground: oklch(0.985 0 0);
19 | --secondary: oklch(0.97 0 0);
20 | --secondary-foreground: oklch(0.205 0 0);
21 | --muted: oklch(0.97 0 0);
22 | --muted-foreground: oklch(0.556 0 0);
23 | --accent: oklch(0.97 0 0);
24 | --accent-foreground: oklch(0.205 0 0);
25 | --destructive: oklch(0.577 0.245 27.325);
26 | --border: oklch(0.922 0 0);
27 | --input: oklch(0.922 0 0);
28 | --ring: oklch(0.708 0 0);
29 | --chart-1: oklch(0.646 0.222 41.116);
30 | --chart-2: oklch(0.6 0.118 184.704);
31 | --chart-3: oklch(0.398 0.07 227.392);
32 | --chart-4: oklch(0.828 0.189 84.429);
33 | --chart-5: oklch(0.769 0.188 70.08);
34 | --sidebar: oklch(0.985 0 0);
35 | --sidebar-foreground: oklch(0.145 0 0);
36 | --sidebar-primary: oklch(0.205 0 0);
37 | --sidebar-primary-foreground: oklch(0.985 0 0);
38 | --sidebar-accent: oklch(0.97 0 0);
39 | --sidebar-accent-foreground: oklch(0.205 0 0);
40 | --sidebar-border: oklch(0.922 0 0);
41 | --sidebar-ring: oklch(0.708 0 0);
42 | }
43 |
44 | @theme inline {
45 | --radius-sm: calc(var(--radius) - 4px);
46 | --radius-md: calc(var(--radius) - 2px);
47 | --radius-lg: var(--radius);
48 | --radius-xl: calc(var(--radius) + 4px);
49 | --color-background: var(--background);
50 | --color-foreground: var(--foreground);
51 | --color-card: var(--card);
52 | --color-card-foreground: var(--card-foreground);
53 | --color-popover: var(--popover);
54 | --color-popover-foreground: var(--popover-foreground);
55 | --color-primary: var(--primary);
56 | --color-primary-foreground: var(--primary-foreground);
57 | --color-secondary: var(--secondary);
58 | --color-secondary-foreground: var(--secondary-foreground);
59 | --color-muted: var(--muted);
60 | --color-muted-foreground: var(--muted-foreground);
61 | --color-accent: var(--accent);
62 | --color-accent-foreground: var(--accent-foreground);
63 | --color-destructive: var(--destructive);
64 | --color-border: var(--border);
65 | --color-input: var(--input);
66 | --color-ring: var(--ring);
67 | --color-chart-1: var(--chart-1);
68 | --color-chart-2: var(--chart-2);
69 | --color-chart-3: var(--chart-3);
70 | --color-chart-4: var(--chart-4);
71 | --color-chart-5: var(--chart-5);
72 | --color-sidebar: var(--sidebar);
73 | --color-sidebar-foreground: var(--sidebar-foreground);
74 | --color-sidebar-primary: var(--sidebar-primary);
75 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
76 | --color-sidebar-accent: var(--sidebar-accent);
77 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
78 | --color-sidebar-border: var(--sidebar-border);
79 | --color-sidebar-ring: var(--sidebar-ring);
80 | }
81 |
82 | .dark {
83 | --background: oklch(0.145 0 0);
84 | --foreground: oklch(0.985 0 0);
85 | --card: oklch(0.205 0 0);
86 | --card-foreground: oklch(0.985 0 0);
87 | --popover: oklch(0.205 0 0);
88 | --popover-foreground: oklch(0.985 0 0);
89 | --primary: oklch(0.922 0 0);
90 | --primary-foreground: oklch(0.205 0 0);
91 | --secondary: oklch(0.269 0 0);
92 | --secondary-foreground: oklch(0.985 0 0);
93 | --muted: oklch(0.269 0 0);
94 | --muted-foreground: oklch(0.708 0 0);
95 | --accent: oklch(0.269 0 0);
96 | --accent-foreground: oklch(0.985 0 0);
97 | --destructive: oklch(0.704 0.191 22.216);
98 | --border: oklch(1 0 0 / 10%);
99 | --input: oklch(1 0 0 / 15%);
100 | --ring: oklch(0.556 0 0);
101 | --chart-1: oklch(0.488 0.243 264.376);
102 | --chart-2: oklch(0.696 0.17 162.48);
103 | --chart-3: oklch(0.769 0.188 70.08);
104 | --chart-4: oklch(0.627 0.265 303.9);
105 | --chart-5: oklch(0.645 0.246 16.439);
106 | --sidebar: oklch(0.205 0 0);
107 | --sidebar-foreground: oklch(0.985 0 0);
108 | --sidebar-primary: oklch(0.488 0.243 264.376);
109 | --sidebar-primary-foreground: oklch(0.985 0 0);
110 | --sidebar-accent: oklch(0.269 0 0);
111 | --sidebar-accent-foreground: oklch(0.985 0 0);
112 | --sidebar-border: oklch(1 0 0 / 10%);
113 | --sidebar-ring: oklch(0.556 0 0);
114 | }
115 |
116 | @layer base {
117 | * {
118 | @apply border-border outline-ring/50;
119 | }
120 | body {
121 | @apply bg-background text-foreground;
122 | }
123 | }
124 |
125 | .chat-message-content p,
126 | .chat-message-content ul,
127 | .chat-message-content ol {
128 | margin-bottom: 1rem;
129 | }
130 |
131 | .chat-message-content li {
132 | margin-bottom: 0.5rem;
133 | }
134 |
135 | .chat-message-content li > p {
136 | margin-bottom: 0.5rem;
137 | }
138 |
139 | .chat-message-content p:last-child {
140 | margin-bottom: 0;
141 | }
142 |
143 | .chat-message-content a {
144 | text-decoration: underline;
145 | }
146 |
--------------------------------------------------------------------------------
/agent-sdk-chat-with-approval/src/components/ui/prompt-input.tsx:
--------------------------------------------------------------------------------
1 | import { Textarea } from "@/components/ui/textarea"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip"
8 | import { cn } from "@/lib/utils"
9 | import React, {
10 | createContext,
11 | useContext,
12 | useEffect,
13 | useRef,
14 | useState,
15 | } from "react"
16 |
17 | type PromptInputContextType = {
18 | isLoading: boolean
19 | value: string
20 | setValue: (value: string) => void
21 | maxHeight: number | string
22 | onSubmit?: () => void
23 | disabled?: boolean
24 | }
25 |
26 | const PromptInputContext = createContext({
27 | isLoading: false,
28 | value: "",
29 | setValue: () => {},
30 | maxHeight: 240,
31 | onSubmit: undefined,
32 | disabled: false,
33 | })
34 |
35 | function usePromptInput() {
36 | const context = useContext(PromptInputContext)
37 | if (!context) {
38 | throw new Error("usePromptInput must be used within a PromptInput")
39 | }
40 | return context
41 | }
42 |
43 | type PromptInputProps = {
44 | isLoading?: boolean
45 | value?: string
46 | onValueChange?: (value: string) => void
47 | maxHeight?: number | string
48 | onSubmit?: () => void
49 | children: React.ReactNode
50 | className?: string
51 | }
52 |
53 | function PromptInput({
54 | className,
55 | isLoading = false,
56 | maxHeight = 240,
57 | value,
58 | onValueChange,
59 | onSubmit,
60 | children,
61 | }: PromptInputProps) {
62 | const [internalValue, setInternalValue] = useState(value || "")
63 |
64 | const handleChange = (newValue: string) => {
65 | setInternalValue(newValue)
66 | onValueChange?.(newValue)
67 | }
68 |
69 | return (
70 |
71 |
80 |
86 | {children}
87 |
88 |
89 |
90 | )
91 | }
92 |
93 | export type PromptInputTextareaProps = {
94 | disableAutosize?: boolean
95 | } & React.ComponentProps
96 |
97 | function PromptInputTextarea({
98 | className,
99 | onKeyDown,
100 | disableAutosize = false,
101 | ...props
102 | }: PromptInputTextareaProps) {
103 | const { value, setValue, maxHeight, onSubmit, disabled } = usePromptInput()
104 | const textareaRef = useRef(null)
105 |
106 | useEffect(() => {
107 | if (disableAutosize) return
108 |
109 | if (!textareaRef.current) return
110 | textareaRef.current.style.height = "auto"
111 | textareaRef.current.style.height =
112 | typeof maxHeight === "number"
113 | ? `${Math.min(textareaRef.current.scrollHeight, maxHeight)}px`
114 | : `min(${textareaRef.current.scrollHeight}px, ${maxHeight})`
115 | }, [value, maxHeight, disableAutosize])
116 |
117 | const handleKeyDown = (e: React.KeyboardEvent) => {
118 | if (e.key === "Enter" && !e.shiftKey) {
119 | e.preventDefault()
120 | onSubmit?.()
121 | }
122 | onKeyDown?.(e)
123 | }
124 |
125 | return (
126 |