Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
npm i
npm run build
- name: Publish package for testing branch
run: npx pkg-pr-new publish || echo "Have you set up pkg-pr-new for this repo?"
run: npx pkg-pr-new publish || echo "pkg-pr-new failed"
- name: Test
run: |
npm run test
Expand Down
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import convexPlugin from "@convex-dev/eslint-plugin";

export default [
{
Expand Down Expand Up @@ -51,7 +52,12 @@ export default [
languageOptions: {
globals: globals.worker,
},
plugins: {
"@convex-dev": convexPlugin,
},
rules: {
...convexPlugin.configs.recommended[0].rules,

"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-explicit-any": "off",
"no-unused-vars": "off",
Expand Down
24 changes: 13 additions & 11 deletions example/convex/approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
import { describe, expect, test } from "vitest";
import { Agent, createTool, stepCountIs, mockModel } from "@convex-dev/agent";
import { anyApi, actionGeneric, mutationGeneric } from "convex/server";
import type { ApiFromModules, ActionBuilder, MutationBuilder } from "convex/server";
import type {
ApiFromModules,
ActionBuilder,
MutationBuilder,
} from "convex/server";
import { v } from "convex/values";
import { components } from "./_generated/api.js";
import { initConvexTest } from "./setup.test.js";
Expand Down Expand Up @@ -153,9 +157,7 @@ export const testApproveE2E = action({
const approvalMsg = result1.savedMessages?.find(
(m) =>
Array.isArray(m.message?.content) &&
m.message!.content.some(
(p: any) => p.type === "tool-approval-request",
),
m.message!.content.some((p: any) => p.type === "tool-approval-request"),
);
if (!approvalMsg)
throw new Error("No approval request found in saved messages");
Expand Down Expand Up @@ -209,9 +211,7 @@ export const testDenyE2E = action({
const approvalMsg = result1.savedMessages?.find(
(m) =>
Array.isArray(m.message?.content) &&
m.message!.content.some(
(p: any) => p.type === "tool-approval-request",
),
m.message!.content.some((p: any) => p.type === "tool-approval-request"),
);
if (!approvalMsg) throw new Error("No approval request found");
const approvalPart = (approvalMsg.message!.content as any[]).find(
Expand Down Expand Up @@ -323,7 +323,11 @@ export const submitApprovalForTestApprovalAgent = mutation({
reason: v.optional(v.string()),
},
handler: async (ctx, { threadId, approvalId, reason }) => {
return testApprovalAgent.approveToolCall(ctx, { threadId, approvalId, reason });
return testApprovalAgent.approveToolCall(ctx, {
threadId,
approvalId,
reason,
});
},
});

Expand Down Expand Up @@ -403,9 +407,7 @@ describe("Example Approval E2E (exercises usageHandler + insertRawUsage)", () =>
const t = initConvexTest();
const result = await t.action(testApi.testDenyE2E, {});

expect(result.secondText).toBe(
"Understood, I won't delete that file.",
);
expect(result.secondText).toBe("Understood, I won't delete that file.");
expect(result.totalThreadMessages).toBeGreaterThanOrEqual(4);

const rawUsageDocs = await t.run(async (ctx) => {
Expand Down
12 changes: 6 additions & 6 deletions example/convex/chat/approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
// 5. AI SDK automatically handles the approval: executes tool (if approved)
// or creates execution-denied result (if denied), then continues generation
import { paginationOptsValidator } from "convex/server";
import {
listUIMessages,
syncStreams,
vStreamArgs,
} from "@convex-dev/agent";
import { listUIMessages, syncStreams, vStreamArgs } from "@convex-dev/agent";
import { components, internal } from "../_generated/api";
import { internalAction, mutation, query } from "../_generated/server";
import { v } from "convex/values";
Expand Down Expand Up @@ -74,7 +70,11 @@ export const submitApproval = mutation({
handler: async (ctx, { threadId, approvalId, approved, reason }) => {
await authorizeThreadAccess(ctx, threadId);
const { messageId } = approved
? await approvalAgent.approveToolCall(ctx, { threadId, approvalId, reason })
? await approvalAgent.approveToolCall(ctx, {
threadId,
approvalId,
reason,
})
: await approvalAgent.denyToolCall(ctx, { threadId, approvalId, reason });
return { messageId };
},
Expand Down
1 change: 1 addition & 0 deletions example/convex/usage_tracking/invoicing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ async function createInvoice(
.withIndex("billingPeriod_userId", (q) =>
q.eq("billingPeriod", billingPeriod).eq("userId", invoice.userId),
)
// eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many failed invoices
.filter((q) => q.neq(q.field("status"), "failed"))
.first();
if (existingInvoice) {
Expand Down
35 changes: 26 additions & 9 deletions example/ui/chat/ChatApproval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export default function ChatApproval() {
}

function Chat({ threadId, reset }: { threadId: string; reset: () => void }) {
const { results: messages, status, loadMore } = useUIMessages(
const {
results: messages,
status,
loadMore,
} = useUIMessages(
api.chat.approval.listThreadMessages,
{ threadId },
{ initialNumItems: 10, stream: true },
Expand All @@ -47,7 +51,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) {
);

const submitApproval = useMutation(api.chat.approval.submitApproval);
const triggerContinuation = useMutation(api.chat.approval.triggerContinuation);
const triggerContinuation = useMutation(
api.chat.approval.triggerContinuation,
);

// Track the last approval messageId so we can use it for continuation.
const lastApprovalMessageIdRef = useRef<string | null>(null);
Expand All @@ -56,7 +62,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) {

const hasPendingApprovals = messages.some((m) =>
m.parts.some(
(p) => p.type.startsWith("tool-") && (p as ToolUIPart).state === "approval-requested",
(p) =>
p.type.startsWith("tool-") &&
(p as ToolUIPart).state === "approval-requested",
),
);

Expand Down Expand Up @@ -90,7 +98,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) {
lastApprovalMessageIdRef.current = messageId;
}

const [prompt, setPrompt] = useState("Delete the file important.txt and transfer $500 to account savings-123");
const [prompt, setPrompt] = useState(
"Delete the file important.txt and transfer $500 to account savings-123",
);
const messagesEndRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down Expand Up @@ -142,7 +152,11 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) {
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
placeholder={hasPendingApprovals ? "Respond to pending approvals first..." : "Ask the agent to do something..."}
placeholder={
hasPendingApprovals
? "Respond to pending approvals first..."
: "Ask the agent to do something..."
}
disabled={hasPendingApprovals}
/>
<button
Expand All @@ -159,7 +173,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) {
reset();
lastApprovalMessageIdRef.current = null;
continuationTriggeredRef.current = false;
setPrompt("Delete the file important.txt and transfer $500 to account savings-123");
setPrompt(
"Delete the file important.txt and transfer $500 to account savings-123",
);
}}
type="button"
>
Expand Down Expand Up @@ -189,8 +205,8 @@ function Message({
const isUser = message.role === "user";

// Find tool parts that need approval
const toolParts = message.parts.filter(
(p): p is ToolUIPart => p.type.startsWith("tool-"),
const toolParts = message.parts.filter((p): p is ToolUIPart =>
p.type.startsWith("tool-"),
);

return (
Expand Down Expand Up @@ -330,7 +346,8 @@ function ToolCallDisplay({
</div>
)}

{(tool.state === "input-available" || tool.state === "input-streaming") && (
{(tool.state === "input-available" ||
tool.state === "input-streaming") && (
<div className="text-gray-500 text-xs">Processing...</div>
)}
</div>
Expand Down
2 changes: 0 additions & 2 deletions example/ui/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { StrictMode } from "react";
import StreamArray from "./objects/StreamArray";
import ChatApproval from "./chat/ChatApproval";


const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

createRoot(document.getElementById("root")!).render(
Expand Down Expand Up @@ -49,7 +48,6 @@ export function App() {
<Route path="/weather-fashion" element={<WeatherFashion />} />
<Route path="/stream-array" element={<StreamArray />} />
<Route path="/chat-approval" element={<ChatApproval />} />

</Routes>
</main>
<Toaster />
Expand Down
Loading
Loading