Copy page
View as Markdown View this page as plain text

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:

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.

ScenarioBehavior
Tool handoffCalling an ai_human agent as a tool transfers control to that agent
Manual handoffQueue a tool call to an agent to trigger handoff programmatically
dual_ai executionCreates 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

PropertyTypeDescription
threadIdstringUnique thread identifier (readonly)
agentIdstringOwning agent name (readonly)
userIdstring | nullAssociated user (readonly)
createdAtnumberCreation timestamp in microseconds (readonly)
childrenSubagentRegistryEntry[]Resumable child subagent registry (readonly)
terminatednumber | nullTermination timestamp in microseconds, or null when active (readonly)
argumentsRecord<string, unknown>Arguments supplied when this thread was created or invoked. Resumable subagent initial arguments appear here. See Thread Arguments
contextRecord<string, unknown>Mutable per-execution scratch storage. See Context Storage
executionExecutionState | nullExecution state during active execution, null at rest. See Execution State
_notPackableRuntimeContextRecord<string, unknown> | undefinedRuntime-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.

MessagesMessages →

MethodReturnsDescription
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 SystemFile System →

MethodReturnsDescription
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 ExecutionCode Execution →

MethodReturnsDescription
runCode(source, options?)CodeExecutionExecute JS/TS source in an isolated sandbox; returns a thenable handle with terminate()

Subagent Hierarchy — see §9 Subagent Hierarchy

MethodReturnsDescription
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 when present
stopSession(options?)Promise<void>Terminally stop the active agent session; optional notifyParent and status trigger those parent side effects

Resource Loading — see §2 Resource Loading

MethodReturnsDescription
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

MethodReturnsDescription
queueTool(toolName, args)voidQueue tool for async execution
invokeTool(toolName, args)Promise<ToolResult>Invoke tool and wait for result

Effect Scheduling — see §4 Effect Scheduling

MethodReturnsDescription
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

MethodReturnsDescription
emit(event, data)voidEmit event to connected clients

Durable Key-Value Store — see §7 Durable Key-Value Store

MethodReturnsDescription
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

User identity

MethodReturnsDescription
user()Promise<ThreadUser | null>Profile of the thread’s user (identity fields only — id, username, role, permissions, email, display_name, avatar_url; never credentials). null for user-less threads.

Skills — see §7.4 Agent Skills

MethodReturnsDescription
skills.list(options?)Promise<SkillMetadata[]>Installed skills (enabled only unless includeDisabled)
skills.get(name)Promise<{ metadata, skillMd } | null>A skill’s metadata + SKILL.md source
skills.readFile(name, path)Promise<string | null>One bundled file’s text
skills.install(files)Promise<SkillMetadata>Install/replace a skill from its file set
skills.setEnabled(name, enabled)Promise<boolean>Enable/disable an installed skill
skills.remove(name)Promise<boolean>Uninstall a skill

Lifecycle — see §8 Lifecycle

MethodReturnsDescription
terminate()Promise<void>Soft-terminate the thread

1.3 Access Contexts

ThreadState is provided in different contexts:

ContextExecution StateUse Case
ToolsPresentDuring agent execution
HooksPresentLifecycle interception
EndpointsNullHTTP 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:

  1. Thread environment variables
  2. User-account environment variables
  3. Runtime instance environment variables
  4. Agent definition defaults (agent.env)
  5. 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.9.

3. Tool Invocation

3.1 Queuing Tools

Queue a tool for asynchronous execution:

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.

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: 'user@example.com', subject: 'Reminder' },
  30 * 60 * 1000  // delay in milliseconds
);
ParameterTypeDescription
namestringEffect name (file in agents/effects/)
argsRecord<string, unknown>Arguments passed to effect handler
delaynumberDelay in milliseconds (default: 0 for immediate)

Returns a unique effect ID (UUID) that can be used to cancel the effect.

4.2 ScheduledEffect Record

PropertyTypeDescription
idstringUnique effect ID (UUID)
namestringEffect name
argsRecord<string, unknown>Arguments to be passed
scheduledAtnumberScheduled execution time (microseconds)
createdAtnumberWhen 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. Thread Arguments and Context Storage

6.1 Thread Arguments

arguments is the read-only argument object supplied when the thread was created or invoked.

For resumable subagent threads, runtimes MUST restore the creation-time subagent_create.arguments object into ThreadState.arguments on every run and when exposing the child through endpoints, hooks, effects, or other ThreadState access. For dual_ai child threads, both sides see the same argument object:

const initialArgs = state.arguments;
const shirtSize = state.arguments.shirt_size;

Runtimes should keep the durable copy with child thread metadata/properties. They should also expose top-level argument keys to prompt variable interpolation, for whichever child side is currently rendering, including included prompts, without requiring callers to put those values in state.context.

