What is ThreadState?
ThreadState is the unified interface for all thread operations in Standard Agents. It provides a consistent API for interacting with threads regardless of context—whether from tools during execution, hooks, or HTTP endpoints.
ThreadState gives you access to:
- Thread identity (ID, agent, user)
- Message operations (read, inject, update, delete)
- File system operations
- Tool invocation
- Effect scheduling
- Event emission
- Resource loading (models, prompts, agents)
ThreadState Interface
interface ThreadState {
// Identity (readonly)
readonly threadId: string;
readonly agentId: string;
readonly userId: string | null;
readonly createdAt: number;
// Messages
getMessages(options?: GetMessagesOptions): Promise<MessagesResult>;
getMessage(messageId: string): Promise<Message | null>;
injectMessage(input: InjectMessageInput): Promise<Message>;
updateMessage(messageId: string, updates: MessageUpdates): Promise<Message>;
deleteMessage(messageId: string): Promise<boolean>;
// Resource Loading
loadModel<T = unknown>(name: string): Promise<T>;
loadPrompt<T = unknown>(name: string): Promise<T>;
loadAgent<T = unknown>(name: string): Promise<T>;
getPromptNames(): string[];
getAgentNames(): string[];
getModelNames(): string[];
// Tool Invocation
queueTool(toolName: string, args: Record<string, unknown>): void;
invokeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
// Effect Scheduling
scheduleEffect(name: string, args: Record<string, unknown>, delay?: number): Promise<string>;
getScheduledEffects(name?: string): Promise<ScheduledEffect[]>;
removeScheduledEffect(id: string): Promise<boolean>;
// Agent Execution
runAgent(agentName: string): Promise<void>;
// Events
emit(event: string, data: unknown): void;
// Context Storage
context: Record<string, unknown>;
// File System
writeFile(path: string, data: ArrayBuffer | string, mimeType: string, options?: WriteFileOptions): Promise<FileRecord>;
readFile(path: string): Promise<ArrayBuffer | null>;
readFileStream(path: string, options?: ReadFileStreamOptions): Promise<AsyncIterable<FileChunk> | null>;
statFile(path: string): Promise<FileRecord | null>;
readdirFile(path: string): Promise<ReaddirResult>;
unlinkFile(path: string): Promise<void>;
mkdirFile(path: string): Promise<FileRecord>;
rmdirFile(path: string): Promise<void>;
getFileStats(): Promise<FileStats>;
grepFiles(pattern: string): Promise<GrepResult[]>;
findFiles(pattern: string): Promise<FindResult>;
getFileThumbnail(path: string): Promise<ArrayBuffer | null>;
// Execution State (null when at rest)
execution: ExecutionState | null;
// Runtime Context (non-portable)
readonly _notPackableRuntimeContext?: Record<string, unknown>;
}
Accessing ThreadState
ThreadState is passed as the first argument to tool handlers, hooks, and thread endpoints:
Identity Properties
| Property | Type | Description |
|---|
threadId | string | Unique thread identifier |
agentId | string | Agent that owns this thread |
userId | string | null | Associated user (if any) |
createdAt | number | Creation timestamp (microseconds since epoch) |
Messages
Reading Messages
// Get recent messages with pagination
const { messages, total, hasMore } = await state.getMessages({
limit: 50,
offset: 0,
order: 'desc',
});
// Get a single message
const message = await state.getMessage('msg-123');
Query Options
| Option | Type | Default | Description |
|---|
limit | number | - | Maximum messages to return |
offset | number | 0 | Messages to skip |
order | 'asc' | 'desc' | 'desc' | Sort order |
includeSilent | boolean | false | Include silent messages |
maxDepth | number | - | Max nesting depth for sub-prompts |
Injecting Messages
const message = await state.injectMessage({
role: 'user',
content: 'Additional context...',
silent: true, // Hide from UI
metadata: { source: 'tool' },
});
Updating Messages
const updated = await state.updateMessage('msg-123', {
content: 'Updated content',
metadata: { edited: true },
});
Deleting Messages
const deleted = await state.deleteMessage('msg-123');
// Returns true if found and deleted, false if not found
Queue a tool for asynchronous execution:
// Non-blocking - adds to execution queue
state.queueTool('send_email', {
to: 'user@example.com',
subject: 'Hello',
body: 'Message content',
});
If the thread is currently executing, the tool is added to the queue. Otherwise, a new execution is started.
Invoke a tool and wait for the result:
// Blocking - waits for completion
const result = await state.invokeTool('get_weather', {
location: 'San Francisco',
});
if (result.status === 'success') {
console.log(result.result);
}
Effect Scheduling
Effects are scheduled operations that run outside the conversation flow. Unlike tools which execute immediately, effects can be delayed and run independently.
Schedule an Effect
// Schedule to run after 30 minutes
const effectId = await state.scheduleEffect(
'send_reminder_email',
{ to: 'user@example.com', subject: 'Reminder' },
30 * 60 * 1000 // delay in milliseconds
);
Manage Effects
// Get all pending effects
const effects = await state.getScheduledEffects();
// Get effects filtered by name
const reminders = await state.getScheduledEffects('send_reminder_email');
// Cancel a scheduled effect
const removed = await state.removeScheduledEffect(effectId);
ScheduledEffect Record
| Property | Type | Description |
|---|
id | string | Unique effect ID (UUID) |
name | string | Effect name |
args | Record<string, unknown> | Arguments to pass |
scheduledAt | number | Execution time (microseconds) |
createdAt | number | When scheduled (microseconds) |
Event Emission
Send custom events to connected clients via WebSocket:
state.emit('progress', {
step: 3,
total: 10,
message: 'Processing data...',
});
Event names should be lowercase with underscores. Event data must be JSON-serializable.
Context Storage
The context property provides temporary key-value storage:
// Store data during execution
state.context.userData = { id: 123, name: 'Alice' };
// Retrieve in another tool call
const user = state.context.userData;
Context is scoped to a single execution and lost when execution ends. For persistent storage, use the file system or messages.
Resource Loading
Load registered definitions at runtime:
// Load a model definition
const model = await state.loadModel('gpt-4o');
// Load a prompt definition
const prompt = await state.loadPrompt('support-prompt');
// Load an agent definition
const agent = await state.loadAgent('customer-support');
// List available resources
const prompts = state.getPromptNames();
const agents = state.getAgentNames();
const models = state.getModelNames();
File System
Each thread has an isolated file system for storing files and data.
Writing Files
// Write a text file
const record = await state.writeFile(
'/data/config.json',
JSON.stringify({ key: 'value' }),
'application/json'
);
// Write a binary file with metadata
const imageRecord = await state.writeFile(
'/images/photo.jpg',
imageBuffer,
'image/jpeg',
{ width: 1920, height: 1080 }
);
Reading Files
// Read file content
const data = await state.readFile('/data/config.json');
if (data) {
const text = new TextDecoder().decode(data);
const config = JSON.parse(text);
}
// Get file metadata
const info = await state.statFile('/images/photo.jpg');
Streaming Large Files
const stream = await state.readFileStream('/uploads/video.mp4');
if (stream) {
for await (const chunk of stream) {
console.log(`Progress: ${chunk.index + 1}/${chunk.totalChunks}`);
// Process chunk.data (Uint8Array)
}
}
Directory Operations
// Create a directory
await state.mkdirFile('/documents');
// List directory contents
const { entries } = await state.readdirFile('/documents');
// Remove a directory (must be empty)
await state.rmdirFile('/documents');
// Delete a file
await state.unlinkFile('/data/config.json');
Search Operations
// Search file contents
const grepResults = await state.grepFiles('error');
// Find files by glob pattern
const { paths } = await state.findFiles('**/*.json');
File Statistics
const stats = await state.getFileStats();
console.log(`Files: ${stats.fileCount}`);
console.log(`Directories: ${stats.directoryCount}`);
console.log(`Total size: ${stats.totalSize} bytes`);
Execution State
The execution property is:
- Present during active execution (tools, hooks)
- Null when accessing thread at rest (endpoints)
if (state.execution) {
console.log(`Flow ID: ${state.execution.flowId}`);
console.log(`Step count: ${state.execution.stepCount}`);
console.log(`Current side: ${state.execution.currentSide}`);
}
ExecutionState Interface
interface ExecutionState {
readonly flowId: string; // Unique execution ID
readonly currentSide: 'a' | 'b'; // Active side (dual_ai)
readonly stepCount: number; // Total LLM request/response cycles
readonly sideAStepCount: number; // Side A steps
readonly sideBStepCount: number; // Side B steps
readonly stopped: boolean; // Execution stopped
readonly stoppedBy?: 'a' | 'b'; // Who stopped
readonly messageHistory: Message[];// Current history
readonly abortSignal: AbortSignal; // Cancellation signal
forceTurn(side: 'a' | 'b'): void; // Force next turn (dual_ai)
stop(): void; // Stop execution
}
Controlling Execution
// Force next turn to a specific side (dual_ai only)
state.execution?.forceTurn('b');
// Stop execution after current operation
state.execution?.stop();
// Use abort signal for cancellation
fetch(url, { signal: state.execution?.abortSignal });
Runtime Context
The _notPackableRuntimeContext property provides access to platform-specific context like Cloudflare environment bindings and the underlying thread instance.
Shape
In AgentBuilder, the shape is:
{
threadInstance: ThreadInstance // The underlying DurableObject instance (always present)
env?: Env // Cloudflare Workers environment bindings (only during execution)
}
The threadInstance provides direct access to DurableObject RPC methods for advanced use cases.
The env object (when present) contains all bindings configured in wrangler.jsonc:
AGENT_BUILDER_THREAD - Thread Durable Object namespace
AGENT_BUILDER - Singleton metadata namespace
- API keys:
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.
- Custom bindings:
MY_KV, MY_D1, MY_R2, etc.
Availability
| Context | threadInstance | env |
|---|
| Tools (during execution) | Present | Present |
| Hooks (during execution) | Present | Present |
| Effects (during execution) | Present | Present |
| Endpoints (thread at rest) | Present | undefined |
The threadInstance is always available. The env bindings are only available during active execution. In thread endpoints where execution is null, env is undefined but threadInstance is still accessible.
Example Usage
export default defineTool({
description: 'Query internal database',
args: z.object({ userId: z.string() }),
execute: async (state, args) => {
const env = state._notPackableRuntimeContext?.env as Env | undefined;
if (!env?.MY_DATABASE) {
return { status: 'error', error: 'Database not available' };
}
const result = await env.MY_DATABASE
.prepare('SELECT * FROM users WHERE id = ?')
.bind(args.userId)
.first();
return { status: 'success', result: JSON.stringify(result) };
},
});
Tools that access _notPackableRuntimeContext cannot be packed, shared, or published as they depend on runtime-specific context.
Access Contexts
ThreadState is provided in different contexts with different capabilities:
| Context | Execution State | Use Case |
|---|
| Tools | Present | During agent execution |
| Hooks | Present | Lifecycle interception |
| Endpoints | Null | HTTP API access |
| Effects | Present | Scheduled operations |
Common Types
Message
interface Message {
id: string;
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
name?: string | null;
tool_calls?: string | null;
tool_call_id?: string | null;
created_at: number;
parent_id?: string | null;
depth?: number;
silent?: boolean;
metadata?: Record<string, unknown>;
status?: 'pending' | 'completed' | 'failed' | null;
tool_status?: 'success' | 'error' | null;
}
FileRecord
interface FileRecord {
path: string;
name: string;
mimeType: string;
storage: string;
size: number;
isDirectory: boolean;
metadata?: Record<string, unknown>;
width?: number;
height?: number;
createdAt?: number;
updatedAt?: number;
}
Next Steps