On this page

The adf object is a global Proxy available in every code execution context. Any property access on adf returns an async function that sends an RPC call to the main thread, where the corresponding tool is executed and the result returned.

Calling Convention

Every adf.* call follows three rules:

  1. Single object argument — Pass one object with named parameters
  2. Always await — All calls are asynchronous and return Promises
  3. Tool names match exactly — Use the same names as the built-in tools
// Correct
const result = await adf.fs_read({ path: 'config.json' })
const text = result.content
await adf.fs_write({ path: 'output.txt', content: 'hello' })

// Wrong — multiple arguments
const data = await adf.fs_read('config.json')

// Wrong — not awaited (fires and forgets, errors silently lost)
adf.fs_write({ path: 'output.txt', content: 'hello' })

Tool results are automatically parsed from JSON. If the result is a JSON string, it’s parsed into an object. If parsing fails, the raw string is returned.

Filesystem (fs_*)

fs_read

Read a file from the virtual filesystem. Returns an object with the full file record.

ParameterTypeRequiredDescription
pathstringYesFile path
start_linenumberNoStart line (1-based) for text files
end_linenumberNoEnd line (inclusive) for text files

Return shape: { path, content, mime_type, size, protection, created_at, updated_at }

  • Text files: content is the raw text string
  • Binary files: content is a base64-encoded string
  • Media files (images, audio, video): content is base64-encoded. When the corresponding model.multimodal modality is enabled, the executor sends a native content block (image_url, input_audio, or video_url) to the LLM alongside the JSON row so the agent can perceive the media. Media blocks are ephemeral (not persisted to adf_loop). When disabled, or the file exceeds the size limit, the JSON row is returned with content: null. See Multimodal for details.
  • README.md / mind.md: synthesized record with protection: 'no_delete'

From code execution, fs_read always returns full content with no truncation. When called from the LLM, the executor applies context-window guards (token limit, large file preview).

const result = await adf.fs_read({ path: 'README.md' })
const text = result.content  // raw text
const lines = await adf.fs_read({ path: 'data.csv', start_line: 1, end_line: 100 })
const slicedText = lines.content
const img = await adf.fs_read({ path: 'image.png' })
const base64 = img.content  // base64-encoded binary

fs_write

Create, overwrite, edit, or append to a file. mode is required — one of "write", "edit", or "append".

Write mode (mode: "write") — provide content:

ParameterTypeRequiredDescription
modestringYes"write"
pathstringYesFile path
contentstring or BufferYesFile content (Buffer for binary, string for text)
protectionstringNo"read_only", "no_delete", or "none"
encodingstringNo"base64" when content is a base64-encoded string
mime_typestringNoMIME type for binary files

When content is a Buffer (e.g. from sys_fetch), the file is written as binary automatically — no encoding or mime_type parameters needed.

Edit mode (mode: "edit") — provide old_text + new_text, or a batch edits[]:

ParameterTypeRequiredDescription
modestringYes"edit"
pathstringYesFile path
old_textstringYes*Text to find (must match exactly once unless replace_all)
new_textstringYes*Replacement text
replace_allbooleanNoReplace every occurrence instead of requiring a unique match
editsarrayNo*Batch of { old_text, new_text, replace_all } applied in order and atomically — if any edit fails, the file is left unchanged

*Provide either a single old_text/new_text pair or a non-empty edits[] array.

Append mode (mode: "append") — provide content:

ParameterTypeRequiredDescription
modestringYes"append"
pathstringYesFile path
contentstringYesText appended to the end of the file
// Write a text file
await adf.fs_write({ mode: 'write', path: 'data/report.json', content: JSON.stringify(report, null, 2) })

// Write a binary file (Buffer from sys_fetch)
const resp = await adf.sys_fetch({ url: 'https://example.com/image.png' })
await adf.fs_write({ mode: 'write', path: 'image.png', content: resp.body })

// Append to a log file
await adf.fs_write({ mode: 'append', path: 'run.log', content: `\n${Date.now()} done` })

// Edit in-place (single)
await adf.fs_write({
  mode: 'edit',
  path: 'README.md',
  old_text: '## Status: Draft',
  new_text: '## Status: Published'
})

// Atomic batch edit
await adf.fs_write({
  mode: 'edit',
  path: 'config.md',
  edits: [
    { old_text: 'v1', new_text: 'v2' },
    { old_text: 'draft', new_text: 'final', replace_all: true }
  ]
})