6.2 Per-Execution Context

The context property is mutable execution-scoped scratch storage for values that should be shared while the current agent run is being assembled and executed. It is used for:

  • sharing derived values between tools, hooks, effects, and runtime lifecycle code during one execution
  • runtime-internal coordination data that should not become a thread argument

For example:

state.context.userData = { id: 123, name: 'Alice' };

// Retrieve in another tool call
const user = state.context.userData;

Runtimes may seed context at execution start with runtime coordination data. User code may mutate it during execution, and later participants in that same execution see those mutations.

Invocation or creation arguments belong in arguments, not in context.

6.3 Context Limitations

AspectBehavior
ScopeSingle execution only
PersistenceMutations are lost after execution ends
TypeRecord<string, unknown>
SizeNo 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

AspectBehavior
ScopePer-thread; not shared with parent, child, or sibling threads
PersistenceDurable across executions for the lifetime of the thread
Value typeAny JSON-serializable value (objects, arrays, strings, numbers, booleans, null)
Missing keysgetValue returns null
DeletionsetValue(key, null) or setValue(key, undefined) deletes the key
SubagentsValues are not automatically copied to spawned child threads

7.3 Comparison With Other Storage

SurfaceScopePersistenceIntended Use
argumentsThread invocationDurable when backed by thread metadata/propertiesCreation/invocation arguments exposed to thread code and prompt interpolation
contextSingle executionEphemeralMutable scratch state shared during one execution
getValue / setValueThreadDurableSmall structured state scoped to the thread
env / envType / setEnvThread → user → instance → agent → promptDurable (encrypted at rest when supported)Configuration and secrets with layered resolution and display/redaction metadata
File systemThreadDurableLarge or binary payloads

Runtime semantics and conformance requirements for the durable key-value store live in Runtime §15.

7.4 Agent Skills

state.skills exposes the instance-global Agent Skills library — the one deliberate exception to per-thread scoping in this section. Skills follow the Agent Skills standard: a folder whose root SKILL.md opens with YAML frontmatter carrying a required name (lowercase kebab-case, ≤64 chars) and description (≤1024 chars), optionally accompanied by scripts, references, and assets.

Requirements:

  • Skills are stored server-side and installed once for the whole instance; every thread sees the same library. Runtimes MUST NOT require client-side storage for skill content.
  • A skill’s identity MUST come from its SKILL.md frontmatter — never from a caller-supplied name — and installs MUST validate the frontmatter against the standard.
  • Runtimes SHOULD surface skills to agents with progressive disclosure: only name + description ride in ambient context; the SKILL.md body (and bundled files) load on demand when the agent judges the skill applicable.
  • Disabled skills stay installed but MUST be hidden from ambient agent context.
const available = await state.skills.list();
const skill = await state.skills.get('pdf-processing');
const script = await state.skills.readFile('pdf-processing', 'scripts/extract.py');
await state.skills.install([{ path: 'SKILL.md', content: '---\nname: commit-style\ndescription: ...\n---\n...' }]);

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;
  initialArguments?: Record<string, unknown>;
  spawnGroupId?: string;
  createdAt?: number;
  status: string;
  parentCommunication?: 'implicit' | 'explicit';
}

initialArguments stores the structured values supplied at child creation. The registry copy is parent-visible metadata; the durable child-side source should live with the child thread’s own metadata/properties and be exposed to both child sides as ThreadState.arguments and prompt interpolation variables.

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');
await state.stopSession({
  result: 'waiting for input',
  notifyParent: 'Please answer the clarification question',
  status: 'waiting for clarification',
});

stopSession() is distinct from state.execution.stop(): it marks the active agent session as terminal for lifecycle/subagent purposes. By default it only stops the session; notifyParent and status are optional side effects.

If a child has no parent thread, notifyParent() does not queue anything externally; setStatus() similarly becomes a no-op when there is no parent registry entry. Clients can still render the originating tool call or log entry as outbound parent communication.

10. Runtime Context (Non-Portable)

The _notPackableRuntimeContext property allows runtime implementations to inject platform-specific context.

PropertyTypeDescription
_notPackableRuntimeContextRecord<string, unknown> | undefinedRuntime-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 _notPackableRuntimeContext is runtime-defined. Typical implementations expose either a handle to host bindings (databases, key-value stores, object storage) under env, 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

  1. Creation: Thread created with metadata
  2. Execution: Agent processes messages
  3. At Rest: Thread accessible via endpoints
  4. 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;
  readonly arguments: Record<string, unknown>;

  // ─────────────────────────────────────────────────────────────────────────
  // 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>;
  stopSession(options?: {
    result?: string;
    notifyParent?: string;
    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
}