Ian Macartney

@ianmacartney.bsky.social

Friendly engineer at Convex.dev

I feel clever subbing `return await fn()` with `return fn()` But gotchas don't seem worth it anymore • Breaks try/catch/finally expectations • Loses stack trace context My default is now to always await. Change my mind.

Documentation for the Agent component is live 🎉 -> docs.convex​.dev/agents Odd coincidence that there's ~2300 lines of documentation & ~2300 lines of example code 🤔 ...maybe more surprising there's fewer lines of React? @​convex-dev/agent

Bild

Rate limiting LLM chat per-user with `useRateLimit` const { status } = useRateLimit(api.rl.getRateLimit) Full code in 🧵 Algorithms: Sending messages: fixed window Token usage: token bucket courtesy of @​convex-dev/rate-limiter + @​convex-dev/agent (components)

Recent talk on agents & agentic workflows, as well as my Convex Agent Component. Takes: • Agentic := prompting + routing • Prompting is input -> LLM -> output • Routing has code at every boundary (...even if an LLM "decides" what to do next) Full 📺🔗->🧵

Streaming LLM text using websockets + client smoothing - no HTTP necessary! Agent v0.2.1 is out! Repo & release notes in 🧵 - Streaming text react hook + server fns - Client-side smoothing hook - Optimistic update helpers

Agent Playground for @​convex-dev/agent is live! Investigate threads, messages, tool calls Dial in context params Iterate on prompting, etc. For the @​convex-dev/agent component. Links in 🧵

Not all embeddings are created equal. Some represent more meaningful context. I struggled with AI Town to efficiently do vector search that also included a 1-10 "importance" Last night I figured out a way to prioritize some embeddings over others using a "bias" feature 🧵

I do RAG via hybrid text/vector search using reciprocal rank fusion (the one-weird-trick of hybrid search imo) for my new Agent framework/component. It's open source and the code is remarkably simple, if you're looking for an example for yourself.

    let textSearchMessages: Doc<"messages">[] | undefined;
    if (args.text) {
      textSearchMessages = await ctx.runQuery(api.messages.textSearch, {
        userId: args.userId,
        threadId: args.threadId,
        text: args.text,
        limit,
      });
    }
    if (args.vector) {
      const dimension = args.vector.length as VectorDimension;
      if (!VectorDimensions.includes(dimension)) {
        throw new Error(`Unsupported vector dimension: ${dimension}`);
      }
      const vectors = (
        await searchVectors(ctx, args.vector, {
          dimension,
          model: args.vectorModel ?? "unknown",
          table: "messages",
          userId: args.userId,
          threadId: args.threadId,
          limit,
        })
      ).filter((v) => v._score > (args.vectorScoreThreshold ?? 0));
      // Reciprocal rank fusion
      const k = 10;
      const textEmbeddingIds = textSearchMessages?.map((m) => m.embeddingId);
      const vectorScores = vectors
        .map((v, i) => ({
          id: v._id,
          score:
            1 / (i + k) +
            1 / ((textEmbeddingIds?.indexOf(v._id) ?? Infinity) + k),
        }))
        .sort((a, b) => b.score - a.score);
      const vectorIds = vectorScores.slice(0, limit).map((v) => v.id);
      const messages: Doc<"messages">[] = await ctx.runQuery(
        internal.messages._fetchVectorMessages,
        {
          userId: args.userId,
          threadId: args.threadId,
          vectorIds,
          textSearchMessages: textSearchMessages?.filter(
            (m) => !vectorIds.includes(m.embeddingId!)
          ),
          messageRange: args.messageRange ?? DEFAULT_MESSAGE_RANGE,
          parentMessageId: args.parentMessageId,
          limit,
        }
      );
      return messages;
    }
    return textSearchMessages?.flat() ?? [];

Exciting news for Agent Workflow front: 🪨Durable Workflows🪨: Orchestrate steps async with retries, checkpointing and more, using Inngest-style syntax 🤖 Agent Framework 🤖: Define agents and use threaded memory (can hand off between agents), with hybrid text/vector search. 🧵

Welp not all experiments work out, but what will outlive all products is the insights you glean along the way. I reimplemented Mastra workflows in Convex last week and I regret it. Article in 🧵

Bild

I made some cartoons today for an article I'm writing. This one is me when I'm too deep in a project that's gone off the rails, complete with alt text. Should I ship it or make a "better" version to make it "more professional"?

Where's Ian? Oh, he's in a code hole. Should we tell him his idea is risky? No, I mean his head is literally in a hole coding. He's an ostrich, did you know that?

What I meant to say was "correctness & reliability" - mental blip when I first posted this. The reality is that while everyone's talking about how cool their app is in the happy path, actually handling failure is near impossible when you're using leaky abstractions.

Ian Macartney@ianmacartney.bsky.social · last yr.

Thoughts on about safety & reliability in the age of agentic flows and durable workflows: Details in 🧵, but tl;dr: 1️⃣ Isolate unreliable steps to safely retry 2️⃣ Model LLMs as ~idempotent & ~deterministic 3️⃣ Run asynchronously, subscribe to results Bonus: I made a thing

Thoughts on about safety & reliability in the age of agentic flows and durable workflows: Details in 🧵, but tl;dr: 1️⃣ Isolate unreliable steps to safely retry 2️⃣ Model LLMs as ~idempotent & ~deterministic 3️⃣ Run asynchronously, subscribe to results Bonus: I made a thing

Bild

I'm working on handling [agentic] workflows in @convex.dev using Mastra to define them. Anyone interested in an alpha I made over the last week? Based on the architecture diagram I shared. It uses my durable Workpool component to execute them with parallelism limits, retries, etc. (Open Source)

Self-hosting Convex just got way easier, including running the dashboard. npx degit get-convex/convex-backend/self-hosted/fly fly && cd fly/backend && fly launch Dockerfiles, images, binaries @flydotio and more. Check it out 🧵

Bild

I used AI to write a script to help me write AI evals to help AI write Convex code. Very meta 💡Each step uses RAG from previous evals Flow: 1. 🧑‍💻oneliner -> 🤖 task description -> 🧑‍💻 audit 2. 🤖 answer ->🧑‍💻 audit 3. 🤖 tests for answer ->🧑‍💻 audit 🔗 to evals 👀 & my script in 🧵

GitHub - get-convex/convex-evals

Contribute to get-convex/convex-evals development by creating an account on GitHub.

github.com

Another feature of the BlockNote/Tiptap sync component that might interest folks: Server-side document editing, w/o clobbering user changes You can access & transform a document, but the really cool thing is that you provide a callback to rebase your transform on the latest version.

export const transformExample = action({
  args: { id: v.string() },
  handler: async (ctx, { id }) => {
    const schema = getSchema(extensions);
    const { doc, version } = await prosemirrorSync.getDoc(ctx, id, schema);
    const newContent = await generateAIContent(doc);
    const node = await prosemirrorSync.transform(ctx, id, schema, (doc, v) => {
      if (v !== version) {
        // If we wanted to avoid making changes, we could return null here.
        // Or we could rebase our changes on top of the new document.
      }
      const tr = EditorState.create({ doc }).tr;
      return tr.insertText(newContent, 0);
    });
    await updateDocSearchIndex(ctx, node);
  },
});

Some improvements to the open-source collaborative editor sync component landed this week: 📝 Now supports syncing *BlockNote* & @tiptap.dev 🦾 Server-side editing of documents is now easier & documented, for those of you using AI thingies