ade
(alias: console)v1.9.66Web console for iii — bundles the React UI and proxies the engine WebSocket on a single port.
Install
iii trigger compose::add worker=ade@nextRun in your project with the iii engine running. Exact versions are immutable and digest-pinned.
skill doc
Chat mentions
A worker becomes a mention provider by registering two functions and declaring itself in one of them. Nothing else is needed — no console change, no harness change:
- The user types
@in the ADE composer, presses Tab, searches the worker's items and picks one; the message carries@, shown as a pill (icon, color, name) that previews on hover and opens on click. Typing(id=" ") @plus text searches every provider at once, one group each. - The agent gets, with the message, a
block from the judge's hook: each item's one-line summary and a pre-verified call that returns the full item. It may also write the token in replies; the console renders it.
The contract is crates/mention-contract (Rust types, token grammar, shared
fixtures). The kanban (@kanban), session-manager (@session) and console
(@trace) workers are reference providers.
When to Use
- The user should be able to point the agent at one of the worker's records without pasting ids: a ticket, a document, an email, an event, a user.
- The worker's records deserve a recognizable pill in chat (icon, color, status) instead of a raw id.
- An agent working with the worker should reference records in its replies so the user can open them with one click.
Boundaries
- Mention functions are console and hook plumbing: register them
internal: true(agents never see them in discovery) andtrace_hidden: true(search runs on every keystroke). Agents read a mentioned record through the details function you name — an ordinary, agent-allowed function such as.::item::get - Search and get only read. Never mutate state, never return secrets or another user's private data (session-manager leaves the parked draft out of its view).
- One token name per provider;
fn,fileandskillare taken. Two workers claiming one name: the lexicographically first get-function id wins, everywhere. - The id in the token is the record's canonical, stable id. Human handles
(
KAN-12) belong inhint; a get may accept them but answers with the canonical id.
The two functions
Search — { query, limit?, context? } → { items }, best match first.
{ "query": "fix log", "limit": 8,
"context": { "session_id": "console-…", "working_dir": "/repo" } }
→ { "items": [ { "id": "6ac4f6df-…", "label": "Fix login redirect", "hint": "KAN-12",
"description": "In progress · high", "icon": "ticket", "color": "amber" } ] }- An empty
queryreturns the most relevant recent items (the console asks for them as soon as the user scopes to@).: - Rank exact handles first (
KAN-12,12), then title prefix, then every query word starting a title word, then substring, then body text. Break ties by most recently updated. Skip deleted or archived records. limitdefaults to 8, at most 50; the global@textmenu asks for 4.- Answer fast: the console gives each provider 2.5 s and moves on without it.
context.session_idis the chat the search was asked from; leave that record out when it makes no sense to mention (session-manager drops the asking session).
Get — { id } → a view, or null for an id the worker does not know.
{ "id": "6ac4f6df-…" }
→ { "id": "6ac4f6df-…", "label": "Fix login redirect", "hint": "KAN-12",
"description": "In progress", "icon": "ticket", "color": "amber",
"fields": [ { "label": "Priority", "value": "high", "tone": "warning" },
{ "label": "Assignee", "value": "backend-engineer" } ],
"open": { "page": "<worker>-item", "context": { "id": "KAN-12" } },
"summary": "Ticket KAN-12 \"Fix login redirect\" · status: In progress · priority: high",
"data": { "…": "your domain object, for your own preview renderer" },
"updated_at": "2026-10-06T20:17:20.749Z" }labelandhintmake the pill;descriptionand up to sixfieldsmake the hover card.summaryis the one line the agent reads with the message. Make it self-contained: kind, handle, quoted title, state. Without it the agent getshint "label" — description · field: value ….openis one of{ "page", "context" }(a page the worker registered withhost.pages;tracesis the built-in traces screen,context.trace_idpicks the trace),{ "session": "or" } { "url": "https://…" }(http/https only). Omit it when there is nothing to open.datais optional and only reaches a preview renderer; keep it a small projection (kanban sends the board card plus a 280-character description excerpt), never the full record with history.- Vocabulary:
iconis one ofticket,issue,task,session,chat,message,post,trace,span,activity,event,calendar,email,mail,user,person,team,group,channel,hash,tweet,mention,file,doc,document,folder,link,agent,bot,database,table(anything else falls back to@).coloris one ofneutral,blue,purple,teal,green,amber,rose. A field'stoneisneutral,info,success,warningordanger. Items may override the provider's icon and color (color by status or priority).
The descriptor
Register it as metadata.mention on the get function — the function that
carries it is the get function, so the descriptor names only the search:
{
"internal": true,
"trace_hidden": true,
"mention": {
"v": 1,
"name": "<name>",
"label": "Tickets",
"description": "A ticket, by uuid or key (KAN-12)",
"icon": "ticket",
"color": "blue",
"search": "<worker>::mention::search",
"details": { "function_id": "<worker>::ticket::get", "id_field": "id" }
}
}name: lowercase letters, digits and-, starting with a letter, at most 40 characters. It is what users type after@, so prefer the domain noun (calendar,email) over the worker's package name when they differ.label: the plural noun of one group header ("Tickets", "Events").description: one line on what an id refers to; agents read it in, so say how the id looks.details: the agent-facing function returning the full record, called with{(: " " } id_fielddefaults toid). It must be callable by agents (allowed in the worker'siii-permissions.yaml); the hook offers it only when the session's policy allows it. Omit it when the summary is all there is.
Registering them
Node (iii-sdk; see iii-node for the worker shape):
const MENTION = {
v: 1,
name: 'calendar',
label: 'Events',
description: 'A calendar event, by event id',
icon: 'event',
color: 'purple',
search: 'calendar::mention::search',
details: { function_id: 'calendar::event::get', id_field: 'id' },
}
iii.registerFunction('calendar::mention::search', searchEvents, {
description: 'Search events for the chat @calendar mention menu; best match first.',
request_format: MENTION_SEARCH_REQUEST,
response_format: MENTION_SEARCH_RESPONSE,
metadata: { internal: true, trace_hidden: true },
})
iii.registerFunction('calendar::mention::get', eventView, {
description: 'Resolve a @calendar(id=…) chat mention to its view; null for an unknown id.',
request_format: MENTION_GET_REQUEST,
response_format: MENTION_VIEW_OR_NULL,
metadata: { internal: true, trace_hidden: true, mention: MENTION },
})Write the four JSON Schemas from the shapes above (copy them from the
session-manager goldens, session-manager/tests/golden/schemas/session.mention.*.json,
which are exactly these types).
Rust (path dependency on crates/mention-contract):
use mention_contract::{MentionProvider, MentionSearchRequest, MentionSearchResponse,
MentionGetRequest, MentionView, color};
pub fn provider() -> MentionProvider {
MentionProvider::new("calendar", "Events", "calendar::mention::search")
.description("A calendar event, by event id")
.icon("event")
.color(color::PURPLE)
.details("calendar::event::get", "id")
}
iii.register_function("calendar::mention::get",
RegisterFunction::new_async(move |req: MentionGetRequest| get(req)) // -> Result<Option<MentionView>, _>
.description("Resolve a @calendar(id=…) chat mention to its view; null for an unknown id.")
.metadata(provider().metadata()));MentionProvider::metadata() already adds internal and trace_hidden;
give the search function json!({ "internal": true, "trace_hidden": true }).
MentionView::agent_summary() is the fallback line; MentionSearchRequest:: effective_limit() applies the default and the cap.
Python or anything else: the same JSON — the console and the judge read
only engine::functions::list metadata and call the two functions by id.
Permissions: deny both mention functions to agents and allow the details function:
rules:
- '!<worker>::mention::search'
- '!<worker>::mention::get'
- <worker>::ticket::getA richer preview (optional)
Without UI code the hover shows the generic card (icon, provider label,
handle, name, description, fields). A worker that ships console UI can draw
its own card from view.data:
host.mentions?.registerRenderer({
provider: '<name>',
Preview: ({ view, open }) => <EventCard event={view.data as Event} onOpen={open} />,
})Feature-detect host.mentions (older consoles lack it). If the renderer
throws — for example data from an older backend — the generic card is
shown, so throw rather than render a half card. Keep it about 22rem wide.
What the agent sees
You write nothing for this; it is how to phrase summary and description.
On the first step of a session the judge appends:
<mention_providers>
- @calendar — Events: A calendar event, by event id
</mention_providers>and, for each new mention a user wrote:
<mentions>
- @calendar(id="evt_42") — Event "Design review" · Tue 14:00–15:00 · 6 attendees
details: calendar::event::get {"id":"evt_42"}
</mentions>The chat shows the latter as a quiet "Context for the agent" row.
Definition of done
engine::functions::list { include_internal: true }lists the get function withmetadata.mention, and the descriptor validates (name rules, non-emptylabelandsearch).iii triggeranswers recent items fast; a handle query (::mention::search --json '{"query":""}' KAN-12) ranks that record first.iii triggeranswers the view, and::mention::get --json '{"id":" "}' nullfor an unknown id.iii trigger judge::mentions::resolve --json '{"text":"@answers(id=\" \")"}' status: "resolved"with your summary and the details call.- In the ADE composer
@shows the provider, Tab scopes the search, picking inserts a pill; the pill previews on hover and opens on click. - Unit tests cover ranking (handles before titles, empty query = recent, deleted records skipped) and the view (fields, open target, summary).