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.
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` ,
};
} ,
}) ;
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:
System Context
Silent Message
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" ,
};
} ,
}) ;
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 LLM, stored for audit
});
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 limitnumber- Max messages to return offsetnumber0 Messages to skip order'asc' | 'desc''desc'Sort order includeSilentbooleanfalse Include 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:
Progress Updates
Status 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` ,
};
} ,
}) ;
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 {
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 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: Tools are queued for sequential execution. Ensure the current tool completes successfully and doesn’t stop execution.
getMessages returns empty
Solution: Check that messages exist in the thread and that limit/offset parameters are correct.
Next Steps