fs_list

List files in the virtual filesystem.

ParameterTypeRequiredDescription
prefixstringNoPath prefix filter (e.g., "lib/")
const files = await adf.fs_list({})
const libFiles = await adf.fs_list({ prefix: 'lib/' })

fs_delete

Delete a file.

ParameterTypeRequiredDescription
pathstringYesFile path to delete
await adf.fs_delete({ path: 'temp/scratch.txt' })

Messaging (msg_*)

msg_send

Send a message to another agent. Two modes:

Direct send — provide recipient (DID) + address (delivery URL):

ParameterTypeRequiredDescription
recipientstringYes*Recipient DID (e.g., "did:adf:...") or adapter address (e.g., "telegram:123")
addressstringYes*Delivery URL (validated). Not needed for adapter recipients, or when replying via parent_id.
contentstringYesMessage content
content_typestringNoMIME type of content when not plain text — e.g. "application/vnd.adf.form+json" for interactive forms, rendered natively where the platform supports it (currently Telegram); other adapters send a plain-text questionnaire. Validated at send time for known types.
subjectstringNoOptional subject line
thread_idstringNoConversation thread id (inherited from parent_id if omitted)
parent_idstringNoParent message ID (reply); resolves recipient/address/thread from the parent
attachmentsstring[]NoFile paths to attach
metaobjectNoEnvelope metadata
message_metaobjectNoPayload metadata (trust/verification keys are stripped on send)

*Not required when parent_id is provided — the runtime resolves recipient and address from the referenced inbox message.

Reply via parent_id — provide parent_id + payload:

// Direct send
await adf.msg_send({
  recipient: 'did:adf:9gvayMZx5m...',
  address: 'http://127.0.0.1:7295/agents/monitor/inbox',
  payload: 'Health check passed'
})

// Reply to an inbox message (runtime resolves recipient + address)
await adf.msg_send({ parent_id: 'inbox-abc123', payload: 'Acknowledged' })

// Adapter send (no address needed)
await adf.msg_send({ recipient: 'telegram:123456', payload: 'Hello from ADF' })

msg_read

Read messages from the inbox.

ParameterTypeRequiredDescription
limitnumberNoMax messages to return
statusstringNoFilter: "unread", "read", "archived"
include_originalbooleanNoInclude the raw platform message (original_message) for channel-adapter messages — the full Telegram update, Slack event, parsed email, etc. Large; request only when the normalized fields aren’t enough. Default: false
const unread = await adf.msg_read({ status: 'unread', limit: 10 })

msg_update

Update message status.

ParameterTypeRequiredDescription
idsstring[]YesMessage IDs to update
statusstringYesNew status: "read" or "archived"
await adf.msg_update({ ids: ['msg_abc123'], status: 'archived' })

msg_list

Get inbox message counts by status.

ParameterTypeRequiredDescription
statusstringNoFilter by status
const counts = await adf.msg_list({})

agent_discover

Discover agents on the mesh.

ParameterTypeRequiredDescription
include_subdirectoriesbooleanNoInclude agents in subdirectories
const agents = await adf.agent_discover({})

chat_info

Read-only chat/channel metadata lookup through a connected channel adapter: title, description, participant roster (truncated), counts. This is a code-path capability — it ships enabled: true, visible: false, so it’s callable here without occupying a slot in the LLM tool schema (flip visible in the agent’s tool config to expose it as a visible tool).

ParameterTypeRequiredDescription
adapterstringYesAdapter type: "telegram", "discord", "slack", "whatsapp", …
chat_idstringYesPlatform chat id — source_context.chat_id (or channel_id) from an inbox message
limitnumberNoMax participants to return (1-100, default 50)
const info = await adf.chat_info({ adapter: 'slack', chat_id: 'C0123ABC' })
// { platform, chat_id, chat_type, title, description, participant_count,
//   participants: [{id, name?, role?}], participants_truncated, participants_scope, fetched_at }
// or { supported: false, reason } — adapter not connected/running, or no live
// query surface (email: recipients are in source_context.to/cc via msg_read)

Platform limits: Telegram can only enumerate admins (participants_scope: 'admins'); Discord reports only mentioned/cached users — a full roster would need the privileged GuildMembers gateway intent, which the adapter does not currently request; WhatsApp returns JIDs and roles but no names.

