Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Agent streaming and controls

Observe durable content parts, tool progress, approvals, and terminal outcomes.

Stream agent updates to a client while keeping cancellation and other controls available.

Expose the declared controls

Client-exposed agents require an agent-state profile. useAgent exposes only controls declared by the agent: steer, follow-up, stop, and approval/denial. An agent with a reachable approval-requiring tool must declare the approve capability.

/** Composes the offline support model, explicit order/native tools, todo middleware and shared checkpoint/memory drivers. */
import { defineAgent } from "@relkit/langchain";
import { todoListMiddleware } from "langchain";
import { supportInput, supportOutput } from "../../../platform/schemas.js";
import lookupOrder from "../tools/lookup-order.tool.js";
import cancelOrder from "../tools/cancel-order.tool.js";
import { createCommerceModel } from "./commerce-model.js";
import { orderCheckpoints, orderMemory } from "./order-agent-persistence.js";
import { orderStatus } from "./order-native-tool.js";

const orderSupport = defineAgent({
  // Inputs and final answers remain schema checked around the model call.
  input: supportInput,
  output: supportOutput,
  model: createCommerceModel("support"),
  instructions: "Answer order questions using the read-only order lookup tool.",
  // Only explicitly listed application tools are available to the agent.
  tools: [lookupOrder, cancelOrder, orderStatus],
  middleware: [todoListMiddleware()],
  checkpointer: orderCheckpoints,
  store: orderMemory,
  // Bound both tool use and total execution time.
  limits: { maxSteps: 4, maxToolCalls: 4, timeoutMs: 60_000 },
  stateProfile: "agents",
  client: { public: true, state: ["todos"] },
  chat: { input: "message", output: "answer" },
  controls: ["steer", "follow-up", "stop", "approve"],
});

export default orderSupport;

Use from the frontend

After frontend setup and contract pull, use useAgent under RelkitClientProvider. This component selects orders.order-support, the agent above, and reads its typed answer:

"use client";

/** Sends an explicit support request to the commerce agent and renders its public typed answer. */
import { useAgent, useRelkitClient } from "@relkit/client/react";
import { useState } from "react";

/** Uses an application-owned business thread rather than creating one during rendering.
 * @param props - Authorized thread identity retained by the application.
 * @returns A send action, observed answer, and visible submission failure. */
export function OrderSupport({ threadId }: { readonly threadId: string }) {
  const { status } = useRelkitClient();
  const support = useAgent("orders.order-support");
  const [failure, setFailure] = useState("");

  /** Submits once; uncertain acceptance stays visible for explicit recovery.
   * @returns Completion after acceptance or displaying the failure. */
  async function askSupport() {
    setFailure("");
    try {
      await support.send("Where is order demo-1?", { threadId });
    } catch (error) {
      setFailure(String(error));
    }
  }

  return (
    <section>
      <button
        disabled={status !== "ready" || support.status !== "idle" || failure !== ""}
        onClick={askSupport}
      >
        Ask about the order
      </button>
      <p role="status">{support.output?.answer ?? support.status}</p>
      {failure && <p role="alert">{failure}. Check this thread before submitting again.</p>}
    </section>
  );
}

Pass an application-owned thread ID, for example <OrderSupport threadId="support:order-demo-1" />, and retain it when reopening the same conversation. The hook does not create a thread on mount. The selected backend declaration determines the available client operations:

Backend declaration Typed frontend behavior
chat mapping send(message, { threadId }); other agents use run(input, { threadId }).
Input and output schemas Typed initial input and output.
client.state, tool contracts, and custom events Selected values, toolCalls, timeline, and event payloads.
Declared controls Stop, steer, follow-up, approval, and denial methods only when supported.
A graph continuation schema run(reply, { threadId, resume: true }) for the same waiting thread.

observe({ threadId }) reconnects to an existing authorized thread. Never replace an uncertain run by choosing a new thread ID. See threads and recovery and approvals for those flows. Generic HTTP streams use useStream instead.

Observe without blocking execution

Persistent journal acceptance is separate from browser delivery. Slow observers detach with SLOW_CONSUMER and recover from the journal; they never backpressure model or tool execution. Browser projections contain validated public messages, parts, progress, tool data, and results—not provider payloads, internal prompts, credentials, private context, or private reasoning.

Agent content streams and task progress/streams are separate contracts. Use a task job’s run ID and generated job client for background work; do not treat an agent graph state or realtime channel as a task run.