Threads
A thread is an instance of an agent — the runtime execution context where conversations happen. Each thread has isolated storage for messages and files, maintains its own conversation history, and can be handed off between agents while preserving full context.
This page covers the core ideas: what a thread is, its identity, the ThreadState interface, and the small primitives that live directly on it (resource loading, tool invocation, effects, events, context, runtime context, subagent hierarchy, lifecycle). Larger surfaces have their own pages:
- Messages — reading, injecting, queuing, updating, deleting
- File System — per-thread file storage, streaming, search
- Code Execution — isolated JS/TS sandbox via
runCode - Execution State — step counts, turn control, cancellation
- Thread Endpoints — HTTP APIs that receive a
ThreadState
1. Thread Definition
A thread is an instance of an agent with isolated storage for messages and files.
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) |
children | SubagentRegistryEntry[] | Resumable child subagent registry (readonly) |
terminated | number | null | Termination timestamp in microseconds, or null when active (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
The methods directory below is grouped by topic. Topics with their own page link out; the rest are documented on this page.
Messages — Messages →
| 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 |
queueMessage(input) | Promise<void> | Queue a message for delivery on next step |
updateMessage(messageId, updates) | Promise<Message> | Update existing message |
deleteMessage(messageId) | Promise<boolean> | Delete a message |
File System — File System →
| 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 |
Code Execution — Code Execution →
| Method | Returns | Description |
|---|---|---|
runCode(source, options?) | CodeExecution | Execute JS/TS source in an isolated sandbox; returns a thenable handle with terminate() |
Subagent Hierarchy — see §9 Subagent Hierarchy
| Method | Returns | Description |
|---|---|---|
getChildThread(referenceId) | Promise<ThreadState | null> | Resolve child thread by subagent reference |
getParentThread() | Promise<ThreadState | null> | Resolve parent thread for this thread |
notifyParent(content) | Promise<void> | Queue a silent message to this thread’s parent |
setStatus(status) | Promise<void> | Update this thread’s status in the parent registry |
Resource Loading — see §2 Resource Loading
| 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 |
env(propertyName) | Promise<string> | Resolve environment variable using thread/user/instance/agent/prompt precedence |
envType(propertyName) | Promise<'text' | 'secret'> | Resolve display/redaction classification for an environment value |
setEnv(propertyName, value, options?) | Promise<void> | Set a thread environment variable, optionally choosing text or secret, and propagate to active descendants |
Tool Invocation — see §3 Tool Invocation
| 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 §4 Effect Scheduling
| 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 §5 Events
| Method | Returns | Description |
|---|---|---|
emit(event, data) | void | Emit event to connected clients |
Durable Key-Value Store — see §7 Durable Key-Value Store
| Method | Returns | Description |
|---|---|---|
getValue(key) | Promise<T | null> | Read a value from the thread’s durable key-value store |
setValue(key, value) | Promise<void> | Write a JSON-serializable value; null/undefined deletes the key |
Lifecycle — see §8 Lifecycle
| Method | Returns | Description |
|---|---|---|
terminate() | Promise<void> | Soft-terminate the thread |
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. Resource Loading
2.1 Loading Definitions
ThreadState provides access to registered definitions:
const model = await state.loadModel('gpt-4o');
const prompt = await state.loadPrompt('support-prompt');
const agent = await state.loadAgent('customer-support');
2.2 Listing Resources
const prompts = state.getPromptNames();
const agents = state.getAgentNames();
const models = state.getModelNames();
2.3 Environment Variable Resolution
const apiKey = await state.env('GOOGLE_API_KEY');
await state.setEnv('TOPDOWN_APPROVAL_GATE', 'open');
await state.setEnv('PUBLIC_LABEL', 'QA', { type: 'text' });
await state.setEnv('API_TOKEN', 'secret-token', { type: 'secret' });
env(propertyName) resolves in this order:
- Thread environment variables
- User-account environment variables
- Runtime instance environment variables
- Agent definition defaults (
agent.env) - Prompt definition defaults (
prompt.env)
envType(propertyName) returns the display/redaction classification for a resolved environment value. Unknown or non-thread sources default to secret. Values marked secret should be redacted from tool output and logs; values marked text may be displayed.
setEnv(propertyName, value, options?) writes to thread storage and propagates recursively to active descendants. Pass { type: 'text' } or { type: 'secret' } to choose the display/redaction classification. Omitting type preserves any existing classification for that key, or defaults a new key to secret. During propagation, descendants must filter out inherited values for variable names marked scoped in that descendant’s agent graph. Terminated descendants are skipped.
Thread, user, and instance environment values should be encrypted at rest when the runtime supports encrypted storage.
For resumable subagent creation, runtimes may require missing scoped variables to be provided through a pending request flow before boot can continue. See Agents §7.8.
3. Tool Invocation
3.1 Queuing Tools
Queue a tool for asynchronous execution:
state.queueTool('send_email', {
to: '[email protected]',
subject: 'Hello',
body: 'Message content',
});
If the thread is currently executing, the tool is added to the queue. Otherwise, a new execution is started.
3.2 Direct Invocation
Invoke a tool and wait for the result:
const result = await state.invokeTool('get_weather', {
location: 'San Francisco',
});
if (result.status === 'success') {
console.log(result.result);
}
4. 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
Effect definition syntax is specified in Effects. Packed effects may use qualified names ({packageId}/{effectName}) to avoid collisions.
4.1 Scheduling Effects
const effectId = await state.scheduleEffect(
'send_reminder_email',
{ to: '[email protected]', 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.
4.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) |
4.3 Managing Scheduled Effects
const effects = await state.getScheduledEffects();
const reminders = await state.getScheduledEffects('send_reminder_email');
const removed = await state.removeScheduledEffect(effectId);
if (removed) {
console.log('Effect cancelled');
}
5. Events
5.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.
5.2 Event Guidelines
- Event names SHOULD be lowercase with underscores
- Event data MUST be JSON-serializable
- Events SHOULD NOT contain sensitive information
6. Context Storage
6.1 Per-Execution Context
The context property provides temporary storage:
state.context.userData = { id: 123, name: 'Alice' };
// Retrieve in another tool call
const user = state.context.userData;
6.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 across executions, use the durable key-value store, the file system, or external storage.
7. Durable Key-Value Store
getValue and setValue provide a simple durable key-value store scoped to the thread. Values persist across executions, tool calls, hook invocations, and endpoint requests for the lifetime of the thread.
It is one of two ways to store persistent data on a thread, alongside the file system.
7.1 Reading and Writing Values
const count = (await state.getValue<number>('invocation_count')) ?? 0;
await state.setValue('invocation_count', count + 1);
await state.setValue('preferences', { theme: 'dark', locale: 'en-US' });
const prefs = await state.getValue<{ theme: string; locale: string }>('preferences');
7.2 Semantics
| Aspect | Behavior |
|---|---|
| Scope | Per-thread; not shared with parent, child, or sibling threads |
| Persistence | Durable across executions for the lifetime of the thread |
| Value type | Any JSON-serializable value (objects, arrays, strings, numbers, booleans, null) |
| Missing keys | getValue returns null |
| Deletion | setValue(key, null) or setValue(key, undefined) deletes the key |
| Subagents | Values are not automatically copied to spawned child threads |
7.3 Comparison With Other Storage
| Surface | Scope | Persistence | Intended Use |
|---|---|---|---|
context | Single execution | Ephemeral | Sharing state between tool calls in one execution |
getValue / setValue | Thread | Durable | Small structured state scoped to the thread |
env / envType / setEnv | Thread → user → instance → agent → prompt | Durable (encrypted at rest when supported) | Configuration and secrets with layered resolution and display/redaction metadata |
| File system | Thread | Durable | Large or binary payloads |
Runtime semantics and conformance requirements for the durable key-value store live in Runtime §15.
8. Lifecycle
terminate() is a soft shutdown:
- Marks the thread inactive and sets
terminated - Aborts in-flight execution
- Rejects future
queueMessage()and execution attempts - Parent registries should reflect
'terminated'status
await state.terminate();
9. Subagent Hierarchy, Communication, and Termination
Subagent-aware runtimes expose parent/child thread relationships through ThreadState:
const child = await state.getChildThread(referenceId);
const parent = await state.getParentThread();
children entries track resumable child references and status/projection metadata:
interface SubagentRegistryEntry {
reference: string;
name: string;
title?: string;
description: string;
resumable?: boolean;
blocking?: boolean;
threadName?: string;
spawnGroupId?: string;
createdAt?: number;
status: string;
parentCommunication?: 'implicit' | 'explicit';
}
See Agents §7 for the full lifecycle, communication modes, and cross-thread semantics.
9.1 Cross-Thread Attachment Copying
When communication crosses thread boundaries, filesystem references must be copied:
- parent → child for invocation/messaging attachments
- child → parent for completion/failure payload attachments
Returned attachment paths must be valid in the receiving thread filesystem.
9.2 Lifecycle Status Messages
Runtimes may persist synthesized status messages (for example, system messages tagged with metadata.status_kind) to make cross-thread lifecycle events observable in message feeds.
9.3 Parent Messaging
Children can push information up the hierarchy without waiting for completion:
await state.notifyParent('halfway through the batch');
await state.setStatus('running');
10. Runtime Context (Non-Portable)
The _notPackableRuntimeContext property allows runtime implementations to inject platform-specific context.
| Property | Type | Description |
|---|---|---|
_notPackableRuntimeContext | Record<string, unknown> | undefined | Runtime-specific context injected by the implementation |
10.1 Portability
Tools that access _notPackableRuntimeContext cannot be packed, shared, or published to tool registries.
10.2 Example: Accessing a Host-Provided Binding
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 HostEnv | undefined;
if (!env?.DATABASE) {
return { status: 'error', error: 'Database not available' };
}
const result = await env.DATABASE
.prepare('SELECT * FROM users WHERE id = ?')
.bind(args.userId)
.first();
return { status: 'success', result: JSON.stringify(result) };
},
});
Tip: The shape of
_notPackableRuntimeContextis runtime-defined. Typical implementations expose either a handle to host bindings (databases, key-value stores, object storage) underenv, or a process/environment handle for server-style runtimes. Consult your runtime’s documentation for the specific contract.
11. Conformance Requirements
11.1 Thread Lifecycle
- Creation: Thread created with metadata
- Execution: Agent processes messages
- At Rest: Thread accessible via endpoints
- Resumption: New execution on next message
11.2 Platform Independence
ThreadState is an abstract interface. Implementations MUST NOT expose:
- Storage implementation details
- Platform-specific bindings
- Internal state management
11.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
12. 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;
readonly children: SubagentRegistryEntry[];
readonly terminated: number | null;
// ─────────────────────────────────────────────────────────────────────────
// Messages — see /threads/messages
// ─────────────────────────────────────────────────────────────────────────
getMessages(options?: GetMessagesOptions): Promise<MessagesResult>;
getMessage(messageId: string): Promise<Message | null>;
injectMessage(input: InjectMessageInput): Promise<Message>;
queueMessage(input: QueueMessageInput): Promise<void>;
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>;
getChildThread(referenceId: string): Promise<ThreadState | null>;
getParentThread(): Promise<ThreadState | null>;
getPromptNames(): string[];
getAgentNames(): string[];
getModelNames(): string[];
env(propertyName: string): Promise<string>;
envType(propertyName: string): Promise<'text' | 'secret'>;
setEnv(
propertyName: string,
value: string,
options?: { type?: 'text' | 'secret' }
): Promise<void>;
notifyParent(content: string): Promise<void>;
setStatus(status: string): Promise<void>;
// ─────────────────────────────────────────────────────────────────────────
// 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>;
// ─────────────────────────────────────────────────────────────────────────
// Durable Key-Value Store
// ─────────────────────────────────────────────────────────────────────────
getValue<T = unknown>(key: string): Promise<T | null>;
setValue(key: string, value: unknown): Promise<void>;
// ─────────────────────────────────────────────────────────────────────────
// File System — see /threads/filesystem
// ─────────────────────────────────────────────────────────────────────────
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>;
// ─────────────────────────────────────────────────────────────────────────
// Code Execution — see /threads/code-execution
// ─────────────────────────────────────────────────────────────────────────
runCode(source: string, options?: CodeExecutionOptions): CodeExecution;
// ─────────────────────────────────────────────────────────────────────────
// Execution State — see /threads/execution-state
// ─────────────────────────────────────────────────────────────────────────
execution: ExecutionState | null;
terminate(): Promise<void>;
// ─────────────────────────────────────────────────────────────────────────
// Runtime Context (Non-Portable)
// ─────────────────────────────────────────────────────────────────────────
readonly _notPackableRuntimeContext?: Record<string, unknown>;
}
/**
* A scheduled effect pending execution.
*/
interface ScheduledEffect {
id: string;
name: string;
args: Record<string, unknown>;
scheduledAt: number; // microseconds
createdAt: number; // microseconds
}