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 callargs: 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 invokeargs: 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 namedata: Event payload (must be JSON-serializable). When it isundefined, 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 inagents/effects/)args: Arguments passed to effect handlerdelay: 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.