Skip to main content

Overview

Standard Agents provides utility functions that wrap common ThreadState operations. While you can call ThreadState methods directly, these utilities are exported from @standardagents/builder for convenience and backwards compatibility.
Most utilities are now methods on ThreadState. You can use either the standalone functions or call methods directly on state.

queueTool

Queue a tool call for execution in the current thread.
// Using the utility function
import { queueTool } from "@standardagents/builder";
queueTool(state, 'tool_name', { arg: 'value' });

// Or call directly on state
state.queueTool('tool_name', { arg: 'value' });
Parameters:
  • state: The ThreadState context
  • toolName: The name of the tool to call
  • args: Arguments to pass to the tool
Example:
import { defineTool } from "@standardagents/spec";
import { z } from "zod";

export default defineTool({
  description: "Process user order and send confirmation",
  args: z.object({
    orderId: z.string(),
    userId: z.string(),
  }),
  execute: async (state, args) => {
    // Validate the order
    const order = await validateOrder(args.orderId);

    if (!order.valid) {
      return {
        status: "error",
        error: "Order validation failed",
      };
    }

    // Queue additional tools to execute after this one
    state.queueTool("charge_payment", {
      orderId: args.orderId,
      amount: order.total,
    });

    state.queueTool("send_confirmation_email", {
      userId: args.userId,
      orderId: args.orderId,
    });

    state.queueTool("update_inventory", {
      items: order.items,
    });

    return {
      status: "success",
      result: `Order ${args.orderId} queued for processing`,
    };
  },
});

invokeTool

Invoke a tool directly and wait for the result.
const result = await state.invokeTool('tool_name', { arg: 'value' });

if (result.status === 'success') {
  console.log(result.result);
}
Parameters:
  • toolName: The name of the tool to invoke
  • args: Arguments to pass to the tool
Returns: Promise<ToolResult>

injectMessage

Inject a message into the thread conversation.
const message = await state.injectMessage({
  role: 'user',
  content: 'Additional context...',
  silent: true,
  metadata: { source: 'tool' },
});
Options:
interface InjectMessageInput {
  role: 'user' | 'assistant' | 'system';
  content: string;
  silent?: boolean;          // Hide from UI
  metadata?: Record<string, unknown>;
}
Examples:
export default defineTool({
  description: "Load user context",
  args: z.object({ userId: z.string() }),
  execute: async (state, args) => {
    const user = await fetchUser(args.userId);

    await state.injectMessage({
      role: "system",
      content: `User context loaded: ${user.name} (${user.email})
        - Account tier: ${user.tier}
        - Preferences: ${JSON.stringify(user.preferences)}`,
    });

    return {
      status: "success",
      result: "User context injected",
    };
  },
});

getMessages

Retrieve message history from the thread.
const { messages, total, hasMore } = await state.getMessages({
  limit: 50,
  offset: 0,
  order: 'desc',
});
Options:
OptionTypeDefaultDescription
limitnumber-Max messages to return
offsetnumber0Messages to skip
order'asc' | 'desc''desc'Sort order
includeSilentbooleanfalseInclude silent messages
maxDepthnumber-Max sub-prompt nesting
Example:
export default defineTool({
  description: "Analyze recent conversation tone",
  execute: async (state) => {
    // Get last 10 messages
    const { messages } = await state.getMessages({ limit: 10 });

    // Filter user and assistant messages only
    const conversationMessages = messages.filter(
      (m) => m.role === "user" || m.role === "assistant"
    );

    const tone = analyzeTone(conversationMessages);

    return {
      status: "success",
      result: `Conversation tone: ${tone}`,
    };
  },
});

emit (Custom Events)

Send custom events to WebSocket clients.
state.emit('event_type', {
  key: 'value',
  progress: 50,
});
Parameters:
  • event: Event type name
  • data: Event payload (must be JSON-serializable)
Event Structure (received by clients):
{
  "type": "event",
  "eventType": "<your-type>",
  "data": <your-data>,
  "timestamp": 1234567890
}
Examples:
export default defineTool({
  description: "Process large dataset",
  args: z.object({ datasetId: z.string() }),
  execute: async (state, args) => {
    const dataset = await loadDataset(args.datasetId);
    const total = dataset.length;

    for (let i = 0; i < total; i++) {
      await processItem(dataset[i]);

      // Emit progress event every 10 items
      if (i % 10 === 0) {
        state.emit("progress", {
          current: i + 1,
          total,
          percentage: ((i + 1) / total) * 100,
          message: `Processing item ${i + 1} of ${total}`,
        });
      }
    }

    return {
      status: "success",
      result: `Processed ${total} items`,
    };
  },
});

Frontend Integration

