Skip to content

Utilities

Common utility functions for working with ThreadState

Overview

ThreadState provides utility methods for common thread operations. Tools, hooks, and thread endpoints call these methods on the state object they receive.

queueTool

Queue a tool call for execution in the current thread. During an execution, the call joins the tool queue. On an idle thread, it starts a new execution. The method returns void.

state.queueTool('tool_name', { arg: 'value' });

Parameters:

  • 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>;
}

injectMessage() persists the record and runs before_create_message and after_create_message in active and at-rest contexts. During execution, later tools and hooks immediately see it in state.execution.messageHistory. silent controls ordinary UI presentation; silent records still participate in the request context. A context assembly uses one stable history snapshot, so injection from filter_messages or prefilter_llm_history becomes eligible for the following assembly.

Examples:

System Context

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",
    };
  },
});

Silent Message

export default defineTool({
  description: "Add internal note",
  args: z.object({ note: z.string() }),
  execute: async (state, args) => {
    await state.injectMessage({
      role: "system",
      content: args.note,
      silent: true,  // Hidden from ordinary UI lists
    });

    return {
      status: "success",
      result: "Internal note added",
    };
  },
});

getMessages

Retrieve message history from the thread.

const { messages, total, hasMore } = await state.getMessages({
  limit: 50,
  offset: 0,
  order: 'desc',
});

Options:

Option Type Default Description
limit number 100 Max messages to return
offset number 0 Messages to skip
order 'asc' | 'desc' 'desc' Sort order
includeSilent boolean false Include silent messages
maxDepth number 0 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). When it is undefined, clients receive "data": null.

Event Structure (received by clients):

{
  "type": "event",
  "eventType": "<your-type>",
  "data": <your-data>,
  "timestamp": 1234567890
}

Examples:

Progress Updates

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`,
    };
  },
});

Status Updates

export default defineTool({
  description: "Upload file",
  args: z.object({ fileUrl: z.string() }),
  execute: async (state, args) => {
    state.emit("upload_status", {
      status: "started",
      message: "Starting download...",
    });

    const file = await downloadFile(args.fileUrl);

    state.emit("upload_status", {
      status: "processing",
      message: "Processing file...",
    });

    const result = await processFile(file);

    state.emit("upload_status", {
      status: "complete",
      message: "Upload complete!",
    });

    return {
      status: "success",
      result: "File processed",
    };
  },
});

Frontend Integration

Listen for events on the frontend using @standardagents/react:

import {
  AgentBuilderProvider,
  ThreadProvider,
  onThreadEvent,
} from "@standardagents/react";

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

function OrderTracker() {
  onThreadEvent<{ percentage: number }>('progress', (data) => {
    console.log('Progress:', data.percentage);
  });

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

forceTurn (Execution Control)

Force the next execution turn to a specific side.

// Only available during execution
if (state.execution) {
  state.execution.forceTurn('b');
}

Parameters:

  • side: The side to force ('a' or 'b')

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: '[email protected]', 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

See Effects for effect definitions, cancellation, and failure behavior.


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

Error Handling

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 instanceof Error ? error.message : String(error)}`,
  };
}
Throttle Event Emissions

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 });
}
Message Pagination

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;
}
Check Execution State

When using execution-specific features, check if execution is active:

if (state.execution) {
  console.log(`Step: ${state.execution.stepCount}`);
  state.execution.forceTurn('b');
}

Troubleshooting

Events not received by frontend

Solution: Ensure you're using ThreadProvider and that you've subscribed to the correct event type name.

Tool not executing after queueTool

Solution: Check the callable name and the current tool's uses list. Check whether the execution stopped before the queued call ran.

getMessages returns empty

Solution: Check that messages exist in the thread and that limit/offset parameters are correct.

Next Steps