Types and build helpers for Agent Code extensions.
An API v2 extension has two kinds of code:
- Runtime: one module per extension that owns commands, state and background work. It runs in a hidden, sandboxed document.
- Views: one or more modules that render UI into a modal or a pane. Each open view is its own sandboxed document and talks to the runtime through requests and published state.
Your code has no Node.js access, no network access, and no access to Agent Code internals. Everything it can do is listed in API reference.
npm i -D github:Juliusolsson05/agent-code-extension-api#v0.11.0The SDK is distributed from GitHub, not npm; its built dist/ is committed. It contains types and a Vite preset only. Your extension bundles what it uses, so installing an extension never downloads the SDK.
API v2 needs an Agent Code build that includes the extension platform. Older builds refuse v2 manifests at install.
agent-code.extension.json at the repository root:
{
"id": "counter",
"name": "Counter",
"description": "Shared state across independent views.",
"version": "0.1.0",
"apiVersion": 2,
"entry": "dist/runtime.js",
"activationEvents": ["onCommand:counter.increment", "onView:counter.main"],
"contributes": {
"commands": [
{ "id": "counter.open", "title": "Counter: Open" },
{ "id": "counter.increment", "title": "Counter: Increment" }
],
"views": [{ "id": "counter.main", "title": "Counter", "mount": "panel", "entry": "dist/view.js" }]
}
}src/runtime.ts:
import { defineRuntime } from 'agent-code-extension-api'
export default defineRuntime({
async activate(context) {
let count = (await context.api.storage.get<number>('count')) ?? 0
context.registerCommand('counter.increment', async () => {
count += 1
await context.api.storage.set('count', count) // persist on change, not in deactivate
await context.views.publish('counter.main', { count })
return count
})
await context.views.publish('counter.main', { count })
},
})src/view.ts:
import { defineView } from 'agent-code-extension-api'
export default defineView<{ count: number }>({
mount(element, context) {
const render = (state?: { count: number }) => {
element.textContent = `Count: ${state?.count ?? 0}`
}
render(context.runtime.state())
return context.runtime.subscribe(render)
},
})vite.config.ts:
import { extensionViteConfig } from 'agent-code-extension-api'
export default extensionViteConfig({
entries: { runtime: 'src/runtime.ts', view: 'src/view.ts' },
})Run vite build, commit dist/, and load the folder in Agent Code with Load extension from folder.
The runtime starts only for an event the manifest declares in activationEvents:
| Event | Starts the runtime |
|---|---|
onStartupFinished |
When Agent Code starts, and after install or update. |
* |
Same as onStartupFinished. |
onCommand:<commandId> |
The first time that command runs. |
onView:<viewId> |
The first time that view opens. |
- Startup extensions restart on use. An
onStartupFinishedor*extension can also be started by any of its own commands or views, so it recovers from a crash without an app restart. - Lazy extensions start only for declared events. Their contributions start nothing unless they are named in
activationEvents. - Declared events must exist. Every activation event must name a command or view this manifest contributes.
- The runtime outlives its views. Once running, it stays up until Agent Code quits or the extension is updated or removed. Closing the last view does not stop it.
Commands appear in the command palette and can have keybindings.
- Opening views: a command whose id equals a view id opens that view, and so does
<extensionId>.openwhen the extension has exactly one view. The host does this for you. - Other commands call the handler the runtime registered with
registerCommand. The handler's return value or thrown error is reported back to the caller. A command that exceeds the 30-second invocation deadline is not retried; its outcome is unknown and the runtime is restarted.
A view is declared with a mount of modal (floating dialog) or panel (a pane in the grid or Dispatch), and its own entry module.
- Mounting: before
mount(element, context)runs, the view has attached to the runtime and received theme tokens. - Cleanup:
mountis synchronous. Return a cleanup function; it runs when the view closes. - Instances: several instances of one view can be open at once, in different windows too. They share whatever the runtime publishes.
Passed to activate(context) in the module you export with defineRuntime.
| Member | Description |
|---|---|
api |
Host API available to runtimes; see Host API. |
registerCommand(id, handler) |
Handle a contributed command. handler() may return JSON or nothing. Returns a disposable. |
registerRequest(name, handler) |
Answer context.runtime.request(name, input) from views. handler(input, view) gets the JSON input and the caller's { id, instanceId }; validate the input yourself. Returns a disposable. |
views.publish(viewId, state) |
Replace the latest JSON state for a view id. Every open instance receives it, and later instances read it on open. |
subscriptions |
Push disposables here; they are disposed when the runtime shuts down. |
The module may also export deactivate(). Shutdown allows 500 ms for deactivate and your subscriptions, and API calls no longer work at that point. Save state when it changes, not on shutdown.
Passed to mount(element, context) in the module you export with defineView.
| Member | Description |
|---|---|
api |
Host API available to views; see Host API. |
view |
This instance's { id, instanceId }. |
runtime.state() |
Latest published state, or undefined before the first publish. |
runtime.subscribe(listener) |
Receive later publishes. Returns an unsubscribe function. Read state() first for the current value. |
runtime.request(name, input?) |
Call a runtime registerRequest handler. Resolves with its JSON result. Closing the view rejects pending requests. |
context.api in runtimes and views. Calls return promises.
| Call | Runtime | View | Permission |
|---|---|---|---|
extension.id, extension.apiVersion |
✓ | ✓ | none |
storage.get(key), set(key, value), delete(key), keys() |
✓ | ✓ | none |
secrets.get(key), set(key, value), delete(key) |
✓ | ✓ | none (API v2, Agent Code ≥ the first supporting version; optional, so feature-detect if (api.secrets)) |
net.fetch(url, init) |
✓ | ✓ | net.connect (private IPs) or net.origins (declared origins) |
services.start/stop/status/invoke(id, …) |
✓ | ✓ | service.run |
services.expose(id, lan) |
✓ | ✓ | net.listen |
files.readText({ sessionId, path }) |
✓ | ✓ | fs.read |
files.writeText({ sessionId, path, text, expectedVersion }) |
✓ | ✓ | fs.write |
notifications.show(message) |
✓ | ✓ | notifications.show |
ui.close() |
✓ | none | |
ui.showToast(message) |
✓ | none | |
theme.tokens() |
✓ | none | |
workspace.observe(), workspace.subscribe(listener) |
✓ | workspace.observe |
|
sessions.observe(), sessions.subscribe(listener) |
✓ | sessions.observe |
|
panes.observe(), panes.subscribe(listener) |
✓ | panes.observe |
A runtime has no UI, focus or theme of its own. Views pass it what it needs, such as a session id, through requests.
Storage. A durable JSON key-value store per extension:
getresolvesundefinedfor a missing key.- Data survives updates and uninstall, and a reinstall with the same id sees it again.
- Browser
localStorageand cookies are cleared and are not persistent.
Files. Read and write UTF-8 text inside a live session's project.
- Targets:
sessionIdnames the session, and Agent Code resolves its project directory.pathmust be relative and cannot escape through..or symlinks. There is no implicit "current" project, because focus can change while a call is pending. - Session ids come from
sessions.observe()in a view. readTextreturns{ sessionId, path, text, size, mtimeMs, version }and accepts files up to 96 KiB.writeTextis atomic and accepts text up to 64 KiB. PassexpectedVersion: nullto create a file that must not exist yet, or theversionfrom your last read to replace it. A write against a changed file is rejected rather than overwriting newer content.
Secrets. For credentials such as an API key; not a second storage:
- Requires Agent Code ≥ the first supporting version (see the CHANGELOG).
api.secretsis optional in the types; checkif (context.api.secrets)and never fall back tostorage. - The host encrypts each value with the OS keychain (Electron
safeStorage). If the OS cannot encrypt,setrejects: there is no plaintext fallback. - Scoped to your extension id;
getresolvesnullwhen absent or no longer decryptable (for example after a keychain reset). - Deleted on uninstall, unlike
storage. - Keys are 1–64 characters of
[a-zA-Z0-9._-], values 1–4096 characters, at most 32 keys. - The host never logs values or puts them in error messages. Do not put them in toasts, errors or
storageyourself.
Network. net.fetch(url, { httpMethod, headers, body, responseType }) asks the host to fetch; the sandbox has no network of its own.
- Private/loopback IP literals need
net.connect. The exact HTTPS origins in your manifest'snetworkOriginsneednet.origins. Anything else is refused. - The verb field is
httpMethod. Bodies are strings up to 64 KiB; responses are capped at 256 KiB (the host stops reading at the cap); redirects are refused. - Timeouts: 10 s for a private address, 15 s for a declared origin. A timed-out call rejects with a "timed out" error.
- These limits are fixed host limits, not per-call options. They exist because every result crosses a bounded transport. A background runtime receives results through a JSON channel of at most 128 Ki characters. That channel admits a
net.fetchresult up to the broker's 256 KiB cap after base64 expansion (plus a small metadata budget), so a runtime and a view receive the same responses. Hosts older than Agent Code ≥ the first supporting version refused a runtimebase64body above about 96 KiB. - Headers the host uses to identify callers (
x-agent-code-transport,forwarded,x-forwarded-*) are refused on everynet.fetch. - The result is
{ status, contentType, body, bodyEncoding }. PassresponseType: 'base64'for binary bodies and checkbodyEncoding: hosts older than Agent Code ≥ the first supporting version ignoreresponseTypeand return text.
Services and transport attestation. A service written with runService can tell who is calling from the TRANSPORT_ATTESTATION_HEADER (x-agent-code-transport), which the host sets on every request it delivers and never copies from a caller:
TRANSPORT_ATTESTATION.service('service'): your own view or runtime throughfetch('./__service/<id>/…').TRANSPORT_ATTESTATION.lan('lan'): a local-network guest throughservices.expose(id, true). The host also setsx-forwarded-forandx-forwarded-host. Treat the request as that remote peer.- Trust the header only on your loopback socket when
Hostis exactly127.0.0.1:<your port>, and never grant CORS. Any local process can send any header, so pair it with your own token when the distinction guards something that matters. The JSDoc on the export gives the full rules.
Notifications. notifications.show(message) shows an in-app toast in every Agent Code window, prefixed with your extension's name. Messages are at most 200 characters. This is the way to report background work while no view is open. It is not an OS notification.
UI. ui.close() closes the pane or modal hosting this view. ui.showToast(message) shows a transient in-app toast.
Theme. View documents receive Agent Code's --theme-* CSS custom properties, which update on theme changes, so prefer var(--theme-surface) and similar in CSS. theme.tokens() returns the same values as an object for canvas or SVG drawing.
Observation. Read-only snapshots of Agent Code state:
workspace.observe()→{ activeTabId, tabIds, sessionCount }sessions.observe()→[{ id, kind, cwd, title, activity, notification }]activity:'working'or'idle', the same rule as Dispatch's running count.notification: the badge Dispatch shows on that agent,{ kind, label }ornull.labelis the word Dispatch paints (NEW, ACTION, QUESTION, ERROR, …);kindisattentionwhen the agent is waiting on the user now, andoutputfor finished work nobody has looked at.- An
outputbadge clears when the user engages with the agent or rests on its pane. Anattentionbadge clears when what blocks it resolves (the prompt is answered, or a failed start is followed by a successful one), not when the user looks. - Reading them needs
sessions.observeonly; thenotifications.*permission is for showing toasts and is unrelated. - Both need Agent Code ≥ the first supporting version (see the CHANGELOG, 0.11.0) and are optional in the types. A row without
activitycomes from an older host: say so rather than showing every agent as idle. Treat anactivityvalue you do not recognise as unknown.
panes.observe()→[{ tabId, leafSessionIds }]
Each subscribe listener fires when the data changes. Call observe() again to read it. sessions.subscribe also fires when an agent's activity or notification changes. The host coalesces these, and streamed tokens that change neither field do not fire it.
Permissions are declared in permissions. Agent Code asks the user to approve them when the extension is installed or updated. An extension with no permissions installs without a prompt.
| Permission | Allows | API versions |
|---|---|---|
workspace.observe |
workspace.observe/subscribe in views |
1, 2 |
sessions.observe |
sessions.observe/subscribe in views |
1, 2 |
panes.observe |
panes.observe/subscribe in views |
1, 2 |
fs.read |
files.readText |
2 |
fs.write |
files.writeText |
2 |
notifications.show |
notifications.show |
2 |
service.run |
services.start/stop/status/invoke for contributes.services |
2 |
service.transport |
fetch('./__service/<id>/…') to your own running service |
2 |
net.listen |
services.expose(id, true) |
2 |
net.connect |
net.fetch to private/loopback IP literals |
2 |
net.origins |
net.fetch to the exact origins in networkOrigins; the consent dialog lists them |
2 |
Unknown permissions fail installation. Transcript, Git and prompt access are not available.
| Field | Rules |
|---|---|
id |
^[a-z][a-z0-9-]{0,63}$. Also the storage namespace. |
name |
1–80 characters. |
description |
1–400 characters. |
version |
1–40 characters, shown to users. |
apiVersion |
2 (or 1 for legacy extensions). |
entry |
Relative path to the built runtime module (.js or .mjs), inside the repository. |
keywords |
Up to 24, each at most 40 characters. |
activationEvents |
Up to 32; see Activation. |
permissions |
Up to 16; see Permissions. |
networkOrigins |
API v2, with net.origins (and only with it). 1–4 exact origins such as "https://api.example.com": https only, a public DNS name, no wildcard, path, trailing slash, query, credentials, IP literal, localhost or .local. |
contributes |
Commands, views, settings, keybindings and themes, below. |
Contribution ids must start with your extension id and a dot, such as counter.increment. Ids must be unique within a manifest.
Commands (up to 64): { id, title, description?, keywords? }. The title is at most 80 characters, and the description at most 400.
Views (up to 16): { id, title, mount: "modal" | "panel", entry }. entry is required in API v2 and names the built view module.
Settings (up to 64): { id, title, description?, type, default }, where type is boolean, number or string.
- Settings appear on the extension's Settings page.
- The value is stored in extension storage under the setting id.
- Read it with
api.storage.get(id), and fall back todefaultwhen it is unset. - There is no change event, so read the value when you need it.
Keybindings (up to 32): { command, key }.
commandmust be a command this manifest contributes.keymust be a single chord that includes Cmd, such ascmd+shift+t. Installation rejects bare keys and chords without Cmd.- Chords Agent Code already uses are ignored: its built-in shortcuts, Cmd+0–9, Cmd+Alt+0–9, Cmd+Left/Right, and standard editing and app chords (Cmd+A, C, V, X, Z, Shift+Z, Q, H, Alt+H, M, W, Shift+W, comma,
=,-). - Users can rebind any command in Settings.
Themes (up to 16): { id, title, colors }.
- Themes appear in the appearance picker without running extension code.
colorsmaps keys fromEXTENSION_THEME_COLOR_KEYS(81 tokens such ascanvas,surface,ink,accent) to#RGB,#RGBA,#RRGGBB,#RRGGBBAAortransparent, with at least one color. Omitted colors use Agent Code's defaults.- Updating the extension replaces its palette. Removing it falls back to the Dark theme, and reinstalling restores the selection.
Type your manifest with satisfies ExtensionManifest to catch most of these rules at compile time. Length limits and cross-references are checked only at install.
extensionViteConfig(options) returns a Vite config:
- v2:
entries: { runtime: 'src/runtime.ts', view: 'src/view.ts' }emitsdist/runtime.jsanddist/view.js, plus shared chunks. - Legacy:
entryandfileNamebuild a single v1 module. Don't combine them withentries.
The preset bundles your dependencies and uses production React and JSX, so builds work whatever NODE_ENV your shell sets. For React, spread the whole preset and add the plugin, rather than copying selected keys:
export default { ...extensionViteConfig({ entries }), plugins: [react()] }Views are not given your CSS files automatically. Import styles with ?inline and inject a <style> element from the view, or style in JavaScript. Author packages should be ESM ("type": "module").
Agent Code installs an extension from its GitHub repository:
- Source: it downloads the source of the latest GitHub release, or the default branch when the repository has no releases. Commit
dist/and publish a release for each version you want users to get. - Size: a bundle may be at most 32 MB to download and, once extracted, 64 MB, 10,000 files and 32 directory levels deep.
- Updates: Update reinstalls from the same repository. An extension with the same id from a different source is refused until the installed one is removed.
- Local development: use Load extension from folder, then Reload after rebuilding.
| Resource | Limit |
|---|---|
| JSON values (requests, results, published state, storage values) | 4,096 values, depth 32, 128 KiB of text (a net.fetch result may reach the 256 KiB body cap after base64) |
| Runtime startup | 10 s |
| Command or request | 30 s |
| Pending calls per extension | 32 |
Shutdown (deactivate and subscriptions) |
500 ms |
| API messages from a runtime | bursts of 128, refilling at about 128 per second |
| Storage | 256 keys and 1 MiB per extension; 32 queued operations per extension, 256 overall |
| File read / write | 96 KiB / 64 KiB |
| Notification message | 200 characters |
| Open views per window | 32 |
Values that exceed a limit are rejected without replacing earlier data. Saved storage files that are corrupt or unreadable are kept as they are and reported as errors.
API v1 extensions use defineExtension({ activate(context) }) and register views with context.registerView(id, mount). Their module runs separately inside each open view, so there is no shared background state. v1 can use storage, UI, theme and the three observe permissions, but not files or notifications.
To migrate to v2:
- Move commands and shared state into
defineRuntime. - Move DOM code into
defineView. - Give each view an
entry. - Set
apiVersion: 2and declareactivationEvents.
npm test builds the SDK, type-checks the example extension in examples/counter, and builds it through the Vite preset. CI also checks that rebuilding leaves the committed dist/ unchanged.