Threads
[TK]
1. Thread Definition
A thread is an instance of an agent—the runtime execution context where conversations happen. Each thread has isolated storage for messages and files, and maintains its own conversation history.
1.1 Agent Assignment
The agent assigned to a thread can change over time while preserving all history and context. This is called a handoff.
| Scenario | Behavior |
|---|---|
| Tool handoff | Calling an ai_human agent as a tool transfers control to that agent |
| Manual handoff | Queue a tool call to an agent to trigger handoff programmatically |
| dual_ai execution | Creates its own isolated thread instance (no handoff) |
Handoffs enable workflows where specialized agents handle different parts of a conversation—a triage agent routes to support, support escalates to billing—all within the same thread with full context preserved.
1.2 ThreadState Interface
ThreadState is the unified interface for all thread operations. It provides a consistent API for interacting with threads regardless of context—whether from tools during execution, hooks, or HTTP endpoints.
1.2.1 Properties
| Property | Type | Description |
|---|---|---|
threadId | string | Unique thread identifier (readonly) |
agentId | string | Owning agent name (readonly) |
userId | string | null | Associated user (readonly) |
createdAt | number | Creation timestamp in microseconds (readonly) |
context | Record<string, unknown> | Per-execution key-value storage. See Context Storage |
execution | ExecutionState | null | Execution state during active execution, null at rest. See Execution State |
_notPackableRuntimeContext | Record<string, unknown> | undefined | Runtime-specific context (non-portable). See Runtime Context |
1.2.2 Methods
Messages — See Messages for details
| Method | Returns | Description |
|---|---|---|
getMessages(options?) | Promise<MessagesResult> | Get messages with pagination |
getMessage(messageId) | Promise<Message | null> | Get single message by ID |
injectMessage(input) | Promise<Message> | Inject a new message |
updateMessage(messageId, updates) | Promise<Message> | Update existing message |
Logs
| Method | Returns | Description |
|---|---|---|
getLogs(options?) | Promise<Log[]> | Get execution logs |
Resource Loading — See Resource Loading for details
| Method | Returns | Description |
|---|---|---|
loadModel(name) | Promise<T> | Load model definition |
loadPrompt(name) | Promise<T> | Load prompt definition |
loadAgent(name) | Promise<T> | Load agent definition |
getPromptNames() | string[] | List available prompts |
getAgentNames() | string[] | List available agents |
getModelNames() | string[] | List available models |
Tool Invocation — See Tool Invocation for details
| Method | Returns | Description |
|---|---|---|
queueTool(toolName, args) | void | Queue tool for async execution |
invokeTool(toolName, args) | Promise<ToolResult> | Invoke tool and wait for result |
Effect Scheduling — See Effect Scheduling for details
| Method | Returns | Description |
|---|---|---|
scheduleEffect(name, args, delay?) | Promise<string> | Schedule effect, returns effect ID |
getScheduledEffects(name?) | Promise<ScheduledEffect[]> | Get pending effects |
removeScheduledEffect(id) | Promise<boolean> | Cancel scheduled effect |
Events — See Events for details
| Method | Returns | Description |
|---|---|---|
emit(event, data) | void | Emit event to connected clients |
File System — See File System for details
| Method | Returns | Description |
|---|---|---|
writeFile(path, data, mimeType, options?) | Promise<FileRecord> | Write file |
readFile(path) | Promise<ArrayBuffer | null> | Read file content |
readFileStream(path, options?) | Promise<AsyncIterable<FileChunk> | null> | Stream large files |
statFile(path) | Promise<FileRecord | null> | Get file metadata |
readdirFile(path) | Promise<ReaddirResult> | List directory |
unlinkFile(path) | Promise<void> | Delete file |
mkdirFile(path) | Promise<FileRecord> | Create directory |
rmdirFile(path) | Promise<void> | Remove directory |
getFileStats() | Promise<FileStats> | Get filesystem stats |
grepFiles(pattern) | Promise<GrepResult[]> | Search file contents |
findFiles(pattern) | Promise<FindResult> | Find files by glob |
getFileThumbnail(path) | Promise<ArrayBuffer | null> | Get image thumbnail |
1.3 Access Contexts
ThreadState is provided in different contexts:
| Context | Execution State | Use Case |
|---|---|---|
| Tools | Present | During agent execution |
| Hooks | Present | Lifecycle interception |
| Endpoints | Null | HTTP API access |
2. Messages
2.1 Message Structure
| Property | Type | Description |
|---|---|---|
id | string | Unique message ID |
role | 'system' | 'user' | 'assistant' | 'tool' | Message role |
content | string | null | Message content |
name | string | null | Tool name (for tool results) |
tool_calls | string | null | JSON-encoded tool calls |
tool_call_id | string | null | Tool call ID (for results) |
created_at | number | Creation timestamp |
parent_id | string | null | Parent message (sub-prompts) |
depth | number | Nesting depth (0 = top-level) |
silent | boolean | Hidden from UI |
metadata | Record<string, unknown> | Custom metadata |
2.2 Reading Messages
// Get recent messages
const { messages, total, hasMore } = await state.getMessages({
limit: 50,
order: 'desc',
});
// Get a single message
const message = await state.getMessage('msg-123');
2.3 Query Options
| Option | Type | Default | Description |
|---|---|---|---|
limit | number | - | Max 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 |
2.4 Injecting Messages
Messages can be injected into the conversation:
const message = await state.injectMessage({
role: 'user',
content: 'Additional context...',
silent: true, // Hide from UI
metadata: { source: 'tool' },
});
2.5 Updating Messages
Existing messages can be updated:
const updated = await state.updateMessage('msg-123', {
content: 'Updated content',
metadata: { edited: true },
});
3. Resource Loading
3.1 Loading Definitions
ThreadState provides access to registered definitions:
// 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');
3.2 Listing Resources
// Get available resource names
const prompts = state.getPromptNames();
const agents = state.getAgentNames();
const models = state.getModelNames();
4. Tool Invocation
4.1 Queuing Tools
Queue a tool for asynchronous execution:
// Queue a tool (non-blocking)
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.
4.2 Direct Invocation
Invoke a tool and wait for the result:
// Invoke directly (blocking)
const result = await state.invokeTool('get_weather', {
location: 'San Francisco',
});
if (result.status === 'success') {
console.log(result.result);
}
5. Effect Scheduling
Effects are scheduled operations that run outside the normal tool execution context. Unlike tools which execute immediately during conversation, effects:
- Run independently of the conversation flow
- Can be scheduled with delays
- Execute with their own ThreadState context
- Are ideal for side effects that don’t need immediate results
5.1 Scheduling Effects
// Schedule an effect 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
);
| Parameter | Type | Description |
|---|---|---|
name | string | Effect name (file in agents/effects/) |
args | Record<string, unknown> | Arguments passed to effect handler |
delay | number | Delay in milliseconds (default: 0 for immediate) |
Returns a unique effect ID (UUID) that can be used to cancel the effect.
5.2 ScheduledEffect Record
| Property | Type | Description |
|---|---|---|
id | string | Unique effect ID (UUID) |
name | string | Effect name |
args | Record<string, unknown> | Arguments to be passed |
scheduledAt | number | Scheduled execution time (microseconds) |
createdAt | number | When effect was scheduled (microseconds) |
5.3 Managing Scheduled 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);
if (removed) {
console.log('Effect cancelled');
}
6. Events
6.1 Emitting Events
Send custom events to connected clients:
state.emit('progress', {
step: 3,
total: 10,
message: 'Processing data...',
});
Events are delivered via WebSocket to any connected clients.
6.2 Event Guidelines
- Event names SHOULD be lowercase with underscores
- Event data MUST be JSON-serializable
- Events SHOULD NOT contain sensitive information
7. Context Storage
7.1 Per-Execution Context
The context property provides temporary storage:
// Store data during execution
state.context.userData = { id: 123, name: 'Alice' };
// Retrieve in another tool call
const user = state.context.userData;
7.2 Context Limitations
| Aspect | Behavior |
|---|---|
| Scope | Single execution only |
| Persistence | Lost after execution ends |
| Type | Record<string, unknown> |
| Size | No enforced limit |
For persistent storage, use the file system or external storage solutions.
8. File System
Each thread has access to an isolated file system for storing files, images, and other binary data.
8.1 File Record
Files are represented by the FileRecord type:
| Property | Type | Description |
|---|---|---|
path | string | File path (relative to thread root) |
name | string | File name |
mimeType | string | MIME type |
storage | string | Implementation-defined storage identifier |
size | number | File size in bytes |
isDirectory | boolean | Whether this is a directory |
metadata | Record<string, unknown> | Custom metadata |
width | number | Image width in pixels (if applicable) |
height | number | Image height in pixels (if applicable) |
8.2 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 }
);
8.3 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 without reading content
const info = await state.statFile('/images/photo.jpg');
if (info) {
console.log(`Size: ${info.size} bytes`);
}
8.4 Directory Operations
// Create a directory
await state.mkdirFile('/documents');
// List directory contents
const { entries } = await state.readdirFile('/documents');
for (const entry of entries) {
console.log(`${entry.isDirectory ? 'DIR' : 'FILE'}: ${entry.name}`);
}
// Remove an empty directory
await state.rmdirFile('/documents');
// Delete a file
await state.unlinkFile('/data/config.json');
8.5 Search Operations
// Search file contents
const grepResults = await state.grepFiles('error');
for (const result of grepResults) {
console.log(`${result.path}:`);
for (const match of result.matches) {
console.log(` Line ${match.line}: ${match.content}`);
}
}
// Find files by glob pattern
const { paths } = await state.findFiles('**/*.json');
console.log('JSON files:', paths);
8.6 File Statistics
const stats = await state.getFileStats();
console.log(`Files: ${stats.fileCount}`);
console.log(`Directories: ${stats.directoryCount}`);
console.log(`Total size: ${stats.totalSize} bytes`);
8.7 Image Thumbnails
// Get thumbnail for an image
const thumbnail = await state.getFileThumbnail('/images/photo.jpg');
if (thumbnail) {
// Use thumbnail data
}
8.8 Streaming Files
For large files, readFileStream provides memory-efficient access by yielding data in chunks.
Method
readFileStream(
path: string,
options?: ReadFileStreamOptions
): Promise<AsyncIterable<FileChunk> | null>
Options
| Property | Type | Description |
|---|---|---|
signal | AbortSignal | Optional abort signal for cancellation |
FileChunk
| Property | Type | Description |
|---|---|---|
data | Uint8Array | Raw binary data for this chunk |
index | number | 0-based index of this chunk |
totalChunks | number | Total number of chunks |
isLast | boolean | Whether this is the final chunk |
Behavior
-
File Not Found: Returns
nullif the file does not exist. -
External Files: Returns
nullfor files stored externally (S3, R2, URL). External files must be fetched using their storage location. -
Small Files: Files under the chunk threshold are yielded as a single chunk with
index: 0,totalChunks: 1, andisLast: true. -
Chunked Files: Large files yield multiple chunks in sequential order (0, 1, 2…).
-
Ordering Guarantee: Chunks are always yielded in sequential order. Implementations MUST NOT yield chunks out of order.
-
Abort Signal: When triggered, the stream stops yielding new chunks. Already-yielded chunks remain valid.
Example: Processing Large File
const stream = await state.readFileStream('/uploads/video.mp4');
if (!stream) {
throw new Error('File not found');
}
for await (const chunk of stream) {
console.log(`Progress: ${chunk.index + 1}/${chunk.totalChunks}`);
// Process chunk.data without loading entire file into memory
}
Example: HTTP Streaming Response
const stream = await state.readFileStream('/uploads/large-file.bin');
if (!stream) {
return new Response('Not Found', { status: 404 });
}
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
controller.enqueue(chunk.data);
}
controller.close();
}
});
return new Response(readable, {
headers: { 'Content-Type': 'application/octet-stream' }
});
9. Execution State
9.1 Availability
The execution property is:
- Present during active execution (tools, hooks)
- Null when accessing at rest (endpoints)
if (state.execution) {
console.log(`Step ${state.execution.stepCount}`);
}
9.2 Execution Properties
| Property | Type | Description |
|---|---|---|
flowId | string | Unique execution ID |
currentSide | 'a' | 'b' | Active side (dual_ai) |
stepCount | number | Total LLM request/response cycles |
sideAStepCount | number | Side A steps |
sideBStepCount | number | Side B steps |
stopped | boolean | Execution stopped |
stoppedBy | 'a' | 'b' | undefined | Who stopped |
messageHistory | Message[] | Current history |
abortSignal | AbortSignal | Cancellation signal |
9.3 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 });
10. Runtime Context (Non-Portable)
The _notPackableRuntimeContext property allows runtime implementations to inject platform-specific context.
10.1 Property Definition
| Property | Type | Description |
|---|---|---|
_notPackableRuntimeContext | Record<string, unknown> | undefined | Runtime-specific context injected by the implementation |
10.2 Portability
Tools that access _notPackableRuntimeContext cannot be packed, shared, or published to tool registries.
10.3 Example: Accessing Cloudflare Bindings
import { defineTool } from '@standardagents/builder';
import { z } from 'zod';
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) };
},
});
Tip: Common implementations include
{ env: Env }for Cloudflare Workers bindings (KV, D1, R2), or{ process: NodeJS.Process }for Node.js environments.
11. Thread Endpoints
Thread endpoints expose HTTP APIs for thread-specific operations. They automatically look up threads by ID and provide a ThreadState instance.
11.1 Defining Thread Endpoints
import { defineThreadEndpoint } from '@standardagents/spec';
export default defineThreadEndpoint(async (req, state) => {
const { messages, total } = await state.getMessages({ limit: 10 });
return Response.json({
threadId: state.threadId,
messageCount: total,
});
});
11.2 Handler Signature
Thread endpoint handlers receive:
| Parameter | Type | Description |
|---|---|---|
req | Request | The incoming HTTP request |
state | ThreadState | Thread state for the requested thread |
The handler MUST return a Response object.
11.3 Endpoint Context
When accessing a thread via an endpoint, state.execution is always null because the thread is not actively executing:
export default defineThreadEndpoint(async (req, state) => {
// Execution is always null in endpoints
if (state.execution === null) {
// Thread is at rest, not executing
}
return Response.json({ threadId: state.threadId });
});
11.4 Error Handling
Thread endpoints automatically handle common errors:
| Condition | Response |
|---|---|
| Thread not found | 404: { error: "Thread not found: {threadId}" } |
| Missing thread ID | 400: { error: "Thread ID required" } |
11.5 Thread Endpoint Examples
Get Thread Messages:
import { defineThreadEndpoint } from '@standardagents/spec';
export default defineThreadEndpoint(async (req, state) => {
const url = new URL(req.url);
const limit = parseInt(url.searchParams.get('limit') || '50');
const offset = parseInt(url.searchParams.get('offset') || '0');
const result = await state.getMessages({ limit, offset, order: 'desc' });
return Response.json(result);
});
Export Thread Data:
import { defineThreadEndpoint } from '@standardagents/spec';
export default defineThreadEndpoint(async (req, state) => {
const { messages } = await state.getMessages({ order: 'asc' });
return Response.json({
exportedAt: new Date().toISOString(),
thread: {
id: state.threadId,
agent: state.agentId,
user: state.userId,
createdAt: state.createdAt,
},
messages,
});
});
List Thread Files:
import { defineThreadEndpoint } from '@standardagents/spec';
export default defineThreadEndpoint(async (req, state) => {
const { entries } = await state.readdirFile('/');
return Response.json({ files: entries });
});
12. Conformance Requirements
12.1 Thread Lifecycle
- Creation: Thread created with metadata
- Execution: Agent processes messages
- At Rest: Thread accessible via endpoints
- Resumption: New execution on next message
12.2 Platform Independence
ThreadState is an abstract interface. Implementations MUST NOT expose:
- Storage implementation details
- Platform-specific bindings
- Internal state management
12.3 Error Handling
Methods that return promises SHOULD throw on:
- Resource not found (loadModel, loadPrompt, loadAgent)
- Invalid parameters
- Storage errors
Methods SHOULD NOT throw on:
- Empty results (return empty arrays/null instead)
- Missing optional data
13. TypeScript Reference
/**
* Thread state interface - the unified API for thread interactions.
* Available to tools, hooks, and endpoints.
*/
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>;
// ─────────────────────────────────────────────────────────────────────────
// Logs
// ─────────────────────────────────────────────────────────────────────────
getLogs(options?: GetLogsOptions): Promise<Log[]>;
// ─────────────────────────────────────────────────────────────────────────
// 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>;
// ─────────────────────────────────────────────────────────────────────────
// 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
// ─────────────────────────────────────────────────────────────────────────
execution: ExecutionState | null;
// ─────────────────────────────────────────────────────────────────────────
// Runtime Context (Non-Portable)
// ─────────────────────────────────────────────────────────────────────────
readonly _notPackableRuntimeContext?: Record<string, unknown>;
}
/**
* Execution state available during active execution.
*/
interface ExecutionState {
readonly flowId: string;
readonly currentSide: 'a' | 'b';
readonly stepCount: number;
readonly sideAStepCount: number;
readonly sideBStepCount: number;
readonly stopped: boolean;
readonly stoppedBy?: 'a' | 'b';
readonly messageHistory: Message[];
readonly abortSignal: AbortSignal;
forceTurn(side: 'a' | 'b'): void;
stop(): void;
}
/**
* A scheduled effect pending execution.
*/
interface ScheduledEffect {
id: string;
name: string;
args: Record<string, unknown>;
scheduledAt: number; // microseconds
createdAt: number; // microseconds
}