@eminent337/aery 0.1.44 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/compaction.md +18 -18
- package/docs/custom-provider.md +21 -21
- package/docs/extensions.md +104 -102
- package/docs/json.md +5 -5
- package/docs/keybindings.md +3 -3
- package/docs/models.md +2 -2
- package/docs/packages.md +21 -21
- package/docs/prompt-templates.md +1 -1
- package/docs/providers.md +3 -1
- package/docs/rpc.md +1 -1
- package/docs/sdk.md +7 -5
- package/docs/session.md +4 -4
- package/docs/skills.md +2 -2
- package/docs/terminal-setup.md +1 -1
- package/docs/themes.md +1 -1
- package/docs/tree.md +1 -1
- package/docs/tui.md +7 -7
- package/package.json +1 -1
package/docs/extensions.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
>
|
|
1
|
+
> The agent can create extensions. Ask it to build one for your use case.
|
|
2
2
|
|
|
3
3
|
# Extensions
|
|
4
4
|
|
|
5
|
-
Extensions are TypeScript modules that extend
|
|
5
|
+
Extensions are TypeScript modules that extend the agent's behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.
|
|
6
|
+
|
|
7
|
+
> This extension system is shared across the open-source AI coding agent ecosystem. Extensions written for [pi](https://github.com/badlogic/pi-mono) are compatible with [Aery](https://github.com/eminent337/aery). For more extension examples, see [openclaude](https://github.com/Gitlawb/openclaude) and [opencode](https://github.com/sst/opencode) which use similar plugin architectures.
|
|
6
8
|
|
|
7
9
|
> **Placement for /reload:** Put extensions in `~/.aery/agent/extensions/` (global) or `.aery/extensions/` (project-local) for auto-discovery. Use `aery -e ./path.ts` only for quick tests. Extensions in auto-discovered locations can be hot-reloaded with `/reload`.
|
|
8
10
|
|
|
9
11
|
**Key capabilities:**
|
|
10
|
-
- **Custom tools** - Register tools the LLM can call via `
|
|
12
|
+
- **Custom tools** - Register tools the LLM can call via `pi.registerTool()`
|
|
11
13
|
- **Event interception** - Block or modify tool calls, inject context, customize compaction
|
|
12
14
|
- **User interaction** - Prompt users via `ctx.ui` (select, confirm, input, notify)
|
|
13
15
|
- **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()` for complex interactions
|
|
14
|
-
- **Custom commands** - Register commands like `/mycommand` via `
|
|
16
|
+
- **Custom commands** - Register commands like `/mycommand` via `pi.registerCommand()`
|
|
15
17
|
- **Session persistence** - Store state that survives restarts via `aery.appendEntry()`
|
|
16
18
|
- **Custom rendering** - Control how tool calls/results and messages appear in TUI
|
|
17
19
|
|
|
@@ -61,11 +63,11 @@ import { Type } from "@sinclair/typebox";
|
|
|
61
63
|
|
|
62
64
|
export default function (aery: ExtensionAPI) {
|
|
63
65
|
// React to events
|
|
64
|
-
|
|
66
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
65
67
|
ctx.ui.notify("Extension loaded!", "info");
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
69
71
|
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
|
|
70
72
|
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
|
|
71
73
|
if (!ok) return { block: true, reason: "Blocked by user" };
|
|
@@ -73,7 +75,7 @@ export default function (aery: ExtensionAPI) {
|
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
// Register a custom tool
|
|
76
|
-
|
|
78
|
+
pi.registerTool({
|
|
77
79
|
name: "greet",
|
|
78
80
|
label: "Greet",
|
|
79
81
|
description: "Greet someone by name",
|
|
@@ -89,7 +91,7 @@ export default function (aery: ExtensionAPI) {
|
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
// Register a command
|
|
92
|
-
|
|
94
|
+
pi.registerCommand("hello", {
|
|
93
95
|
description: "Say hello",
|
|
94
96
|
handler: async (args, ctx) => {
|
|
95
97
|
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
|
|
@@ -132,7 +134,7 @@ Additional paths via `settings.json`:
|
|
|
132
134
|
}
|
|
133
135
|
```
|
|
134
136
|
|
|
135
|
-
To share extensions via npm or git as
|
|
137
|
+
To share extensions via npm or git as pi packages, see [packages.md](packages.md).
|
|
136
138
|
|
|
137
139
|
## Available Imports
|
|
138
140
|
|
|
@@ -145,7 +147,7 @@ To share extensions via npm or git as Aery packages, see [packages.md](packages.
|
|
|
145
147
|
|
|
146
148
|
npm dependencies work too. Add a `package.json` next to your extension (or in a parent directory), run `npm install`, and imports from `node_modules/` are resolved automatically.
|
|
147
149
|
|
|
148
|
-
For distributed
|
|
150
|
+
For distributed pi packages installed with `aery install` (npm or git), runtime deps must be in `dependencies`. Package installation uses production installs (`npm install --omit=dev`), so `devDependencies` are not available at runtime.
|
|
149
151
|
|
|
150
152
|
Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
|
|
151
153
|
|
|
@@ -158,7 +160,7 @@ import type { ExtensionAPI } from "@eminent337/aery";
|
|
|
158
160
|
|
|
159
161
|
export default function (aery: ExtensionAPI) {
|
|
160
162
|
// Subscribe to events
|
|
161
|
-
|
|
163
|
+
pi.on("event_name", async (event, ctx) => {
|
|
162
164
|
// ctx.ui for user interaction
|
|
163
165
|
const ok = await ctx.ui.confirm("Title", "Are you sure?");
|
|
164
166
|
ctx.ui.notify("Done!", "success");
|
|
@@ -167,8 +169,8 @@ export default function (aery: ExtensionAPI) {
|
|
|
167
169
|
});
|
|
168
170
|
|
|
169
171
|
// Register tools, commands, shortcuts, flags
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
pi.registerTool({ ... });
|
|
173
|
+
pi.registerCommand("name", { ... });
|
|
172
174
|
aery.registerShortcut("ctrl+x", { ... });
|
|
173
175
|
aery.registerFlag("my-flag", { ... });
|
|
174
176
|
}
|
|
@@ -176,7 +178,7 @@ export default function (aery: ExtensionAPI) {
|
|
|
176
178
|
|
|
177
179
|
Extensions are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
|
|
178
180
|
|
|
179
|
-
If the factory returns a `Promise`,
|
|
181
|
+
If the factory returns a `Promise`, pi awaits it before continuing startup. That means async initialization completes before `session_start`, before `resources_discover`, and before provider registrations queued via `pi.registerProvider()` are flushed.
|
|
180
182
|
|
|
181
183
|
### Async factory functions
|
|
182
184
|
|
|
@@ -196,7 +198,7 @@ export default async function (aery: ExtensionAPI) {
|
|
|
196
198
|
}>;
|
|
197
199
|
};
|
|
198
200
|
|
|
199
|
-
|
|
201
|
+
pi.registerProvider("local-openai", {
|
|
200
202
|
baseUrl: "http://localhost:1234/v1",
|
|
201
203
|
apiKey: "LOCAL_OPENAI_API_KEY",
|
|
202
204
|
api: "openai-completions",
|
|
@@ -267,7 +269,7 @@ Run `npm install` in the extension directory, then imports from `node_modules/`
|
|
|
267
269
|
### Lifecycle Overview
|
|
268
270
|
|
|
269
271
|
```
|
|
270
|
-
|
|
272
|
+
pi starts
|
|
271
273
|
│
|
|
272
274
|
├─► session_start { reason: "startup" }
|
|
273
275
|
└─► resources_discover { reason: "startup" }
|
|
@@ -337,7 +339,7 @@ Fired after `session_start` so extensions can contribute additional skill, promp
|
|
|
337
339
|
The startup path uses `reason: "startup"`. Reload uses `reason: "reload"`.
|
|
338
340
|
|
|
339
341
|
```typescript
|
|
340
|
-
|
|
342
|
+
pi.on("resources_discover", async (event, _ctx) => {
|
|
341
343
|
// event.cwd - current working directory
|
|
342
344
|
// event.reason - "startup" | "reload"
|
|
343
345
|
return {
|
|
@@ -357,7 +359,7 @@ See [session.md](session.md) for session storage internals and the SessionManage
|
|
|
357
359
|
Fired when a session is started, loaded, or reloaded.
|
|
358
360
|
|
|
359
361
|
```typescript
|
|
360
|
-
|
|
362
|
+
pi.on("session_start", async (event, ctx) => {
|
|
361
363
|
// event.reason - "startup" | "reload" | "new" | "resume" | "fork"
|
|
362
364
|
// event.previousSessionFile - present for "new", "resume", and "fork"
|
|
363
365
|
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
|
|
@@ -369,7 +371,7 @@ aery.on("session_start", async (event, ctx) => {
|
|
|
369
371
|
Fired before starting a new session (`/new`) or switching sessions (`/resume`).
|
|
370
372
|
|
|
371
373
|
```typescript
|
|
372
|
-
|
|
374
|
+
pi.on("session_before_switch", async (event, ctx) => {
|
|
373
375
|
// event.reason - "new" or "resume"
|
|
374
376
|
// event.targetSessionFile - session we're switching to (only for "resume")
|
|
375
377
|
|
|
@@ -380,7 +382,7 @@ aery.on("session_before_switch", async (event, ctx) => {
|
|
|
380
382
|
});
|
|
381
383
|
```
|
|
382
384
|
|
|
383
|
-
After a successful switch or new-session action,
|
|
385
|
+
After a successful switch or new-session action, pi emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "new" | "resume"` and `previousSessionFile`.
|
|
384
386
|
Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
|
|
385
387
|
|
|
386
388
|
#### session_before_fork
|
|
@@ -388,7 +390,7 @@ Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `
|
|
|
388
390
|
Fired when forking via `/fork` or cloning via `/clone`.
|
|
389
391
|
|
|
390
392
|
```typescript
|
|
391
|
-
|
|
393
|
+
pi.on("session_before_fork", async (event, ctx) => {
|
|
392
394
|
// event.entryId - ID of the selected entry
|
|
393
395
|
// event.position - "before" for /fork, "at" for /clone
|
|
394
396
|
return { cancel: true }; // Cancel fork/clone
|
|
@@ -397,7 +399,7 @@ aery.on("session_before_fork", async (event, ctx) => {
|
|
|
397
399
|
});
|
|
398
400
|
```
|
|
399
401
|
|
|
400
|
-
After a successful fork or clone,
|
|
402
|
+
After a successful fork or clone, pi emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "fork"` and `previousSessionFile`.
|
|
401
403
|
Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
|
|
402
404
|
|
|
403
405
|
#### session_before_compact / session_compact
|
|
@@ -405,7 +407,7 @@ Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `
|
|
|
405
407
|
Fired on compaction. See [compaction.md](compaction.md) for details.
|
|
406
408
|
|
|
407
409
|
```typescript
|
|
408
|
-
|
|
410
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
409
411
|
const { preparation, branchEntries, customInstructions, signal } = event;
|
|
410
412
|
|
|
411
413
|
// Cancel:
|
|
@@ -421,7 +423,7 @@ aery.on("session_before_compact", async (event, ctx) => {
|
|
|
421
423
|
};
|
|
422
424
|
});
|
|
423
425
|
|
|
424
|
-
|
|
426
|
+
pi.on("session_compact", async (event, ctx) => {
|
|
425
427
|
// event.compactionEntry - the saved compaction
|
|
426
428
|
// event.fromExtension - whether extension provided it
|
|
427
429
|
});
|
|
@@ -432,14 +434,14 @@ aery.on("session_compact", async (event, ctx) => {
|
|
|
432
434
|
Fired on `/tree` navigation. See [tree.md](tree.md) for tree navigation concepts.
|
|
433
435
|
|
|
434
436
|
```typescript
|
|
435
|
-
|
|
437
|
+
pi.on("session_before_tree", async (event, ctx) => {
|
|
436
438
|
const { preparation, signal } = event;
|
|
437
439
|
return { cancel: true };
|
|
438
440
|
// OR provide custom summary:
|
|
439
441
|
return { summary: { summary: "...", details: {} } };
|
|
440
442
|
});
|
|
441
443
|
|
|
442
|
-
|
|
444
|
+
pi.on("session_tree", async (event, ctx) => {
|
|
443
445
|
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
|
|
444
446
|
});
|
|
445
447
|
```
|
|
@@ -449,7 +451,7 @@ aery.on("session_tree", async (event, ctx) => {
|
|
|
449
451
|
Fired on exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM).
|
|
450
452
|
|
|
451
453
|
```typescript
|
|
452
|
-
|
|
454
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
453
455
|
// Cleanup, save state, etc.
|
|
454
456
|
});
|
|
455
457
|
```
|
|
@@ -461,7 +463,7 @@ aery.on("session_shutdown", async (_event, ctx) => {
|
|
|
461
463
|
Fired after user submits prompt, before agent loop. Can inject a message and/or modify the system prompt.
|
|
462
464
|
|
|
463
465
|
```typescript
|
|
464
|
-
|
|
466
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
465
467
|
// event.prompt - user's prompt text
|
|
466
468
|
// event.images - attached images (if any)
|
|
467
469
|
// event.systemPrompt - current system prompt
|
|
@@ -484,9 +486,9 @@ aery.on("before_agent_start", async (event, ctx) => {
|
|
|
484
486
|
Fired once per user prompt.
|
|
485
487
|
|
|
486
488
|
```typescript
|
|
487
|
-
|
|
489
|
+
pi.on("agent_start", async (_event, ctx) => {});
|
|
488
490
|
|
|
489
|
-
|
|
491
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
490
492
|
// event.messages - messages from this prompt
|
|
491
493
|
});
|
|
492
494
|
```
|
|
@@ -496,11 +498,11 @@ aery.on("agent_end", async (event, ctx) => {
|
|
|
496
498
|
Fired for each turn (one LLM response + tool calls).
|
|
497
499
|
|
|
498
500
|
```typescript
|
|
499
|
-
|
|
501
|
+
pi.on("turn_start", async (event, ctx) => {
|
|
500
502
|
// event.turnIndex, event.timestamp
|
|
501
503
|
});
|
|
502
504
|
|
|
503
|
-
|
|
505
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
504
506
|
// event.turnIndex, event.message, event.toolResults
|
|
505
507
|
});
|
|
506
508
|
```
|
|
@@ -513,16 +515,16 @@ Fired for message lifecycle updates.
|
|
|
513
515
|
- `message_update` fires for assistant streaming updates.
|
|
514
516
|
|
|
515
517
|
```typescript
|
|
516
|
-
|
|
518
|
+
pi.on("message_start", async (event, ctx) => {
|
|
517
519
|
// event.message
|
|
518
520
|
});
|
|
519
521
|
|
|
520
|
-
|
|
522
|
+
pi.on("message_update", async (event, ctx) => {
|
|
521
523
|
// event.message
|
|
522
524
|
// event.assistantMessageEvent (token-by-token stream event)
|
|
523
525
|
});
|
|
524
526
|
|
|
525
|
-
|
|
527
|
+
pi.on("message_end", async (event, ctx) => {
|
|
526
528
|
// event.message
|
|
527
529
|
});
|
|
528
530
|
```
|
|
@@ -537,15 +539,15 @@ In parallel tool mode:
|
|
|
537
539
|
- `tool_execution_end` is emitted in assistant source order, matching final tool result message order
|
|
538
540
|
|
|
539
541
|
```typescript
|
|
540
|
-
|
|
542
|
+
pi.on("tool_execution_start", async (event, ctx) => {
|
|
541
543
|
// event.toolCallId, event.toolName, event.args
|
|
542
544
|
});
|
|
543
545
|
|
|
544
|
-
|
|
546
|
+
pi.on("tool_execution_update", async (event, ctx) => {
|
|
545
547
|
// event.toolCallId, event.toolName, event.args, event.partialResult
|
|
546
548
|
});
|
|
547
549
|
|
|
548
|
-
|
|
550
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
549
551
|
// event.toolCallId, event.toolName, event.result, event.isError
|
|
550
552
|
});
|
|
551
553
|
```
|
|
@@ -555,7 +557,7 @@ aery.on("tool_execution_end", async (event, ctx) => {
|
|
|
555
557
|
Fired before each LLM call. Modify messages non-destructively. See [session.md](session.md) for message types.
|
|
556
558
|
|
|
557
559
|
```typescript
|
|
558
|
-
|
|
560
|
+
pi.on("context", async (event, ctx) => {
|
|
559
561
|
// event.messages - deep copy, safe to modify
|
|
560
562
|
const filtered = event.messages.filter(m => !shouldPrune(m));
|
|
561
563
|
return { messages: filtered };
|
|
@@ -567,7 +569,7 @@ aery.on("context", async (event, ctx) => {
|
|
|
567
569
|
Fired after the provider-specific payload is built, right before the request is sent. Handlers run in extension load order. Returning `undefined` keeps the payload unchanged. Returning any other value replaces the payload for later handlers and for the actual request.
|
|
568
570
|
|
|
569
571
|
```typescript
|
|
570
|
-
|
|
572
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
571
573
|
console.log(JSON.stringify(event.payload, null, 2));
|
|
572
574
|
|
|
573
575
|
// Optional: replace payload
|
|
@@ -582,7 +584,7 @@ This is mainly useful for debugging provider serialization and cache behavior.
|
|
|
582
584
|
Fired after an HTTP response is received and before its stream body is consumed. Handlers run in extension load order.
|
|
583
585
|
|
|
584
586
|
```typescript
|
|
585
|
-
|
|
587
|
+
pi.on("after_provider_response", (event, ctx) => {
|
|
586
588
|
// event.status - HTTP status code
|
|
587
589
|
// event.headers - normalized response headers
|
|
588
590
|
if (event.status === 429) {
|
|
@@ -600,7 +602,7 @@ Header availability depends on provider and transport. Providers that abstract H
|
|
|
600
602
|
Fired when the model changes via `/model` command, model cycling (`Ctrl+P`), or session restore.
|
|
601
603
|
|
|
602
604
|
```typescript
|
|
603
|
-
|
|
605
|
+
pi.on("model_select", async (event, ctx) => {
|
|
604
606
|
// event.model - newly selected model
|
|
605
607
|
// event.previousModel - previous model (undefined if first selection)
|
|
606
608
|
// event.source - "set" | "cycle" | "restore"
|
|
@@ -622,7 +624,7 @@ Use this to update UI elements (status bars, footers) or perform model-specific
|
|
|
622
624
|
|
|
623
625
|
Fired after `tool_execution_start`, before the tool executes. **Can block.** Use `isToolCallEventType` to narrow and get typed inputs.
|
|
624
626
|
|
|
625
|
-
Before `tool_call` runs,
|
|
627
|
+
Before `tool_call` runs, pi waits for previously emitted Agent events to finish draining through `AgentSession`. This means `ctx.sessionManager` is up to date through the current assistant tool-calling message.
|
|
626
628
|
|
|
627
629
|
In the default parallel tool execution mode, sibling tool calls from the same assistant message are preflighted sequentially, then executed concurrently. `tool_call` is not guaranteed to see sibling tool results from that same assistant message in `ctx.sessionManager`.
|
|
628
630
|
|
|
@@ -637,7 +639,7 @@ Behavior guarantees:
|
|
|
637
639
|
```typescript
|
|
638
640
|
import { isToolCallEventType } from "@eminent337/aery";
|
|
639
641
|
|
|
640
|
-
|
|
642
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
641
643
|
// event.toolName - "bash", "read", "write", "edit", etc.
|
|
642
644
|
// event.toolCallId
|
|
643
645
|
// event.input - tool parameters (mutable)
|
|
@@ -674,7 +676,7 @@ Use `isToolCallEventType` with explicit type parameters:
|
|
|
674
676
|
import { isToolCallEventType } from "@eminent337/aery";
|
|
675
677
|
import type { MyToolInput } from "my-extension";
|
|
676
678
|
|
|
677
|
-
|
|
679
|
+
pi.on("tool_call", (event) => {
|
|
678
680
|
if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
|
|
679
681
|
event.input.action; // typed
|
|
680
682
|
}
|
|
@@ -695,7 +697,7 @@ Use `ctx.signal` for nested async work inside the handler. This lets Esc cancel
|
|
|
695
697
|
```typescript
|
|
696
698
|
import { isBashToolResult } from "@eminent337/aery";
|
|
697
699
|
|
|
698
|
-
|
|
700
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
699
701
|
// event.toolName, event.toolCallId, event.input
|
|
700
702
|
// event.content, event.details, event.isError
|
|
701
703
|
|
|
@@ -723,7 +725,7 @@ Fired when user executes `!` or `!!` commands. **Can intercept.**
|
|
|
723
725
|
```typescript
|
|
724
726
|
import { createLocalBashOperations } from "@eminent337/aery";
|
|
725
727
|
|
|
726
|
-
|
|
728
|
+
pi.on("user_bash", (event, ctx) => {
|
|
727
729
|
// event.command - the bash command
|
|
728
730
|
// event.excludeFromContext - true if !! prefix
|
|
729
731
|
// event.cwd - working directory
|
|
@@ -731,7 +733,7 @@ aery.on("user_bash", (event, ctx) => {
|
|
|
731
733
|
// Option 1: Provide custom operations (e.g., SSH)
|
|
732
734
|
return { operations: remoteBashOps };
|
|
733
735
|
|
|
734
|
-
// Option 2: Wrap
|
|
736
|
+
// Option 2: Wrap pi's built-in local bash backend
|
|
735
737
|
const local = createLocalBashOperations();
|
|
736
738
|
return {
|
|
737
739
|
operations: {
|
|
@@ -760,7 +762,7 @@ Fired when user input is received, after extension commands are checked but befo
|
|
|
760
762
|
5. Agent processing begins (`before_agent_start`, etc.)
|
|
761
763
|
|
|
762
764
|
```typescript
|
|
763
|
-
|
|
765
|
+
pi.on("input", async (event, ctx) => {
|
|
764
766
|
// event.text - raw input (before skill/template expansion)
|
|
765
767
|
// event.images - attached images, if any
|
|
766
768
|
// event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
|
|
@@ -836,10 +838,10 @@ Use this for abort-aware nested work started by extension handlers, for example:
|
|
|
836
838
|
- file or process helpers that accept `AbortSignal`
|
|
837
839
|
|
|
838
840
|
`ctx.signal` is typically defined during active turn events such as `tool_call`, `tool_result`, `message_update`, and `turn_end`.
|
|
839
|
-
It is usually `undefined` in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while
|
|
841
|
+
It is usually `undefined` in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while pi is idle.
|
|
840
842
|
|
|
841
843
|
```typescript
|
|
842
|
-
|
|
844
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
843
845
|
const response = await fetch("https://example.com/api", {
|
|
844
846
|
method: "POST",
|
|
845
847
|
body: JSON.stringify(event),
|
|
@@ -857,7 +859,7 @@ Control flow helpers.
|
|
|
857
859
|
|
|
858
860
|
### ctx.shutdown()
|
|
859
861
|
|
|
860
|
-
Request a graceful shutdown of
|
|
862
|
+
Request a graceful shutdown of pi.
|
|
861
863
|
|
|
862
864
|
- **Interactive mode:** Deferred until the agent becomes idle (after processing all queued steering and follow-up messages).
|
|
863
865
|
- **RPC mode:** Deferred until the next idle state (after completing the current command response, when waiting for the next command).
|
|
@@ -866,7 +868,7 @@ Request a graceful shutdown of Aery.
|
|
|
866
868
|
Emits `session_shutdown` event to all extensions before exiting. Available in all contexts (event handlers, tools, commands, shortcuts).
|
|
867
869
|
|
|
868
870
|
```typescript
|
|
869
|
-
|
|
871
|
+
pi.on("tool_call", (event, ctx) => {
|
|
870
872
|
if (isFatal(event.input)) {
|
|
871
873
|
ctx.shutdown();
|
|
872
874
|
}
|
|
@@ -905,7 +907,7 @@ ctx.compact({
|
|
|
905
907
|
Returns the current effective system prompt. This includes any modifications made by `before_agent_start` handlers for the current turn.
|
|
906
908
|
|
|
907
909
|
```typescript
|
|
908
|
-
|
|
910
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
909
911
|
const prompt = ctx.getSystemPrompt();
|
|
910
912
|
console.log(`System prompt length: ${prompt.length}`);
|
|
911
913
|
});
|
|
@@ -920,7 +922,7 @@ Command handlers receive `ExtensionCommandContext`, which extends `ExtensionCont
|
|
|
920
922
|
Wait for the agent to finish streaming:
|
|
921
923
|
|
|
922
924
|
```typescript
|
|
923
|
-
|
|
925
|
+
pi.registerCommand("my-cmd", {
|
|
924
926
|
handler: async (args, ctx) => {
|
|
925
927
|
await ctx.waitForIdle();
|
|
926
928
|
// Agent is now idle, safe to modify session
|
|
@@ -1004,7 +1006,7 @@ To discover available sessions, use the static `SessionManager.list()` or `Sessi
|
|
|
1004
1006
|
```typescript
|
|
1005
1007
|
import { SessionManager } from "@eminent337/aery";
|
|
1006
1008
|
|
|
1007
|
-
|
|
1009
|
+
pi.registerCommand("switch", {
|
|
1008
1010
|
description: "Switch to another session",
|
|
1009
1011
|
handler: async (args, ctx) => {
|
|
1010
1012
|
const sessions = await SessionManager.list(ctx.cwd);
|
|
@@ -1025,7 +1027,7 @@ aery.registerCommand("switch", {
|
|
|
1025
1027
|
Run the same reload flow as `/reload`.
|
|
1026
1028
|
|
|
1027
1029
|
```typescript
|
|
1028
|
-
|
|
1030
|
+
pi.registerCommand("reload-runtime", {
|
|
1029
1031
|
description: "Reload extensions, skills, prompts, and themes",
|
|
1030
1032
|
handler: async (_args, ctx) => {
|
|
1031
1033
|
await ctx.reload();
|
|
@@ -1053,7 +1055,7 @@ import type { ExtensionAPI } from "@eminent337/aery";
|
|
|
1053
1055
|
import { Type } from "@sinclair/typebox";
|
|
1054
1056
|
|
|
1055
1057
|
export default function (aery: ExtensionAPI) {
|
|
1056
|
-
|
|
1058
|
+
pi.registerCommand("reload-runtime", {
|
|
1057
1059
|
description: "Reload extensions, skills, prompts, and themes",
|
|
1058
1060
|
handler: async (_args, ctx) => {
|
|
1059
1061
|
await ctx.reload();
|
|
@@ -1061,13 +1063,13 @@ export default function (aery: ExtensionAPI) {
|
|
|
1061
1063
|
},
|
|
1062
1064
|
});
|
|
1063
1065
|
|
|
1064
|
-
|
|
1066
|
+
pi.registerTool({
|
|
1065
1067
|
name: "reload_runtime",
|
|
1066
1068
|
label: "Reload Runtime",
|
|
1067
1069
|
description: "Reload extensions, skills, prompts, and themes",
|
|
1068
1070
|
parameters: Type.Object({}),
|
|
1069
1071
|
async execute() {
|
|
1070
|
-
|
|
1072
|
+
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
|
|
1071
1073
|
return {
|
|
1072
1074
|
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
|
|
1073
1075
|
};
|
|
@@ -1078,15 +1080,15 @@ export default function (aery: ExtensionAPI) {
|
|
|
1078
1080
|
|
|
1079
1081
|
## ExtensionAPI Methods
|
|
1080
1082
|
|
|
1081
|
-
###
|
|
1083
|
+
### pi.on(event, handler)
|
|
1082
1084
|
|
|
1083
1085
|
Subscribe to events. See [Events](#events) for event types and return values.
|
|
1084
1086
|
|
|
1085
|
-
###
|
|
1087
|
+
### pi.registerTool(definition)
|
|
1086
1088
|
|
|
1087
1089
|
Register a custom tool callable by the LLM. See [Custom Tools](#custom-tools) for full details.
|
|
1088
1090
|
|
|
1089
|
-
`
|
|
1091
|
+
`pi.registerTool()` works both during extension load and after startup. You can call it inside `session_start`, command handlers, or other event handlers. New tools are refreshed immediately in the same session, so they appear in `aery.getAllTools()` and are callable by the LLM without `/reload`.
|
|
1090
1092
|
|
|
1091
1093
|
Use `aery.setActiveTools()` to enable or disable tools (including dynamically added tools) at runtime.
|
|
1092
1094
|
|
|
@@ -1098,7 +1100,7 @@ See [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) for a full examp
|
|
|
1098
1100
|
import { Type } from "@sinclair/typebox";
|
|
1099
1101
|
import { StringEnum } from "@eminent337/aery-ai";
|
|
1100
1102
|
|
|
1101
|
-
|
|
1103
|
+
pi.registerTool({
|
|
1102
1104
|
name: "my_tool",
|
|
1103
1105
|
label: "My Tool",
|
|
1104
1106
|
description: "What this tool does",
|
|
@@ -1154,23 +1156,23 @@ aery.sendMessage({
|
|
|
1154
1156
|
- `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
|
|
1155
1157
|
- `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
|
|
1156
1158
|
|
|
1157
|
-
###
|
|
1159
|
+
### pi.sendUserMessage(content, options?)
|
|
1158
1160
|
|
|
1159
1161
|
Send a user message to the agent. Unlike `sendMessage()` which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn.
|
|
1160
1162
|
|
|
1161
1163
|
```typescript
|
|
1162
1164
|
// Simple text message
|
|
1163
|
-
|
|
1165
|
+
pi.sendUserMessage("What is 2+2?");
|
|
1164
1166
|
|
|
1165
1167
|
// With content array (text + images)
|
|
1166
|
-
|
|
1168
|
+
pi.sendUserMessage([
|
|
1167
1169
|
{ type: "text", text: "Describe this image:" },
|
|
1168
1170
|
{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
|
|
1169
1171
|
]);
|
|
1170
1172
|
|
|
1171
1173
|
// During streaming - must specify delivery mode
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
+
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
|
|
1175
|
+
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });
|
|
1174
1176
|
```
|
|
1175
1177
|
|
|
1176
1178
|
**Options:**
|
|
@@ -1190,7 +1192,7 @@ Persist extension state (does NOT participate in LLM context).
|
|
|
1190
1192
|
aery.appendEntry("my-state", { count: 42 });
|
|
1191
1193
|
|
|
1192
1194
|
// Restore on reload
|
|
1193
|
-
|
|
1195
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1194
1196
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
1195
1197
|
if (entry.type === "custom" && entry.customType === "my-state") {
|
|
1196
1198
|
// Reconstruct from entry.data
|
|
@@ -1235,14 +1237,14 @@ const label = ctx.sessionManager.getLabel(entryId);
|
|
|
1235
1237
|
|
|
1236
1238
|
Labels persist in the session and survive restarts. Use them to mark important points (turns, checkpoints) in the conversation tree.
|
|
1237
1239
|
|
|
1238
|
-
###
|
|
1240
|
+
### pi.registerCommand(name, options)
|
|
1239
1241
|
|
|
1240
1242
|
Register a command.
|
|
1241
1243
|
|
|
1242
|
-
If multiple extensions register the same command name,
|
|
1244
|
+
If multiple extensions register the same command name, pi keeps them all and assigns numeric invocation suffixes in load order, for example `/review:1` and `/review:2`.
|
|
1243
1245
|
|
|
1244
1246
|
```typescript
|
|
1245
|
-
|
|
1247
|
+
pi.registerCommand("stats", {
|
|
1246
1248
|
description: "Show session statistics",
|
|
1247
1249
|
handler: async (args, ctx) => {
|
|
1248
1250
|
const count = ctx.sessionManager.getEntries().length;
|
|
@@ -1256,7 +1258,7 @@ Optional: add argument auto-completion for `/command ...`:
|
|
|
1256
1258
|
```typescript
|
|
1257
1259
|
import type { AutocompleteItem } from "@eminent337/aery-tui";
|
|
1258
1260
|
|
|
1259
|
-
|
|
1261
|
+
pi.registerCommand("deploy", {
|
|
1260
1262
|
description: "Deploy to an environment",
|
|
1261
1263
|
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
1262
1264
|
const envs = ["dev", "staging", "prod"];
|
|
@@ -1404,17 +1406,17 @@ aery.events.on("my:event", (data) => { ... });
|
|
|
1404
1406
|
aery.events.emit("my:event", { ... });
|
|
1405
1407
|
```
|
|
1406
1408
|
|
|
1407
|
-
###
|
|
1409
|
+
### pi.registerProvider(name, config)
|
|
1408
1410
|
|
|
1409
1411
|
Register or override a model provider dynamically. Useful for proxies, custom endpoints, or team-wide model configurations.
|
|
1410
1412
|
|
|
1411
1413
|
Calls made during the extension factory function are queued and applied once the runner initialises. Calls made after that — for example from a command handler following a user setup flow — take effect immediately without requiring a `/reload`.
|
|
1412
1414
|
|
|
1413
|
-
If you need to discover models from a remote endpoint, prefer an async extension factory over deferring the fetch to `session_start`.
|
|
1415
|
+
If you need to discover models from a remote endpoint, prefer an async extension factory over deferring the fetch to `session_start`. pi waits for the factory before startup continues, so the registered models are available immediately, including to `aery --list-models`.
|
|
1414
1416
|
|
|
1415
1417
|
```typescript
|
|
1416
1418
|
// Register a new provider with custom models
|
|
1417
|
-
|
|
1419
|
+
pi.registerProvider("my-proxy", {
|
|
1418
1420
|
baseUrl: "https://proxy.example.com",
|
|
1419
1421
|
apiKey: "PROXY_API_KEY", // env var name or literal
|
|
1420
1422
|
api: "anthropic-messages",
|
|
@@ -1432,12 +1434,12 @@ aery.registerProvider("my-proxy", {
|
|
|
1432
1434
|
});
|
|
1433
1435
|
|
|
1434
1436
|
// Override baseUrl for an existing provider (keeps all models)
|
|
1435
|
-
|
|
1437
|
+
pi.registerProvider("anthropic", {
|
|
1436
1438
|
baseUrl: "https://proxy.example.com"
|
|
1437
1439
|
});
|
|
1438
1440
|
|
|
1439
1441
|
// Register provider with OAuth support for /login
|
|
1440
|
-
|
|
1442
|
+
pi.registerProvider("corporate-ai", {
|
|
1441
1443
|
baseUrl: "https://ai.corp.com",
|
|
1442
1444
|
api: "openai-responses",
|
|
1443
1445
|
models: [...],
|
|
@@ -1479,7 +1481,7 @@ Remove a previously registered provider and its models. Built-in models that wer
|
|
|
1479
1481
|
Like `registerProvider`, this takes effect immediately when called after the initial load phase, so a `/reload` is not required.
|
|
1480
1482
|
|
|
1481
1483
|
```typescript
|
|
1482
|
-
|
|
1484
|
+
pi.registerCommand("my-setup-teardown", {
|
|
1483
1485
|
description: "Remove the custom proxy provider",
|
|
1484
1486
|
handler: async (_args, _ctx) => {
|
|
1485
1487
|
aery.unregisterProvider("my-proxy");
|
|
@@ -1496,7 +1498,7 @@ export default function (aery: ExtensionAPI) {
|
|
|
1496
1498
|
let items: string[] = [];
|
|
1497
1499
|
|
|
1498
1500
|
// Reconstruct state from session
|
|
1499
|
-
|
|
1501
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1500
1502
|
items = [];
|
|
1501
1503
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
1502
1504
|
if (entry.type === "message" && entry.message.role === "toolResult") {
|
|
@@ -1507,7 +1509,7 @@ export default function (aery: ExtensionAPI) {
|
|
|
1507
1509
|
}
|
|
1508
1510
|
});
|
|
1509
1511
|
|
|
1510
|
-
|
|
1512
|
+
pi.registerTool({
|
|
1511
1513
|
name: "my_tool",
|
|
1512
1514
|
// ...
|
|
1513
1515
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -1523,7 +1525,7 @@ export default function (aery: ExtensionAPI) {
|
|
|
1523
1525
|
|
|
1524
1526
|
## Custom Tools
|
|
1525
1527
|
|
|
1526
|
-
Register tools the LLM can call via `
|
|
1528
|
+
Register tools the LLM can call via `pi.registerTool()`. Tools appear in the system prompt and can have custom rendering.
|
|
1527
1529
|
|
|
1528
1530
|
Use `promptSnippet` for a short one-line entry in the `Available tools` section in the default system prompt. If omitted, custom tools are left out of that section.
|
|
1529
1531
|
|
|
@@ -1568,7 +1570,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
1568
1570
|
import { StringEnum } from "@eminent337/aery-ai";
|
|
1569
1571
|
import { Text } from "@eminent337/aery-tui";
|
|
1570
1572
|
|
|
1571
|
-
|
|
1573
|
+
pi.registerTool({
|
|
1572
1574
|
name: "my_tool",
|
|
1573
1575
|
label: "My Tool",
|
|
1574
1576
|
description: "What this tool does (shown to LLM)",
|
|
@@ -1631,12 +1633,12 @@ async execute(toolCallId, params) {
|
|
|
1631
1633
|
|
|
1632
1634
|
**Important:** Use `StringEnum` from `@eminent337/aery-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
|
|
1633
1635
|
|
|
1634
|
-
**Argument preparation:** `prepareArguments(args)` is optional. If defined, it runs before schema validation and before `execute()`. Use it to mimic an older accepted input shape when
|
|
1636
|
+
**Argument preparation:** `prepareArguments(args)` is optional. If defined, it runs before schema validation and before `execute()`. Use it to mimic an older accepted input shape when pi resumes an older session whose stored tool call arguments no longer match the current schema. Return the object you want validated against `parameters`. Keep the public schema strict. Do not add deprecated compatibility fields to `parameters` just to keep old resumed sessions working.
|
|
1635
1637
|
|
|
1636
1638
|
Example: an older session may contain an `edit` tool call with top-level `oldText` and `newText`, while the current schema only accepts `edits: [{ oldText, newText }]`.
|
|
1637
1639
|
|
|
1638
1640
|
```typescript
|
|
1639
|
-
|
|
1641
|
+
pi.registerTool({
|
|
1640
1642
|
name: "edit",
|
|
1641
1643
|
label: "Edit",
|
|
1642
1644
|
description: "Edit a single file using exact text replacement",
|
|
@@ -1702,13 +1704,13 @@ See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.
|
|
|
1702
1704
|
**Your implementation must match the exact result shape**, including the `details` type. The UI and session logic depend on these shapes for rendering and state tracking.
|
|
1703
1705
|
|
|
1704
1706
|
Built-in tool implementations:
|
|
1705
|
-
- [read.ts](https://github.com/
|
|
1706
|
-
- [bash.ts](https://github.com/
|
|
1707
|
-
- [edit.ts](https://github.com/
|
|
1708
|
-
- [write.ts](https://github.com/
|
|
1709
|
-
- [grep.ts](https://github.com/
|
|
1710
|
-
- [find.ts](https://github.com/
|
|
1711
|
-
- [ls.ts](https://github.com/
|
|
1707
|
+
- [read.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
|
|
1708
|
+
- [bash.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
|
|
1709
|
+
- [edit.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)
|
|
1710
|
+
- [write.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)
|
|
1711
|
+
- [grep.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
|
|
1712
|
+
- [find.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`
|
|
1713
|
+
- [ls.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`
|
|
1712
1714
|
|
|
1713
1715
|
### Remote Execution
|
|
1714
1716
|
|
|
@@ -1726,7 +1728,7 @@ const remoteRead = createReadTool(cwd, {
|
|
|
1726
1728
|
});
|
|
1727
1729
|
|
|
1728
1730
|
// Register, checking flag at execution time
|
|
1729
|
-
|
|
1731
|
+
pi.registerTool({
|
|
1730
1732
|
...remoteRead,
|
|
1731
1733
|
async execute(id, params, signal, onUpdate, _ctx) {
|
|
1732
1734
|
const ssh = getSshConfig();
|
|
@@ -1741,7 +1743,7 @@ aery.registerTool({
|
|
|
1741
1743
|
|
|
1742
1744
|
**Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
|
|
1743
1745
|
|
|
1744
|
-
For `user_bash`, extensions can reuse
|
|
1746
|
+
For `user_bash`, extensions can reuse pi's local shell backend via `createLocalBashOperations()` instead of reimplementing local process spawning, shell resolution, and process-tree termination.
|
|
1745
1747
|
|
|
1746
1748
|
The bash tool also supports a spawn hook to adjust the command, cwd, or env before execution:
|
|
1747
1749
|
|
|
@@ -1819,11 +1821,11 @@ One extension can register multiple tools with shared state:
|
|
|
1819
1821
|
export default function (aery: ExtensionAPI) {
|
|
1820
1822
|
let connection = null;
|
|
1821
1823
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1824
|
+
pi.registerTool({ name: "db_connect", ... });
|
|
1825
|
+
pi.registerTool({ name: "db_query", ... });
|
|
1826
|
+
pi.registerTool({ name: "db_close", ... });
|
|
1825
1827
|
|
|
1826
|
-
|
|
1828
|
+
pi.on("session_shutdown", async () => {
|
|
1827
1829
|
connection?.close();
|
|
1828
1830
|
});
|
|
1829
1831
|
}
|
|
@@ -1831,14 +1833,14 @@ export default function (aery: ExtensionAPI) {
|
|
|
1831
1833
|
|
|
1832
1834
|
### Custom Rendering
|
|
1833
1835
|
|
|
1834
|
-
Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/
|
|
1836
|
+
Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) for how tool rows are composed.
|
|
1835
1837
|
|
|
1836
1838
|
By default, tool output is wrapped in a `Box` that handles padding and background. A defined `renderCall` or `renderResult` must return a `Component`. If a slot renderer is not defined, `tool-execution.ts` uses fallback rendering for that slot.
|
|
1837
1839
|
|
|
1838
1840
|
Set `renderShell: "self"` when the tool should render its own shell instead of using the default `Box`. This is useful for tools that need complete control over framing or background behavior, for example large previews that must stay visually stable after the tool settles.
|
|
1839
1841
|
|
|
1840
1842
|
```typescript
|
|
1841
|
-
|
|
1843
|
+
pi.registerTool({
|
|
1842
1844
|
name: "my_tool",
|
|
1843
1845
|
label: "My Tool",
|
|
1844
1846
|
description: "Custom shell example",
|
|
@@ -2191,7 +2193,7 @@ class VimEditor extends CustomEditor {
|
|
|
2191
2193
|
}
|
|
2192
2194
|
|
|
2193
2195
|
export default function (aery: ExtensionAPI) {
|
|
2194
|
-
|
|
2196
|
+
pi.on("session_start", (_event, ctx) => {
|
|
2195
2197
|
ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
|
|
2196
2198
|
new VimEditor(theme, keybindings)
|
|
2197
2199
|
);
|