Listen for events on the frontend using @standardagents/react:
import {
  StandardAgentsProvider,
  ThreadProvider,
  useThread,
} from "@standardagents/react";

function App() {
  return (
    <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
      <ThreadProvider threadId="123e4567-e89b-12d3-a456-426614174000">
        <OrderTracker />
      </ThreadProvider>
    </StandardAgentsProvider>
  );
}

function OrderTracker() {
  const { onCustomEvent } = useThread();

  // Listen for custom events
  useEffect(() => {
    const unsubscribe = onCustomEvent('progress', (data) => {
      console.log('Progress:', data.percentage);
    });
    return unsubscribe;
  }, [onCustomEvent]);

  return <div>...</div>;
}

forceTurn (Execution Control)

Force the next execution turn to a specific side (for dual_ai agents).
// Only available during execution
if (state.execution) {
  state.execution.forceTurn('b');
}
Parameters:
  • side: The side to force ('a' or 'b')
This is primarily used for dual_ai agents to control turn order.

stop (Execution Control)

Stop the current execution after the current operation completes.
if (state.execution) {
  state.execution.stop();
}

scheduleEffect

Schedule an effect for delayed or background execution.
const effectId = await state.scheduleEffect(
  'send_reminder_email',
  { to: 'user@example.com', subject: 'Reminder' },
  30 * 60 * 1000  // 30 minutes
);
Parameters:
  • name: Effect name (file in agents/effects/)
  • args: Arguments passed to effect handler
  • delay: Delay in milliseconds (default: 0)
Returns: Effect ID (UUID) for later cancellation

Complete Example

Here’s a comprehensive example showing multiple utilities working together:
import { defineTool } from "@standardagents/spec";
import { z } from "zod";

export default defineTool({
  description: "Complete order processing workflow",
  args: z.object({
    orderId: z.string(),
    userId: z.string(),
    notifyUser: z.boolean().default(true),
  }),
  execute: async (state, args) => {
    // Emit initial status event
    state.emit("order_status", {
      orderId: args.orderId,
      status: "started",
    });

    // Inject system message for context
    await state.injectMessage({
      role: "system",
      content: `Processing order ${args.orderId} for user ${args.userId}`,
    });

    // Queue validation tool
    state.queueTool("validate_order", {
      orderId: args.orderId,
    });

    // Queue payment processing
    state.queueTool("process_payment", {
      orderId: args.orderId,
      userId: args.userId,
    });

    // Queue inventory update
    state.queueTool("update_inventory", {
      orderId: args.orderId,
    });

    // Conditionally queue notification
    if (args.notifyUser) {
      state.queueTool("send_notification", {
        userId: args.userId,
        type: "order_confirmation",
        orderId: args.orderId,
      });
    }

    // Get conversation history for audit
    const { messages } = await state.getMessages({ limit: 50 });
    const orderMessages = messages.filter(
      (m) => m.content?.includes(args.orderId)
    );

    // Emit completion event
    state.emit("order_status", {
      orderId: args.orderId,
      status: "queued",
      steps: 4,
      relatedMessages: orderMessages.length,
    });

    return {
      status: "success",
      result: `Order ${args.orderId} workflow initiated`,
    };
  },
});

Best Practices

Always wrap async operations in try-catch blocks:
try {
  await state.injectMessage({
    role: "system",
    content: "Context updated",
  });
} catch (error) {
  console.error("Failed to inject message:", error);
  return {
    status: "error",
    error: `Message injection failed: ${error.message}`,
  };
}
Avoid excessive event emissions that could overwhelm WebSocket clients:
// Good: Throttle emissions
for (let i = 0; i < 10000; i++) {
  if (i % 100 === 0) {
    state.emit("progress", {
      current: i,
      total: 10000,
    });
  }
}

// Avoid: Emitting inside tight loop
for (let i = 0; i < 10000; i++) {
  state.emit("progress", { value: i });
}
When retrieving large message histories, use pagination:
const pageSize = 50;
let offset = 0;
let hasMore = true;

while (hasMore) {
  const result = await state.getMessages({ limit: pageSize, offset });
  await processMessages(result.messages);
  hasMore = result.hasMore;
  offset += pageSize;
}
When using execution-specific features, check if execution is active:
if (state.execution) {
  console.log(`Step: ${state.execution.stepCount}`);
  state.execution.forceTurn('b');
}

Troubleshooting

Solution: Ensure you’re using ThreadProvider and that you’ve subscribed to the correct event type name.
Solution: Tools are queued for sequential execution. Ensure the current tool completes successfully and doesn’t stop execution.
Solution: Check that messages exist in the thread and that limit/offset parameters are correct.

Next Steps