Extensions / Guides
Build an agent provider
An agent provider recognizes one foreground CLI, proves a session belongs to that terminal, then translates its journal into Terminay lifecycle events.
What you will build
This minimal provider recognizes acme-agent, follows its current JSONL
journal, and publishes a turn when it sees a prompt record. It observes an existing CLI;
it does not start, drive, or send input to that CLI.
1. Declare the integration
Put the Terminay manifest in your package’s terminay field. The contributed
provider ID must match the ID passed to registerProvider().
{
"name": "acme-terminay-agent",
"version": "0.1.0",
"type": "module",
"dependencies": {
"@terminay/extension-api": "^1.2.0"
},
"terminay": {
"manifestVersion": 1,
"id": "com.acme.agent",
"displayName": "Acme Agent",
"api": "^1.2.0",
"engines": { "terminay": ">=1.0.0", "node": ">=20" },
"entrypoint": "dist/index.js",
"permissions": ["agent-observation"],
"contributes": {
"agentProviders": [{
"id": "com.acme.agent/cli",
"displayName": "Acme Agent",
"processMatchers": [{ "executableName": "acme-agent" }],
"requiredEnvironmentCapabilities": [
"filesystem-observation",
"agent-journal"
],
"mappings": [{
"mappingVersion": "1",
"providerVersionRange": ">=1 <2"
}]
}]
}
}
}agent-observationauthorizes terminal-scoped observation and canonical event publication.processMatchersare exact, safe CLI identification hints—not shell commands or regular expressions.- Request only the environment capabilities your mapping needs. A provider must handle their absence.
2. Register and observe
Keep matchesForeground() fast and side-effect-free. Put journal discovery and
session proof in observe(), where the host gives you a terminal-scoped context.
import {
defineAgentProvider,
defineExtension,
jsonlSession,
} from "@terminay/extension-api";
const provider = defineAgentProvider({
mappingVersion: "1",
matchesForeground(process) {
return process.executableName === "acme-agent";
},
async observe(terminal) {
if (!terminal.capabilities.has("agent-journal")) {
return { state: "unavailable", reason: "environment-capability-missing" };
}
const journal = await terminal.observation.files.resolveHomeRelative(
".acme-agent/sessions/current.jsonl",
{ beneath: { homeRelative: ".acme-agent/sessions" }, extension: ".jsonl", signal: terminal.signal },
);
if (!journal) return { state: "not-bound" };
const header = await terminal.observation.files.readJsonLine(journal, {
position: "first", maxBytes: 64 * 1024, signal: terminal.signal,
});
if (!isSessionHeader(header)) return { state: "not-bound" };
const binding = await terminal.bindSession({
providerSessionId: header.id,
mappingVersion: "1",
journal,
fingerprint: { kind: "known-agent-journal", file: journal },
});
return jsonlSession({
binding,
source: terminal.observation.files.follow(journal, { signal: terminal.signal }),
mapRecord(record, session) {
if (isRecord(record) && record.type === "prompt" && typeof record.id === "string") {
session.publish.turnStarted({ turnId: record.id, promptText: asText(record.text) });
}
},
});
},
});
export default defineExtension({
activate(context) {
context.subscriptions.add(
context.agents.registerProvider("com.acme.agent/cli", provider),
);
},
});
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isSessionHeader(value: unknown): value is { id: string } {
return isRecord(value) && typeof value.id === "string";
}
function asText(value: unknown): string | undefined {
return typeof value === "string" ? value.slice(0, 4_000) : undefined;
}Observation outcomes
| Return value | Use it when |
|---|---|
{ state: "bound" } | Your provider found and bound a verifiable session. jsonlSession() returns this form. |
{ state: "not-bound" } | The CLI is recognized but no safe, current session can be established. |
{ state: "unavailable", reason } | A required environment capability is missing or the provider cannot safely observe this terminal. |
Use not-bound for an ordinary miss. Use unavailable only with a
documented reason such as environment-capability-missing,
unsupported-provider-version, or malformed-observation.
Safe boundaries
File and process values are opaque, terminal-scoped handles. A display path is not read
authority; use the observation broker for every read and follow. The API does not provide a
PTY, shell, raw terminal stream, renderer UI, or direct access to Terminay’s agent store.