594 lines
21 KiB
Plaintext
594 lines
21 KiB
Plaintext
---
|
|
title: Authoring a plugin
|
|
description: Write a custom runtime, agent, workspace, tracker, SCM, notifier, or terminal plugin for AO.
|
|
---
|
|
|
|
import { Callout } from "fumadocs-ui/components/callout";
|
|
import { Step, Steps } from "fumadocs-ui/components/steps";
|
|
import { Card, Cards } from "fumadocs-ui/components/card";
|
|
|
|
AO's seven user-pluggable slots (Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal) are all resolved at startup from the plugin registry. Each slot is backed by a TypeScript interface defined in `packages/core/src/types.ts`. Adding a new tracker, notifier, or any other integration is a matter of publishing an npm package — or pointing to a local path — that exports a manifest, a `create()` factory, and an optional `detect()` probe as its default export.
|
|
|
|
---
|
|
|
|
## The contract
|
|
|
|
Every plugin module must satisfy `PluginModule<T>`, where `T` is the interface for its slot:
|
|
|
|
```typescript
|
|
export interface PluginModule<T = unknown> {
|
|
manifest: PluginManifest;
|
|
create(config?: Record<string, unknown>): T;
|
|
detect?(): boolean;
|
|
}
|
|
|
|
export interface PluginManifest {
|
|
name: string; // must match the package-name suffix (see below)
|
|
slot: PluginSlot; // use "as const" to preserve the literal type
|
|
description: string;
|
|
version: string;
|
|
displayName?: string;
|
|
}
|
|
```
|
|
|
|
### `manifest`
|
|
|
|
The static description of your plugin. Every field is required except `displayName`.
|
|
|
|
### `create(config?)`
|
|
|
|
Called once during startup. `config` contains whatever key-value pairs appear under your plugin's YAML block. Validate everything here; store validated values in a closure. Return an object that implements the slot interface.
|
|
|
|
### `detect()` (optional)
|
|
|
|
Return `true` when the system has what your plugin needs — a binary on PATH, an environment variable, a running process. AO uses this to warn about missing dependencies at startup rather than at first use.
|
|
|
|
### Default export
|
|
|
|
```typescript
|
|
import type { PluginModule, Notifier } from "@aoagents/ao-core";
|
|
|
|
export const manifest = {
|
|
name: "my-notifier",
|
|
slot: "notifier" as const,
|
|
description: "Send alerts to My Service",
|
|
version: "0.1.0",
|
|
};
|
|
|
|
export function create(config?: Record<string, unknown>): Notifier {
|
|
// validate here, return interface implementation
|
|
}
|
|
|
|
export function detect(): boolean {
|
|
return Boolean(process.env["MY_SERVICE_TOKEN"]);
|
|
}
|
|
|
|
export default { manifest, create, detect } satisfies PluginModule<Notifier>;
|
|
```
|
|
|
|
---
|
|
|
|
## Manifest rules
|
|
|
|
- **`name`** must match the suffix of your package name. `@acme/ao-plugin-notifier-pagerduty` → `name: "pagerduty"`. The registry looks up plugins by `slot:name` key.
|
|
- **`slot`** must use `as const` to preserve the string literal type — `"notifier" as const`, not `"notifier"`.
|
|
- **`version`** follows semver. Start at `0.1.0`.
|
|
- **`displayName`** is optional. When omitted, `name` is used in log output.
|
|
|
|
---
|
|
|
|
## Create a plugin with `ao plugin create`
|
|
|
|
The fastest way to start is `ao plugin create`. It runs an interactive prompt (using `@clack/prompts`) and writes a ready-to-build package.
|
|
|
|
<Steps>
|
|
<Step>
|
|
|
|
### Run the scaffold command
|
|
|
|
```bash
|
|
ao plugin create
|
|
```
|
|
|
|
You can also pass `[directory]` as a positional argument, or supply any field with flags to skip individual prompts:
|
|
|
|
```bash
|
|
ao plugin create ./my-plugins/pagerduty \
|
|
--slot notifier \
|
|
--name "PagerDuty" \
|
|
--description "Route urgent alerts to PagerDuty" \
|
|
--author "Alice" \
|
|
--package-name "ao-plugin-notifier-pagerduty"
|
|
```
|
|
|
|
Add `--non-interactive` to require all fields to be provided via flags (useful in CI).
|
|
|
|
</Step>
|
|
<Step>
|
|
|
|
### Answer the interactive prompts
|
|
|
|
The CLI asks for four things, in order:
|
|
|
|
| Prompt | Example input |
|
|
|--------|--------------|
|
|
| Plugin display name | `PagerDuty` |
|
|
| Plugin slot | `notifier` (selected from a list of all 7 slots) |
|
|
| Short description | `Route urgent alerts to PagerDuty` |
|
|
| Author | `Alice` |
|
|
| Package name | `ao-plugin-notifier-pagerduty` (pre-filled from slot + name) |
|
|
|
|
</Step>
|
|
<Step>
|
|
|
|
### Inspect the generated files
|
|
|
|
The scaffold is written to `./{normalized-name}/` (or the directory you provided). The generated structure is:
|
|
|
|
```
|
|
pagerduty/
|
|
├── package.json # type: module, main: dist/index.js, exports map
|
|
├── tsconfig.json # ES2022 + Node16 modules, strict mode
|
|
├── README.md # AO config snippets for local and npm installs
|
|
├── .gitignore # dist/, node_modules/
|
|
└── src/
|
|
└── index.ts # manifest + stub create() + default export
|
|
```
|
|
|
|
The generated `src/index.ts` looks like this:
|
|
|
|
```typescript
|
|
import type { PluginModule } from "@aoagents/ao-core";
|
|
|
|
export const manifest = {
|
|
name: "pagerduty",
|
|
slot: "notifier" as const,
|
|
description: "Route urgent alerts to PagerDuty",
|
|
version: "0.1.0",
|
|
displayName: "PagerDuty",
|
|
};
|
|
|
|
const plugin: PluginModule = {
|
|
manifest,
|
|
create(config?: Record<string, unknown>) {
|
|
return {
|
|
name: manifest.name,
|
|
config: config ?? {},
|
|
// TODO: replace this placeholder with a real notifier implementation.
|
|
};
|
|
},
|
|
};
|
|
|
|
export default plugin;
|
|
```
|
|
|
|
</Step>
|
|
<Step>
|
|
|
|
### Implement the slot interface
|
|
|
|
Replace the placeholder `create()` body with a real implementation. Import the slot interface from `@aoagents/ao-core`:
|
|
|
|
```typescript
|
|
import type { PluginModule, Notifier } from "@aoagents/ao-core";
|
|
import { validateUrl } from "@aoagents/ao-core";
|
|
```
|
|
|
|
See [Slot interfaces at a glance](#slot-interfaces-at-a-glance) for the methods you need to implement.
|
|
|
|
Then build:
|
|
|
|
```bash
|
|
npm install
|
|
npm run build
|
|
```
|
|
|
|
</Step>
|
|
<Step>
|
|
|
|
### Register it in `agent-orchestrator.yaml`
|
|
|
|
For local development, use `source: local`:
|
|
|
|
```yaml
|
|
plugins:
|
|
- name: pagerduty
|
|
source: local
|
|
path: ./pagerduty
|
|
|
|
notifiers:
|
|
pagerduty-urgent:
|
|
plugin: pagerduty
|
|
routing_key: "your-integration-key"
|
|
|
|
notificationRouting:
|
|
urgent: [pagerduty-urgent]
|
|
```
|
|
|
|
Once published to npm, switch to `source: registry` (or `source: npm`):
|
|
|
|
```yaml
|
|
plugins:
|
|
- name: pagerduty
|
|
source: registry
|
|
package: "ao-plugin-notifier-pagerduty"
|
|
version: "^1.0.0"
|
|
```
|
|
|
|
</Step>
|
|
</Steps>
|
|
|
|
---
|
|
|
|
## Slot interfaces at a glance
|
|
|
|
All interfaces are defined in `packages/core/src/types.ts` and exported from `@aoagents/ao-core`.
|
|
|
|
### Runtime
|
|
|
|
| Method | Signature | Required |
|
|
|--------|-----------|---------|
|
|
| `create` | `(config: RuntimeCreateConfig) => Promise<RuntimeHandle>` | yes |
|
|
| `destroy` | `(handle: RuntimeHandle) => Promise<void>` | yes |
|
|
| `sendMessage` | `(handle: RuntimeHandle, message: string) => Promise<void>` | yes |
|
|
| `getOutput` | `(handle: RuntimeHandle, lines?: number) => Promise<string>` | yes |
|
|
| `isAlive` | `(handle: RuntimeHandle) => Promise<boolean>` | yes |
|
|
| `getMetrics` | `(handle: RuntimeHandle) => Promise<RuntimeMetrics>` | optional |
|
|
| `getAttachInfo` | `(handle: RuntimeHandle) => Promise<AttachInfo>` | optional |
|
|
|
|
### Agent
|
|
|
|
Agent plugins have the richest interface. Methods are split into required and optional:
|
|
|
|
**Required:**
|
|
|
|
| Method | Purpose | Returns `null`? |
|
|
|--------|---------|----------------|
|
|
| `getLaunchCommand` | Shell command to start the agent | No |
|
|
| `getEnvironment` | Env vars for the process (must include `~/.ao/bin` in PATH) | No |
|
|
| `detectActivity` | Terminal-output activity classification (deprecated but required) | No |
|
|
| `getActivityState` | JSONL/API-based activity detection | Yes (if no data) |
|
|
| `isProcessRunning` | Check whether the process is still alive | No (returns `false`) |
|
|
| `getSessionInfo` | Extract summary, cost, session ID from agent data | Yes |
|
|
|
|
**Optional:**
|
|
|
|
| Method | Purpose | When to skip |
|
|
|--------|---------|-------------|
|
|
| `getRestoreCommand` | Resume a previous session | Agent has no resume capability |
|
|
| `setupWorkspaceHooks` | Install metadata hooks for PR tracking | Never — required for the dashboard |
|
|
| `postLaunchSetup` | Post-launch config | Only if no post-launch work is needed |
|
|
| `recordActivity` | Write terminal-derived activity to JSONL | Agent has native JSONL (Claude Code) |
|
|
|
|
<Callout type="warn">
|
|
`setupWorkspaceHooks` is marked optional in the TypeScript interface but is critical in practice. Without it, PRs created by your agent will never appear in the dashboard. See [Agent plugin specifics](#agent-plugin-specifics) for the two patterns (agent-native hooks vs PATH wrappers).
|
|
</Callout>
|
|
|
|
### Workspace
|
|
|
|
| Method | Signature | Required |
|
|
|--------|-----------|---------|
|
|
| `create` | `(config: WorkspaceCreateConfig) => Promise<WorkspaceInfo>` | yes |
|
|
| `destroy` | `(workspacePath: string) => Promise<void>` | yes |
|
|
| `list` | `(projectId: string) => Promise<WorkspaceInfo[]>` | yes |
|
|
| `postCreate` | `(info: WorkspaceInfo, project: ProjectConfig) => Promise<void>` | optional |
|
|
| `exists` | `(workspacePath: string) => Promise<boolean>` | optional |
|
|
| `restore` | `(config: WorkspaceCreateConfig, workspacePath: string) => Promise<WorkspaceInfo>` | optional |
|
|
|
|
### Tracker
|
|
|
|
| Method | Signature | Required |
|
|
|--------|-----------|---------|
|
|
| `getIssue` | `(identifier: string, project: ProjectConfig) => Promise<Issue>` | yes |
|
|
| `isCompleted` | `(identifier: string, project: ProjectConfig) => Promise<boolean>` | yes |
|
|
| `issueUrl` | `(identifier: string, project: ProjectConfig) => string` | yes |
|
|
| `branchName` | `(identifier: string, project: ProjectConfig) => string` | yes |
|
|
| `generatePrompt` | `(identifier: string, project: ProjectConfig) => Promise<string>` | yes |
|
|
| `issueLabel` | `(url: string, project: ProjectConfig) => string` | optional |
|
|
| `listIssues` | `(filters: IssueFilters, project: ProjectConfig) => Promise<Issue[]>` | optional |
|
|
| `updateIssue` | `(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise<void>` | optional |
|
|
| `createIssue` | `(input: CreateIssueInput, project: ProjectConfig) => Promise<Issue>` | optional |
|
|
|
|
### SCM
|
|
|
|
The richest interface — covers PR lifecycle, CI tracking, review tracking, and merge readiness.
|
|
|
|
**Required:** `detectPR`, `getPRState`, `mergePR`, `closePR`, `getCIChecks`, `getCISummary`, `getReviews`, `getReviewDecision`, `getPendingComments`, `getAutomatedComments`, `getMergeability`
|
|
|
|
**Optional:** `verifyWebhook`, `parseWebhook`, `resolvePR`, `assignPRToCurrentUser`, `checkoutPR`, `getPRSummary`, `enrichSessionsPRBatch`
|
|
|
|
### Notifier
|
|
|
|
| Method | Signature | Required |
|
|
|--------|-----------|---------|
|
|
| `notify` | `(event: OrchestratorEvent) => Promise<void>` | yes |
|
|
| `notifyWithActions` | `(event: OrchestratorEvent, actions: NotifyAction[]) => Promise<void>` | optional |
|
|
| `post` | `(message: string, context?: NotifyContext) => Promise<string \| null>` | optional |
|
|
|
|
### Terminal
|
|
|
|
| Method | Signature | Required |
|
|
|--------|-----------|---------|
|
|
| `openSession` | `(session: Session) => Promise<void>` | yes |
|
|
| `openAll` | `(sessions: Session[]) => Promise<void>` | yes |
|
|
| `isSessionOpen` | `(session: Session) => Promise<boolean>` | optional |
|
|
|
|
---
|
|
|
|
## Config validation in `create()`
|
|
|
|
Validate all config once at load time. Store validated values in a closure. Never re-validate inside individual methods.
|
|
|
|
```typescript
|
|
export function create(config?: Record<string, unknown>): Notifier {
|
|
// Resolve config or fall back to an environment variable.
|
|
const url = (config?.url as string | undefined) ?? process.env["WEBHOOK_URL"];
|
|
|
|
if (!url) {
|
|
// Warn for missing optional config — don't throw.
|
|
// Throw only when a required field is missing at method call time.
|
|
console.warn("[notifier-webhook] No url configured — notifications will be no-ops");
|
|
} else {
|
|
validateUrl(url, "notifier-webhook"); // throws on malformed URL
|
|
}
|
|
|
|
// Custom headers are optional — silently ignore non-string values.
|
|
const customHeaders: Record<string, string> = {};
|
|
const rawHeaders = config?.headers;
|
|
if (rawHeaders && typeof rawHeaders === "object" && !Array.isArray(rawHeaders)) {
|
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
if (typeof v === "string") customHeaders[k] = v;
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: "webhook",
|
|
async notify(event) {
|
|
if (!url) return;
|
|
await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
body: JSON.stringify({ type: "notification", event }),
|
|
});
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Loading paths
|
|
|
|
AO resolves plugins through four mechanisms, tried in this order:
|
|
|
|
### 1. Bundled
|
|
|
|
Plugins under `packages/plugins/` in the AO monorepo are registered automatically. No config is required.
|
|
|
|
### 2. Registry / npm (`source: registry` or `source: npm`)
|
|
|
|
An npm package. AO installs it into its internal plugin cache on first use and keeps the version pinned in `agent-orchestrator.yaml`.
|
|
|
|
```yaml
|
|
plugins:
|
|
- name: pagerduty
|
|
source: registry
|
|
package: "ao-plugin-notifier-pagerduty"
|
|
version: "^1.0.0"
|
|
```
|
|
|
|
Use `ao plugin install ao-plugin-notifier-pagerduty` to add this block automatically.
|
|
|
|
### 3. Local path (`source: local`)
|
|
|
|
A filesystem path. Useful during development. Path is resolved relative to `agent-orchestrator.yaml`.
|
|
|
|
```yaml
|
|
plugins:
|
|
- name: pagerduty
|
|
source: local
|
|
path: ./plugins/pagerduty
|
|
```
|
|
|
|
AO will import `dist/index.js` (falling back to `index.js`) from that directory.
|
|
|
|
### 4. Inline shortcut
|
|
|
|
For `tracker`, `scm`, and `notifier` blocks you can embed the `package` or `path` key directly instead of adding a separate `plugins[]` entry. `collectExternalPluginConfigs` in `packages/core/src/config.ts` auto-promotes these inline references to the `plugins[]` array at load time.
|
|
|
|
```yaml
|
|
projects:
|
|
myapp:
|
|
tracker:
|
|
package: "@acme/ao-plugin-tracker-jira"
|
|
version: "^1.0.0"
|
|
projectKey: "MYAPP"
|
|
```
|
|
|
|
You can also pass `plugin:` to assert that the loaded plugin's `manifest.name` must match a specific value:
|
|
|
|
```yaml
|
|
projects:
|
|
myapp:
|
|
tracker:
|
|
plugin: jira
|
|
package: "@acme/ao-plugin-tracker-jira"
|
|
version: "^1.0.0"
|
|
```
|
|
|
|
---
|
|
|
|
## `ao plugin` subcommands
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `ao plugin list` | List plugins from the bundled marketplace catalog. Add `--installed` to show only what's in your config. Filter by slot with `--type <slot>`. Add `--refresh` to pull the latest catalog from the registry. |
|
|
| `ao plugin search <query>` | Search the bundled catalog by name, package, description, or slot. |
|
|
| `ao plugin create [dir]` | Scaffold a new plugin package interactively. |
|
|
| `ao plugin install <reference>` | Install a plugin by marketplace ID, package name, or local path. Writes the entry to `agent-orchestrator.yaml`. |
|
|
| `ao plugin update [reference]` | Update an installer-managed plugin. Pass `--all` to update everything. |
|
|
| `ao plugin uninstall <reference>` | Remove a plugin from the config. |
|
|
|
|
---
|
|
|
|
## Core utilities available to plugins
|
|
|
|
All utilities are exported from `@aoagents/ao-core`. The source lives in `packages/core/src/index.ts`.
|
|
|
|
**Shell safety and HTTP:**
|
|
|
|
| Export | Purpose |
|
|
|--------|---------|
|
|
| `shellEscape` | Safely escape command-line arguments. Use for every argument passed to child processes. |
|
|
| `validateUrl` | Validate a webhook URL and throw a descriptive error on failure. |
|
|
|
|
**Activity detection (agent plugins):**
|
|
|
|
| Export | Purpose |
|
|
|--------|---------|
|
|
| `readLastJsonlEntry` | Efficiently read the last entry from an agent's native JSONL log. |
|
|
| `readLastActivityEntry` | Read the last entry from the AO activity JSONL (`{workspace}/.ao/activity.jsonl`). |
|
|
| `checkActivityLogState` | Extract `waiting_input` or `blocked` from an activity entry (with staleness cap). Returns `null` for other states. |
|
|
| `getActivityFallbackState` | Age-based decay fallback: converts an activity entry into `active` / `ready` / `idle` using entry timestamp. |
|
|
| `recordTerminalActivity` | Shared `recordActivity` implementation — classifies, deduplicates, and appends to the activity JSONL. |
|
|
| `classifyTerminalActivity` | Classify terminal output via a `detectActivity` function. |
|
|
| `appendActivityEntry` | Low-level JSONL append for activity entries. |
|
|
|
|
**Workspace setup (agent plugins):**
|
|
|
|
| Export | Purpose |
|
|
|--------|---------|
|
|
| `setupPathWrapperWorkspace` | Install `~/.ao/bin/gh` and `~/.ao/bin/git` PATH wrappers and write `.ao/AGENTS.md` in the workspace. Required for PATH-wrapper agents (Codex, Aider, OpenCode). |
|
|
| `buildAgentPath` | Prepend `~/.ao/bin` to a PATH string. |
|
|
| `normalizeAgentPermissionMode` | Normalize legacy permission mode aliases (e.g. `"skip"` → `"permissionless"`). |
|
|
|
|
**Constants:**
|
|
|
|
| Export | Value |
|
|
|--------|-------|
|
|
| `DEFAULT_READY_THRESHOLD_MS` | `300_000` (5 min) — ready → idle threshold |
|
|
| `DEFAULT_ACTIVE_WINDOW_MS` | `30_000` (30 s) — active → ready window |
|
|
| `ACTIVITY_INPUT_STALENESS_MS` | `300_000` (5 min) — waiting_input/blocked expiry |
|
|
| `PREFERRED_GH_PATH` | `/usr/local/bin/gh` |
|
|
| `CI_STATUS` | `{ PENDING, PASSING, FAILING, NONE }` |
|
|
| `ACTIVITY_STATE` | `{ ACTIVE, READY, IDLE, WAITING_INPUT, BLOCKED, EXITED }` |
|
|
| `SESSION_STATUS` | Full session status constant map |
|
|
|
|
**Types:** `Session`, `ProjectConfig`, `RuntimeHandle` (and everything else in `types.ts`).
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
Tests live in `src/__tests__/index.test.ts` and run with Vitest.
|
|
|
|
Minimum test coverage for every plugin:
|
|
|
|
- Manifest values (`name`, `slot`, `version`)
|
|
- Shape of the object returned by `create()`
|
|
- Every public method: happy path, not-found case (returns `null`), error path (throws)
|
|
- Config validation: missing required field, invalid value
|
|
|
|
Mock everything external — CLI binaries, HTTP calls, file I/O:
|
|
|
|
```typescript
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import pluginModule from "../index.js";
|
|
|
|
vi.mock("node:fs/promises", () => ({ readFile: vi.fn() }));
|
|
|
|
describe("manifest", () => {
|
|
it("has correct slot and name", () => {
|
|
expect(pluginModule.manifest.slot).toBe("notifier");
|
|
expect(pluginModule.manifest.name).toBe("pagerduty");
|
|
});
|
|
});
|
|
|
|
describe("create()", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("throws when routing_key is missing", () => {
|
|
expect(() => pluginModule.create({})).toThrow("routing_key");
|
|
});
|
|
|
|
it("returns a notifier with notify()", () => {
|
|
const notifier = pluginModule.create({ routing_key: "test-key" });
|
|
expect(typeof notifier.notify).toBe("function");
|
|
});
|
|
});
|
|
```
|
|
|
|
For agent plugins, the required `getActivityState` tests are listed in the CLAUDE.md "Agent Plugin Implementation Standards" section.
|
|
|
|
---
|
|
|
|
## Common pitfalls
|
|
|
|
| Pitfall | Correct approach |
|
|
|---------|-----------------|
|
|
| Hardcoded secrets (API keys, tokens) | Read from `process.env`, throw if required and missing |
|
|
| Shell injection | Use `shellEscape()` for every argument passed to child processes |
|
|
| Reading large log files in full | Use `readLastJsonlEntry()` or stream the tail |
|
|
| Config validation inside methods | Validate once in `create()`, capture validated values in a closure |
|
|
| Silently swallowing errors | Either throw with `{ cause: err }` or return `null` — never no-op |
|
|
|
|
---
|
|
|
|
## Agent plugin specifics
|
|
|
|
Agent plugins have significantly more surface area than other slot types. The most critical method is `getActivityState`, which powers the dashboard, stuck-detection, and the lifecycle manager's reaction engine.
|
|
|
|
<Callout type="warn">
|
|
**Step 4 of the `getActivityState` cascade (`getActivityFallbackState`) is mandatory.** If you skip it, `getActivityState` returns `null` whenever the native API is unavailable (binary not found, session lookup failed, timeout). The dashboard shows no activity state and stuck-detection stops working entirely. This was a real production bug in the OpenCode plugin.
|
|
</Callout>
|
|
|
|
The required 4-step cascade:
|
|
|
|
```typescript
|
|
async getActivityState(session, readyThresholdMs?): Promise<ActivityDetection | null> {
|
|
const threshold = readyThresholdMs ?? DEFAULT_READY_THRESHOLD_MS;
|
|
|
|
// 1. PROCESS CHECK — always first
|
|
const running = await this.isProcessRunning(session.runtimeHandle!);
|
|
if (!running) return { state: "exited", timestamp: new Date() };
|
|
|
|
// 2. ACTIONABLE STATES — waiting_input / blocked from JSONL
|
|
const activityResult = await readLastActivityEntry(session.workspacePath);
|
|
const actionable = checkActivityLogState(activityResult);
|
|
if (actionable) return actionable;
|
|
|
|
// 3. NATIVE SIGNAL — agent-specific API (preferred when available)
|
|
try {
|
|
const nativeTimestamp = await this.getNativeTimestamp(session);
|
|
if (nativeTimestamp) {
|
|
const age = Date.now() - nativeTimestamp.getTime();
|
|
const activeWindowMs = Math.min(DEFAULT_ACTIVE_WINDOW_MS, threshold);
|
|
const state = age < activeWindowMs ? "active" : age < threshold ? "ready" : "idle";
|
|
return { state, timestamp: nativeTimestamp };
|
|
}
|
|
} catch {
|
|
// fall through to JSONL fallback
|
|
}
|
|
|
|
// 4. JSONL ENTRY FALLBACK — mandatory safety net
|
|
const activeWindowMs = Math.min(DEFAULT_ACTIVE_WINDOW_MS, threshold);
|
|
const fallback = getActivityFallbackState(activityResult, activeWindowMs, threshold);
|
|
if (fallback) return fallback;
|
|
|
|
// 5. No data at all
|
|
return null;
|
|
}
|
|
```
|
|
|
|
For agents that don't have a native session API (most new agents), skip step 3 and rely entirely on the AO activity JSONL written by `recordActivity`. The full specification — including hook patterns, `isProcessRunning` requirements, and the 7 required test cases — is in the "Agent Plugin Implementation Standards" section of `CLAUDE.md` in the project root.
|
|
|
|
---
|
|
|
|
## Next steps
|
|
|
|
<Cards>
|
|
<Card title="Architecture" description="Understand the plugin registry, session lifecycle, and how slots wire together." href="/docs/architecture" />
|
|
<Card title="Plugin catalog" description="Browse every bundled plugin grouped by slot." href="/docs/plugins" />
|
|
<Card title="Configuration reference" description="How plugins are referenced in agent-orchestrator.yaml." href="/docs/configuration" />
|
|
</Cards>
|