adf Proxy Object
The global adf RPC proxy in code execution: calling convention and every special method (model_invoke, identity, attestations, events)
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:
- Single object argument — Pass one object with named parameters
- Always await — All calls are asynchronous and return Promises
- 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | File path |
start_line | number | No | Start line (1-based) for text files |
end_line | number | No | End line (inclusive) for text files |
Return shape: { path, content, mime_type, size, protection, created_at, updated_at }
- Text files:
contentis the raw text string - Binary files:
contentis a base64-encoded string - Media files (images, audio, video):
contentis base64-encoded. When the correspondingmodel.multimodalmodality is enabled, the executor sends a native content block (image_url,input_audio, orvideo_url) to the LLM alongside the JSON row so the agent can perceive the media. Media blocks are ephemeral (not persisted toadf_loop). When disabled, or the file exceeds the size limit, the JSON row is returned withcontent: null. See Multimodal for details. README.md/mind.md: synthesized record withprotection: '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:
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | "write" |
path | string | Yes | File path |
content | string or Buffer | Yes | File content (Buffer for binary, string for text) |
protection | string | No | "read_only", "no_delete", or "none" |
encoding | string | No | "base64" when content is a base64-encoded string |
mime_type | string | No | MIME 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[]:
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | "edit" |
path | string | Yes | File path |
old_text | string | Yes* | Text to find (must match exactly once unless replace_all) |
new_text | string | Yes* | Replacement text |
replace_all | boolean | No | Replace every occurrence instead of requiring a unique match |
edits | array | No* | 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | "append" |
path | string | Yes | File path |
content | string | Yes | Text 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
prefix | string | No | Path prefix filter (e.g., "lib/") |
const files = await adf.fs_list({})
const libFiles = await adf.fs_list({ prefix: 'lib/' })
fs_delete
Delete a file.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | File 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):
| Parameter | Type | Required | Description |
|---|---|---|---|
recipient | string | Yes* | Recipient DID (e.g., "did:adf:...") or adapter address (e.g., "telegram:123") |
address | string | Yes* | Delivery URL (validated). Not needed for adapter recipients, or when replying via parent_id. |
content | string | Yes | Message content |
content_type | string | No | MIME 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. |
subject | string | No | Optional subject line |
thread_id | string | No | Conversation thread id (inherited from parent_id if omitted) |
parent_id | string | No | Parent message ID (reply); resolves recipient/address/thread from the parent |
attachments | string[] | No | File paths to attach |
meta | object | No | Envelope metadata |
message_meta | object | No | Payload 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Max messages to return |
status | string | No | Filter: "unread", "read", "archived" |
include_original | boolean | No | Include 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
ids | string[] | Yes | Message IDs to update |
status | string | Yes | New status: "read" or "archived" |
await adf.msg_update({ ids: ['msg_abc123'], status: 'archived' })
msg_list
Get inbox message counts by status.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by status |
const counts = await adf.msg_list({})
agent_discover
Discover agents on the mesh.
| Parameter | Type | Required | Description |
|---|---|---|---|
include_subdirectories | boolean | No | Include 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
adapter | string | Yes | Adapter type: "telegram", "discord", "slack", "whatsapp", … |
chat_id | string | Yes | Platform chat id — source_context.chat_id (or channel_id) from an inbox message |
limit | number | No | Max 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | string | Yes | "inbox" or "outbox" |
filter | object | Yes | At 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
sql | string | Yes | SELECT statement |
params | array | No | Bound 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
sql | string | Yes | SQL statement |
params | array | No | Bound 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Code to execute |
language | string | No | Language hint (default: "javascript") |
timeout | number | No | Timeout in ms (max 300000) |
sys_lambda
Call a function from a workspace file.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | string | Yes | "path/file.ts:functionName" (defaults to main if no function specified) |
args | object | No | Arguments 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to fetch |
method | string | No | HTTP method (default: "GET") |
headers | object | No | Request headers |
body | string | No | Request body |
timeout_ms | number | No | Timeout 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) —bodyis astring - Binary (everything else) —
bodyis aBuffer
// 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
state | string | Yes | "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.
| Parameter | Type | Required | Description |
|---|---|---|---|
schedule | object | Yes | Schedule config — see schedule.type values below |
scope | string[] | Yes | ["system"], ["agent"], or ["system", "agent"] |
payload | string | No | String passed to handler on fire |
lambda | string | No | System scope: script entry point |
warm | boolean | No | System scope: keep worker alive |
schedule.type values:
| Type | Required field | Optional 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Timer 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
messages | array | Yes* | Array of message objects with role and content |
prompt | string | Yes* | Shorthand for a single user message (*either messages or prompt is required) |
system | string | No | Shorthand system message, prepended when prompt is used |
model | string | No | Model ID override (e.g., "anthropic/claude-haiku-3-5-20241022") |
max_tokens | number | No | Max response tokens (default: from agent config, fallback 4096) |
temperature | number | No | Sampling temperature (default: from agent config, fallback 0.7) |
top_p | number | No | Top-p sampling (default: from agent config) |
Each message object has:
| Field | Type | Description |
|---|---|---|
role | string | "system", "user", or "assistant" |
content | string or array | Text 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
task_id | string | Yes | The task ID to resolve |
action | string | Yes | "approve", "deny", or "pending_approval" |
reason | string | No | Reason for denial |
modified_args | object | No | Modified tool arguments (for approve) |
requires_authorization | boolean | No | Set 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Text context to deliver. Subject to the configured tool-result-size limit. |
role | "user" | No | Optional explicit role. Only user context is supported. |
category | string | No | Lowercase provenance category, default loop_inject. |
key | string | No | Replaces 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
purpose | string | Yes | The 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
purpose | string | Yes | The identity key (e.g. mcp:garmin:GARMIN_EMAIL) |
value | string | Yes | The 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
attestation | object | Yes | The 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
subject | string | Yes | The DID the certificate is about (must be a did:key, not your own) |
role | string | Yes | e.g. member, reviewer — reserved roles are rejected |
scope | string | No | What the role covers (e.g. group:research) |
expires_at | string | No | ISO 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The meta key |
protection | string | Yes | "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.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | File path |
protection | string | Yes | "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— overwritereadonlykeys, write non-incrementing values toincrementkeys, delete protected keys.fs_write— overwriteread_onlyfiles.fs_delete— deleteread_onlyorno_deletefiles.
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.
| Tool | Default Limit | With _full: true |
|---|---|---|
db_query | 500 row cap | Returns 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
| Code | Description |
|---|---|
NOT_FOUND | Tool does not exist or is not declared in agent config |
DISABLED | Tool exists but is disabled in agent config |
REQUIRES_AUTHORIZED_CODE | Tool is restricted — cannot be called from unauthorized code |
TOOL_ERROR | Tool executed but returned an error |
CIRCULAR_CALL | sys_lambda A called B which called A |
EXCLUDED_TOOL | Tool cannot be called from code (say, ask) |
FN_ERROR | sys_lambda execution failed |
INVALID_INPUT | Missing or invalid parameters |
INVALID_STATE | Task is not in a resolvable state |
MODEL_ERROR | model_invoke LLM call failed |
MODEL_REFUSED | model_invoke returned empty content |
INTERNAL_ERROR | Unexpected runtime error |
WRITE_ERROR | Database write failed (e.g., set_meta_protection, set_file_protection) |
MESH_NOT_ENABLED | Mesh tools (msg_send, agent_discover) require the mesh to be enabled |
TIMEOUT | Execution exceeded the timeout |
Excluded Tools
The following tools cannot be called from code:
say— Turn tool, only meaningful in the LLM loopask— Requires human interaction, only works in the LLM loop