nvoken
DocumentationPatterns

Build

Patterns

Build memory, scheduled turns, and multi-agent orchestration on nvoken's primitives, where the host stays in charge.

Three things come up in every evaluation: memory, scheduling, and multi-agent orchestration. nvoken does not ship any of them as a feature, and that is a choice rather than a backlog.

Each one needs product judgment nvoken does not have — what is worth remembering, when a job should run, which agent goes next. A platform that decided those for you would be a second product to keep in sync with yours. What nvoken provides instead are primitives sharp enough that each pattern is a small amount of ordinary code in your application.

Memory

nvoken stores conversations, not knowledge. There is no vector store, no memory API, and no background process reading your transcripts.

The host supplies context. Two places to put it:

In instructions, when the context is known before the turn starts:

const facts = await db.retrieveContext(customerId, question);
 
const agent = client.agent({
  agentKey: "support",
  instructions: `${basePrompt}\n\nRelevant account facts:\n${facts}`,
});
 
await agent.text(question, { sessionKey: `customer-${customerId}` });

Building the facade per turn costs nothing — it registers nothing and makes no request. Instructions travel with each Invocation, so the retrieved context can differ every turn with no migration and no stale copy.

In a host tool, when the agent should decide whether it needs the context:

const agent = client.agent({
  agentKey: "support",
  tools: [
    {
      mode: "host",
      name: "search_knowledge_base",
      description: "Search internal docs for an answer.",
      inputSchema: {
        type: "object",
        properties: { query: { type: "string" } },
        required: ["query"],
        additionalProperties: false,
      },
      handler: ({ query }) => kb.search(query as string),
    },
  ],
});

The retrieval runs in your application, against your data, under your access rules. nvoken records the call and its result in the transcript, so a support question about why the agent said something has an answer.

For memory that persists across conversations, write it in your own database at the end of a turn and retrieve it at the start of the next. The Session holds the conversation; your product holds what it learned.

Scheduled and recurring turns

You already have the two pieces this needs: a scheduler and a required idempotency key.

// Runs every morning from your own cron.
async function dailyDigest(accountId: string, day: string) {
  await agent.invoke("Summarize yesterday's activity.", {
    sessionKey: `digest-${accountId}`,
    idempotencyKey: `digest:${accountId}:${day}`,
  });
}

Because idempotency_key is required and nvoken compares the whole request, a cron that fires twice, a queue that redelivers, or a retry after an ambiguous timeout all produce one turn. The second attempt returns the original with deduplicated: true. Scheduled work is retry-safe without a lock table.

Two details make this durable in practice:

  • Use invoke() rather than text(). Admission returns immediately, and a worker reads the result later. The turn does not depend on your cron process staying alive.
  • Derive the idempotency key from the schedule slot, not from the clock at send time. digest:acct-12:2026-08-08 is stable across retries; digest:acct-12:${Date.now()} is not.

Set a webhook target if you want to be told when the turn settles instead of polling for it.

Multi-agent orchestration

Your application is the orchestrator. An "agent" is an agent_key plus the instructions and tools you send with a turn, so running several is just making several Invocations.

In parallel, when the work is independent — use separate Sessions:

const [research, pricing] = await Promise.all([
  researcher.text(question, { sessionKey: `${jobId}-research` }),
  analyst.text(question, { sessionKey: `${jobId}-pricing` }),
]);
 
const answer = await editor.text(
  `Combine these findings:\n\n${research}\n\n${pricing}`,
  { sessionKey: `${jobId}-final` },
);

In sequence within one conversation, when later turns should see what earlier ones did — reuse the Session and change the definition per turn. Each Invocation carries its own instructions, model, and tools, so a cheap triage model and an expensive reasoning model can take turns in the same thread.

The primitives that make this safe:

  • One turn at a time per Session. A second concurrent admission is rejected with session_invocation_active. You cannot accidentally interleave two agents' output in one conversation.
  • if_active when you deliberately want to replace live work: "interrupt" stops the current turn and keeps what it produced before admitting the replacement; "supersede" discards it.
  • agent_key as the filter. Every Invocation records which agent ran, so usage, transcripts, and dashboards can be read per agent.

What nvoken will not do is decide which agent runs next, pass control between them, or enforce a handoff protocol. That logic belongs where the rest of your product's rules live.

Where this boundary comes from

The same reasoning runs through all three: nvoken stores what your agent did, never what it does. Adding memory policy, schedules, or an orchestration graph would mean storing behavior — and a stored copy of your product's behavior is a copy that drifts.

What nvoken supports is the full list of what is and is not in the box.