msg_delete

Delete messages from inbox or outbox.

ParameterTypeRequiredDescription
sourcestringYes"inbox" or "outbox"
filterobjectYesAt least one supported filter field required (empty or unsupported filters return an error)

Filter fields — inbox: status, from, source, before (epoch ms), thread_id; outbox: status, before, thread_id. from and source are inbox-only and rejected for outbox.

await adf.msg_delete({ source: 'inbox', filter: { status: 'archived' } })

Database (db_*)

db_query

Execute a read-only SELECT statement.

ParameterTypeRequiredDescription
sqlstringYesSELECT statement
paramsarrayNoBound parameters

Can query local_* tables and most adf_* tables. Cannot query adf_meta, adf_config, or adf_identity. Results are capped at 500 rows by default — use LIMIT or _full: true from code to get more.

const rows = await adf.db_query({ sql: 'SELECT * FROM local_metrics WHERE ts > ?', params: [Date.now() - 3600000] })
const allRows = await adf.db_query({ sql: 'SELECT * FROM local_events', _full: true }) // code execution only

db_execute

Execute INSERT, UPDATE, DELETE, or CREATE TABLE on local_* tables.

ParameterTypeRequiredDescription
sqlstringYesSQL statement
paramsarrayNoBound parameters
await adf.db_execute({
  sql: 'CREATE TABLE IF NOT EXISTS local_events (id TEXT PRIMARY KEY, data TEXT, ts INTEGER)'
})
await adf.db_execute({
  sql: 'INSERT INTO local_events (id, data, ts) VALUES (?, ?, ?)',
  params: ['evt_1', '{"type":"click"}', Date.now()]
})

System (sys_*)

sys_code

Execute code in the persistent sandbox. Has access to standard library packages (xlsx, pdf-lib, mupdf, docx, jszip, sql.js, cheerio, yaml, date-fns, jimp) via standard import syntax.

ParameterTypeRequiredDescription
codestringYesCode to execute
languagestringNoLanguage hint (default: "javascript")
timeoutnumberNoTimeout in ms (max 300000)

sys_lambda

Call a function from a workspace file.

ParameterTypeRequiredDescription
sourcestringYes"path/file.ts:functionName" (defaults to main if no function specified)
argsobjectNoArguments passed to the function
const result = await adf.sys_lambda({ source: 'lib/math.ts:add', args: { a: 1, b: 2 } })

Authorization behavior: When called from the LLM loop targeting an authorized file, the runtime triggers a HIL approval prompt. If approved, the lambda runs with authorization and can call restricted tools/methods. If the target is not authorized, it runs normally with no prompt. From code execution, unauthorized callers cannot call authorized targets (REQUIRES_AUTHORIZED_CALLER); authorized callers propagate authorization based on the target file’s flag.

sys_fetch

Make an HTTP request.

ParameterTypeRequiredDescription
urlstringYesURL to fetch
methodstringNoHTTP method (default: "GET")
headersobjectNoRequest headers
bodystringNoRequest body
timeout_msnumberNoTimeout in ms (default: 30000, max: 60000)

Response bodies are capped at 25 MB. The body field type depends on the response Content-Type:

  • Text (text/*, application/json, application/xml, *+json, *+xml) — body is a string
  • Binary (everything else) — body is a Buffer
// Text response — body is a string
const res = await adf.sys_fetch({ url: 'https://api.example.com/data' })
const parsed = JSON.parse(res.body)

// Binary response — body is a Buffer, write directly to a file
const audio = await adf.sys_fetch({ url: 'https://api.example.com/tts', method: 'POST', ... })
await adf.fs_write({ mode: 'write', path: 'output.mp3', content: audio.body })

sys_set_state

Transition the agent to a new state.

ParameterTypeRequiredDescription
statestringYes"idle", "hibernate", or "off"

Behavior from a lambda:

  • "idle" and "hibernate" apply immediately if the executor is idle, or at end-of-turn if a turn is in progress.
  • "off" is never deferred. It aborts any in-flight LLM call, clears pending triggers, and fires the centralized hard-off teardown (mesh unregister, MCP disconnect, adapters stopped, code sandbox destroyed). Use this when implementing remote shutdown — a compromised child cannot keep running for the remainder of its turn.

See Agent States for the full lifecycle and Triggers for system-scope lambda examples including parent-controlled shutdown.

sys_get_config

Returns the full agent configuration (no parameters needed).

const config = await adf.sys_get_config({})

sys_update_config

Modify agent configuration using a dot-path. See Tools > sys_update_config for the path-based API (basic field updates, array operations, and numeric path indexing).

sys_create_adf

Create a new .adf file. Supports template-based creation (pass template path to a .adf in the file store) and file injection (pass files array of { parent_path, child_path } pairs). See Tools > sys_create_adf for the full parameter list.

// Basic creation
await adf.sys_create_adf({ name: 'worker-1', instructions: 'You are a worker agent.' })

// Template-based creation with file injection
await adf.sys_create_adf({
  name: 'worker-2',
  template: 'templates/worker.adf',
  files: [
    { parent_path: 'config/prompts.md', child_path: 'prompts.md' }
  ],
  model: { temperature: 0.5 }  // overrides template's model.temperature
})

Timer Tools

sys_set_timer

Create a timer. Requires a schedule object with a type discriminator and a scope array.

ParameterTypeRequiredDescription
scheduleobjectYesSchedule config — see schedule.type values below
scopestring[]Yes["system"], ["agent"], or ["system", "agent"]
payloadstringNoString passed to handler on fire
lambdastringNoSystem scope: script entry point
warmbooleanNoSystem scope: keep worker alive

schedule.type values:

TypeRequired fieldOptional fields
"once"at (Unix ms)—
"delay"delay_ms (ms)—
"interval"every_ms (ms)start_at, end_at, max_runs
"cron"cron (5-field expr)end_at, max_runs
await adf.sys_set_timer({
  schedule: { type: 'interval', every_ms: 60000 },
  scope: ['system'],
  lambda: 'lib/monitor.ts:checkHealth',
  warm: true,
  payload: 'health_check'
})

sys_list_timers

List all active timers (no parameters needed).

const timers = await adf.sys_list_timers({})

sys_delete_timer

Delete a timer.

ParameterTypeRequiredDescription
idstringYesTimer ID
await adf.sys_delete_timer({ id: 'timer_abc123' })

Loop Management

loop_compact / loop_clear — not callable from code

Both are refused here (EXCLUDED_TOOL), and equally through adf loop_compact in adf_shell. They are only half a tool: the runtime finishes the reset in turn post-processing by matching the top-level tool name of the model’s turn, so a nested call never triggers it.

await adf.loop_compact({})
// → Error: loop_compact only works as a direct tool call ...

Skipping that branch fails quietly, which is why the call is refused outright:

  • loop_compact — the tool only signals intent; the summarize/clear/re-seed pass lives entirely in the executor, so nothing would be compacted.
  • loop_clear — the rows would be deleted, but the in-memory session would keep holding (and sending) those turns, desyncing the DB from the live loop.

Call them as direct tool calls instead. loop_compact takes an optional instructions string to steer what the summary preserves; loop_clear takes Python-style start/end slice indices (both support negatives).

Special Methods

These methods are available in code execution (sys_code/sys_lambda) via the adf proxy. They are not regular tools — they don’t appear in the LLM’s tool list or the Tools config section. Instead, they are controlled independently via the Code Execution config section in the agent panel. All are enabled by default.

model_invoke

Make a direct LLM call using a messages array (chat completion format). No tools or streaming.

ParameterTypeRequiredDescription
messagesarrayYes*Array of message objects with role and content
promptstringYes*Shorthand for a single user message (*either messages or prompt is required)
systemstringNoShorthand system message, prepended when prompt is used
modelstringNoModel ID override (e.g., "anthropic/claude-haiku-3-5-20241022")
max_tokensnumberNoMax response tokens (default: from agent config, fallback 4096)
temperaturenumberNoSampling temperature (default: from agent config, fallback 0.7)
top_pnumberNoTop-p sampling (default: from agent config)

Each message object has:

FieldTypeDescription
rolestring"system", "user", or "assistant"
contentstring or arrayText string, or array of content blocks (see below)

Each content block in the array can be:

  • { type: "text", text: "..." } — a text block
  • { type: "image_url", image_url: { url: "data:<mime>;base64,<data>" } } — an inline image (requires a vision-capable model)

System messages must appear at the start of the array, before any user/assistant messages.

Returns raw text (not JSON-parsed).

// Simple single-turn call (prompt shorthand)
const gist = await adf.model_invoke({ prompt: 'Summarize this in one sentence: ' + longText })

// Equivalent messages form, with sampling params
const summary = await adf.model_invoke({
  messages: [{ role: 'user', content: 'Summarize this in one sentence: ' + longText }],
  max_tokens: 256,
  temperature: 0.3
})

// With a system prompt
const french = await adf.model_invoke({
  messages: [
    { role: 'system', content: 'Respond in French' },
    { role: 'user', content: 'Hello, how are you?' }
  ]
})

// Multi-turn conversation
const response = await adf.model_invoke({
  messages: [
    { role: 'user', content: 'What is 2+2?' },
    { role: 'assistant', content: '4' },
    { role: 'user', content: 'Multiply that by 3' }
  ]
})

// Model override — use a different model for this call
const fast = await adf.model_invoke({
  messages: [{ role: 'user', content: 'Quick classification: is this spam?' }],
  model: 'anthropic/claude-haiku-3-5-20241022'
})

task_resolve

Approve, deny, or escalate a task. For HIL tasks (executor_managed: true), approval signals the executor to proceed. For deferred tasks, approval executes the tool directly.

ParameterTypeRequiredDescription
task_idstringYesThe task ID to resolve
actionstringYes"approve", "deny", or "pending_approval"
reasonstringNoReason for denial
modified_argsobjectNoModified tool arguments (for approve)
requires_authorizationbooleanNoSet to true to require authorized code for future approve/deny (one-way, cannot be unset)
await adf.task_resolve({ task_id: 'task_abc123', action: 'approve' })
await adf.task_resolve({ task_id: 'task_def456', action: 'deny', reason: 'Rate limit exceeded' })
await adf.task_resolve({ task_id: 'task_ghi789', action: 'pending_approval', requires_authorization: true })

When requires_authorization is set, only authorized code can subsequently approve or deny the task. Setting to pending_approval is always allowed from any code. Tool side effects (e.g., sys_set_state state transitions) are propagated when the task is approved.

sys_lambda

Available as a special method even when the sys_lambda tool is not in the agent’s tool list. See sys_lambda above.

loop_inject

Queue code-authored user context for the next safe model boundary. Only available from code execution — not exposed as an LLM tool. ADF persists the context to adf_loop immediately for audit, then adds it to the active conversation only after any preceding tool batch has its complete results. It therefore never splits a tool_use / tool_result exchange.

ParameterTypeRequiredDescription
contentstringYesText context to deliver. Subject to the configured tool-result-size limit.
role"user"NoOptional explicit role. Only user context is supported.
categorystringNoLowercase provenance category, default loop_inject.
keystringNoReplaces an earlier pending injection with the same key; every version remains auditable and only the latest keyed value rehydrates after restart.

Useful for lambdas and triggers that need to programmatically add mutable context such as a skill catalog, state snapshot, or trigger output. Entries are stored in a versioned [Context: …] format with their category, runtime-derived origin, and optional key, so the loop parser and UI handle them automatically. A key coalesces only updates that are still pending; it does not rewrite provider history that was already delivered. After restart, the latest keyed value is re-queued for the next model boundary; unkeyed one-shot context is not replayed.

loop_inject does not accept system messages, assistant messages, tool calls, or tool results. Those shapes could forge model history, bypass HIL, or create invalid provider tool pairings.

await adf.loop_inject({
  content: JSON.stringify(skillsRegistry),
  category: 'skills_registry',
  key: 'skills_registry'
})

identity_status

Read envelope state without exposing identity values, envelope descriptors, recipient slots, or key material. This code-execution-only method is useful for fail-closed preflights before storing sensitive portable state.

const status = await adf.identity_status({})
// { envelopes: { identity: 'unlocked', credentials: 'unlocked' },
//   password_protected: false }

if (status.envelopes.credentials !== 'unlocked') {
  throw new Error('Credential envelope must be protected and unlocked.')
}

Each envelope is one of unlocked, locked, foreign, or absent. password_protected reports legacy whole-keystore password protection; it does not reveal the password or any stored value.

get_identity

Read a value from the agent’s adf_identity table. Only available from code execution — not exposed as an LLM tool.

ParameterTypeRequiredDescription
purposestringYesThe identity key to look up

Returns the raw value as a string. Returns an error if the key doesn’t exist or code_access is disabled for that key.

Security boundary: get_identity only reads from adf_identity — it never falls back to app-level settings. Runtime/app-level provider keys (used by model_invoke via server-side injection) are never exposed to agent code. If the agent needs raw API access (e.g. for audio APIs), the user must store a key in the agent’s identity store with code_access enabled.

// Read an API key stored in identity with code_access enabled
const apiKey = await adf.get_identity({ purpose: 'provider:openrouter:apiKey' })

// Use it with sys_fetch for direct API calls
const resp = await adf.sys_fetch({
  url: 'https://openrouter.ai/api/v1/chat/completions',
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ model: 'openai/gpt-4o-audio-preview', messages: [...] })
})

set_identity

Store a value in the agent’s adf_identity table. Only available from code execution — not exposed as an LLM tool. Used for MCP server credentials (purpose: mcp:<serverName>:<key>), channel adapter credentials (purpose: adapter:<type>:<KEY>, e.g. adapter:telegram:TELEGRAM_BOT_TOKEN — exact key names and the full self-setup flow in channels.md), API keys, or other secrets.

ParameterTypeRequiredDescription
purposestringYesThe identity key (e.g. mcp:garmin:GARMIN_EMAIL)
valuestringYesThe value to store

Keys created with set_identity get code_access enabled, so the agent can read them back later with get_identity. Overwriting an existing key updates only its value — the key’s current code_access flag is preserved, so a key the user has hidden from code stays hidden even after code overwrites it.

// Store a credential from code...
await adf.set_identity({ purpose: 'mcp:garmin:GARMIN_EMAIL', value: 'user@example.com' })

// ...and read it back later
const email = await adf.get_identity({ purpose: 'mcp:garmin:GARMIN_EMAIL' })

attestation_list

List this agent’s attestations — signed certificates other identities have issued about it (ownership, group membership, roles). No parameters. Attestations are public by design; anyone with the file can read them.

const certs = await adf.attestation_list()
// [{ issuer: 'did:key:z…', subject: '<my did>', role: 'member', scope: 'group:research', signature: 'ed25519:…', … }]

attestation_add

Store an attestation another agent issued about this agent — the receiving half of a trust exchange (e.g. a group leader granting membership).

ParameterTypeRequiredDescription
attestationobjectYesThe signed attestation exactly as received

The runtime validates at the boundary: the signature must verify against the issuer DID, the subject must be this agent’s DID, reserved roles (owner, operator, runtime, clone, rotation) are rejected, and adding the same certificate twice is a harmless no-op.

attestation_issue

Sign an attestation about another DID with this agent’s key — the granting half of a trust exchange. In the default restricted_methods list, so it requires authorized code: signing certificates is a deliberate trust act.

ParameterTypeRequiredDescription
subjectstringYesThe DID the certificate is about (must be a did:key, not your own)
rolestringYese.g. member, reviewer — reserved roles are rejected
scopestringNoWhat the role covers (e.g. group:research)
expires_atstringNoISO 8601 expiry

The signed certificate is returned, not stored — attestations live with their subject. Send it to the subject, who stores it with attestation_add.

// Group leader: sign a membership cert and send it to the requester
const cert = await adf.attestation_issue({ subject: requesterDid, role: 'member', scope: 'group:research' })
await adf.msg_send({ to: requesterDid, content: JSON.stringify({ type: 'membership_granted', cert }) })

// Requester (on receipt): store it
await adf.attestation_add({ attestation: cert })

set_meta_protection

Change the protection level of a meta key. Only available from authorized code.

ParameterTypeRequiredDescription
keystringYesThe meta key
protectionstringYes"none", "readonly", or "increment"

Returns an error if the key doesn’t exist.

// Lock a key after writing it
await adf.sys_set_meta({ key: 'deployment_version', value: '2.1.0' })
await adf.set_meta_protection({ key: 'deployment_version', protection: 'readonly' })

// Unlock a key for update, then re-lock
await adf.set_meta_protection({ key: 'adf_name', protection: 'none' })
await adf.sys_set_meta({ key: 'adf_name', value: 'New Name' })
await adf.set_meta_protection({ key: 'adf_name', protection: 'readonly' })

set_file_protection

Change the protection level of a file. Only available from authorized code.

ParameterTypeRequiredDescription
pathstringYesFile path
protectionstringYes"none", "read_only", or "no_delete"

Returns an error if the file doesn’t exist.

// Lock a config file after deployment
await adf.set_file_protection({ path: 'config/production.json', protection: 'read_only' })

// Temporarily unlock for patching
await adf.set_file_protection({ path: 'lib/handler.ts', protection: 'none' })
await adf.fs_write({ path: 'lib/handler.ts', content: updatedCode })
await adf.set_file_protection({ path: 'lib/handler.ts', protection: 'read_only' })

Authorized Meta/File Bypass

When called from authorized code, the following tools bypass all protection checks — same privilege as the Studio UI:

  • sys_set_meta / sys_delete_meta — overwrite readonly keys, write non-incrementing values to increment keys, delete protected keys.
  • fs_write — overwrite read_only files.
  • fs_delete — delete read_only or no_delete files.

From unauthorized code (including sys_code), protection is enforced normally.

// From authorized code — works even though adf_name is readonly
await adf.sys_set_meta({ key: 'adf_name', value: 'Renamed Agent' })

// From authorized code — works even though the file is read_only
await adf.fs_write({ path: 'locked-config.json', content: '...', mode: 'write' })

// From unauthorized code — returns error: Cannot write to "adf_name": key is readonly.
await adf.sys_set_meta({ key: 'adf_name', value: 'Renamed Agent' })

Async Execution (_async)

Any tool call can be made asynchronous by adding _async: true to the arguments. The tool executes in the background and returns immediately with a task reference. For restricted tools, the task is created with pending_approval status — the caller continues without blocking while approval is pending.

const task = await adf.msg_send({
  recipient: 'did:adf:9gvayMZx5m...',
  address: 'http://127.0.0.1:7295/agents/monitor/inbox',
  payload: 'Large dataset ready',
  _async: true
})
// task = { task_id: "task_xxxxxxxxxxxx", status: "running", tool: "msg_send" }

Use db_query to check task status:

const result = await adf.db_query({
  sql: 'SELECT status, result, error FROM adf_tasks WHERE id = ?',
  params: [task.task_id]
})

Async tasks are tracked in the adf_tasks table and can trigger on_task_complete events.

Full Output (_full)

Some tools truncate their output to protect the LLM context window. When calling these tools from code execution, you can add _full: true to bypass all output limits and get the complete result.

Unlike _async, _full is only available from code execution — the runtime strips it from direct LLM tool calls.

ToolDefault LimitWith _full: true
db_query500 row capReturns all rows

Note: fs_read no longer needs _full — it always returns full content from code execution. Truncation is applied by the executor only when results go to the LLM context.

// Read a large file — fs_read always returns full content from code
const result = await adf.fs_read({ path: 'data/export.csv' })
const lines = result.content.split('\n')

// Process every row in a large table
const rows = await adf.db_query({
  sql: 'SELECT * FROM local_events',
  _full: true
})
for (const row of rows) {
  // process each row...
}

This is safe because the result goes to your code, not the LLM context. Use it when you need to programmatically process data that exceeds the agent’s token limits.

Error Handling

When a tool call fails, the adf proxy throws an error with a code property. Catch errors to handle them gracefully:

try {
  await adf.fs_read({ path: 'missing.txt' })
} catch (err) {
  console.error(err.code)    // 'TOOL_ERROR'
  console.error(err.message) // 'File not found: missing.txt'
}

Error Codes

CodeDescription
NOT_FOUNDTool does not exist or is not declared in agent config
DISABLEDTool exists but is disabled in agent config
REQUIRES_AUTHORIZED_CODETool is restricted — cannot be called from unauthorized code
TOOL_ERRORTool executed but returned an error
CIRCULAR_CALLsys_lambda A called B which called A
EXCLUDED_TOOLTool cannot be called from code (say, ask)
FN_ERRORsys_lambda execution failed
INVALID_INPUTMissing or invalid parameters
INVALID_STATETask is not in a resolvable state
MODEL_ERRORmodel_invoke LLM call failed
MODEL_REFUSEDmodel_invoke returned empty content
INTERNAL_ERRORUnexpected runtime error
WRITE_ERRORDatabase write failed (e.g., set_meta_protection, set_file_protection)
MESH_NOT_ENABLEDMesh tools (msg_send, agent_discover) require the mesh to be enabled
TIMEOUTExecution exceeded the timeout

Excluded Tools

The following tools cannot be called from code:

  • say — Turn tool, only meaningful in the LLM loop
  • ask — Requires human interaction, only works in the LLM loop