mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 06:04:40 +00:00
Add extensions option to createAgentSession SDK
- Accept ExtensionFactory[] for inline extensions (merged with discovery) - Mark preloadedExtensions as @internal (CLI implementation detail) - Update sdk.md with inline extension example - Update CHANGELOG
This commit is contained in:
parent
8da793b1ba
commit
79cb8f0906
6 changed files with 138 additions and 24 deletions
|
|
@ -198,6 +198,7 @@ pi --extension ./safety.ts -e ./todo.ts
|
|||
- Examples: `examples/hooks/` and `examples/custom-tools/` merged into `examples/extensions/`
|
||||
- README: Extensions section expanded with custom tools, commands, events, state persistence, shortcuts, flags, and UI examples
|
||||
- SDK: `customTools` option now accepts `ToolDefinition[]` directly (simplified from `Array<{ path?, tool }>`)
|
||||
- SDK: `extensions` option accepts `ExtensionFactory[]` for inline extensions
|
||||
- SDK: `additionalExtensionPaths` replaces both `additionalHookPaths` and `additionalCustomToolPaths`
|
||||
|
||||
## [0.34.2] - 2026-01-04
|
||||
|
|
|
|||
|
|
@ -468,12 +468,31 @@ Custom tools passed via `customTools` are combined with extension-registered too
|
|||
|
||||
### Extensions
|
||||
|
||||
Extensions are discovered from `~/.pi/agent/extensions/` and `.pi/extensions/`. Use `additionalExtensionPaths` to add extra paths:
|
||||
Extensions are discovered from `~/.pi/agent/extensions/` and `.pi/extensions/`. You can also pass inline extensions or additional paths:
|
||||
|
||||
```typescript
|
||||
import { createAgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, type ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
// Add extension paths (merged with discovery)
|
||||
// Inline extension
|
||||
const myExtension: ExtensionFactory = (pi) => {
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
console.log(`Tool: ${event.toolName}`);
|
||||
});
|
||||
|
||||
pi.registerCommand("hello", {
|
||||
description: "Say hello",
|
||||
handler: async (args, ctx) => {
|
||||
ctx.ui.notify("Hello!", "info");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Pass inline extensions (merged with discovery)
|
||||
const { session } = await createAgentSession({
|
||||
extensions: [myExtension],
|
||||
});
|
||||
|
||||
// Or add paths to load (merged with discovery)
|
||||
const { session } = await createAgentSession({
|
||||
additionalExtensionPaths: ["/path/to/my-extension.ts"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,14 +16,15 @@ Sessions have a version field in the header:
|
|||
|
||||
- **Version 1**: Linear entry sequence (legacy, auto-migrated on load)
|
||||
- **Version 2**: Tree structure with `id`/`parentId` linking
|
||||
- **Version 3**: Renamed `hookMessage` role to `custom` (extensions unification)
|
||||
|
||||
Existing v1 sessions are automatically migrated to v2 when loaded.
|
||||
Existing sessions are automatically migrated to the current version (v3) when loaded.
|
||||
|
||||
## Type Definitions
|
||||
|
||||
- [`src/core/session-manager.ts`](../src/core/session-manager.ts) - Session entry types
|
||||
- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `Attachment`, `ThinkingLevel`
|
||||
- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`
|
||||
- [`packages/agent-core/src/types.ts`](../../agent-core/src/types.ts) - `AgentMessage`
|
||||
- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`, `ImageContent`, `TextContent`
|
||||
|
||||
## Entry Base
|
||||
|
||||
|
|
@ -45,13 +46,13 @@ interface SessionEntryBase {
|
|||
First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
|
||||
|
||||
```json
|
||||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
|
||||
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
|
||||
```
|
||||
|
||||
For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):
|
||||
|
||||
```json
|
||||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
|
||||
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
|
||||
```
|
||||
|
||||
### SessionMessageEntry
|
||||
|
|
@ -89,8 +90,8 @@ Created when context is compacted. Stores a summary of earlier messages.
|
|||
```
|
||||
|
||||
Optional fields:
|
||||
- `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for custom hook implementations)
|
||||
- `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated
|
||||
- `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for extension implementations)
|
||||
- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated
|
||||
|
||||
### BranchSummaryEntry
|
||||
|
||||
|
|
@ -102,30 +103,30 @@ Created when switching branches via `/tree` with an LLM generated summary of the
|
|||
|
||||
Optional fields:
|
||||
- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default implementation, arbitrary for custom implementation
|
||||
- `fromHook`: `true` if generated by a hook
|
||||
- `fromHook`: `true` if generated by an extension
|
||||
|
||||
### CustomEntry
|
||||
|
||||
Hook state persistence. Does NOT participate in LLM context.
|
||||
Extension state persistence. Does NOT participate in LLM context.
|
||||
|
||||
```json
|
||||
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-hook","data":{"count":42}}
|
||||
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}
|
||||
```
|
||||
|
||||
Use `customType` to identify your hook's entries on reload.
|
||||
Use `customType` to identify your extension's entries on reload.
|
||||
|
||||
### CustomMessageEntry
|
||||
|
||||
Hook-injected messages that DO participate in LLM context.
|
||||
Extension-injected messages that DO participate in LLM context.
|
||||
|
||||
```json
|
||||
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-hook","content":"Injected context...","display":true}
|
||||
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
|
||||
- `display`: `true` = show in TUI with purple styling, `false` = hidden
|
||||
- `details`: Optional hook-specific metadata (not sent to LLM)
|
||||
- `display`: `true` = show in TUI with distinct styling, `false` = hidden
|
||||
- `details`: Optional extension-specific metadata (not sent to LLM)
|
||||
|
||||
### LabelEntry
|
||||
|
||||
|
|
@ -190,7 +191,7 @@ for (const line of lines) {
|
|||
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
|
||||
break;
|
||||
case "custom_message":
|
||||
console.log(`[${entry.id}] Hook message (${entry.customType}): ${entry.content}`);
|
||||
console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
|
||||
break;
|
||||
case "label":
|
||||
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
|
||||
|
|
@ -220,18 +221,20 @@ Key methods for working with sessions programmatically:
|
|||
- `appendThinkingLevelChange(level)` - Record thinking change
|
||||
- `appendModelChange(provider, modelId)` - Record model change
|
||||
- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
|
||||
- `appendCustomEntry(customType, data?)` - Hook state (not in context)
|
||||
- `appendCustomMessageEntry(customType, content, display, details?)` - Hook message (in context)
|
||||
- `appendCustomEntry(customType, data?)` - Extension state (not in context)
|
||||
- `appendCustomMessageEntry(customType, content, display, details?)` - Extension message (in context)
|
||||
- `appendLabelChange(targetId, label)` - Set/clear label
|
||||
|
||||
### Tree Navigation
|
||||
- `getLeafId()` - Current position
|
||||
- `getLeafEntry()` - Get current leaf entry
|
||||
- `getEntry(id)` - Get entry by ID
|
||||
- `getPath(fromId?)` - Walk from entry to root
|
||||
- `getBranch(fromId?)` - Walk from entry to root
|
||||
- `getTree()` - Get full tree structure
|
||||
- `getChildren(parentId)` - Get direct children
|
||||
- `getLabel(id)` - Get label for entry
|
||||
- `branch(entryId)` - Move leaf to earlier entry
|
||||
- `resetLeaf()` - Reset leaf to null (before any entries)
|
||||
- `branchWithSummary(entryId, summary, details?, fromHook?)` - Branch with context summary
|
||||
|
||||
### Context
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Extension system for lifecycle events and custom tools.
|
||||
*/
|
||||
|
||||
export { discoverAndLoadExtensions, loadExtensions } from "./loader.js";
|
||||
export { discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from "./loader.js";
|
||||
export type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler } from "./runner.js";
|
||||
export { ExtensionRunner } from "./runner.js";
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -375,6 +375,54 @@ async function loadExtension(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a LoadedExtension from an inline factory function.
|
||||
*/
|
||||
export function loadExtensionFromFactory(
|
||||
factory: ExtensionFactory,
|
||||
cwd: string,
|
||||
eventBus: EventBus,
|
||||
sharedUI: { ui: ExtensionUIContext; hasUI: boolean },
|
||||
name = "<inline>",
|
||||
): LoadedExtension {
|
||||
const handlers = new Map<string, HandlerFn[]>();
|
||||
const tools = new Map<string, RegisteredTool>();
|
||||
const {
|
||||
api,
|
||||
messageRenderers,
|
||||
commands,
|
||||
flags,
|
||||
flagValues,
|
||||
shortcuts,
|
||||
setSendMessageHandler,
|
||||
setAppendEntryHandler,
|
||||
setGetActiveToolsHandler,
|
||||
setGetAllToolsHandler,
|
||||
setSetActiveToolsHandler,
|
||||
setFlagValue,
|
||||
} = createExtensionAPI(handlers, tools, cwd, name, eventBus, sharedUI);
|
||||
|
||||
factory(api);
|
||||
|
||||
return {
|
||||
path: name,
|
||||
resolvedPath: name,
|
||||
handlers,
|
||||
tools,
|
||||
messageRenderers,
|
||||
commands,
|
||||
flags,
|
||||
flagValues,
|
||||
shortcuts,
|
||||
setSendMessageHandler,
|
||||
setAppendEntryHandler,
|
||||
setGetActiveToolsHandler,
|
||||
setGetAllToolsHandler,
|
||||
setSetActiveToolsHandler,
|
||||
setFlagValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load extensions from paths.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ import { AuthStorage } from "./auth-storage.js";
|
|||
import { createEventBus, type EventBus } from "./event-bus.js";
|
||||
import {
|
||||
discoverAndLoadExtensions,
|
||||
type ExtensionFactory,
|
||||
ExtensionRunner,
|
||||
type LoadExtensionsResult,
|
||||
type LoadedExtension,
|
||||
loadExtensionFromFactory,
|
||||
type ToolDefinition,
|
||||
wrapRegisteredTools,
|
||||
wrapToolsWithExtensions,
|
||||
|
|
@ -99,9 +101,14 @@ export interface CreateAgentSessionOptions {
|
|||
tools?: Tool[];
|
||||
/** Custom tools to register (in addition to built-in tools). */
|
||||
customTools?: ToolDefinition[];
|
||||
/** Inline extensions (merged with discovery). */
|
||||
extensions?: ExtensionFactory[];
|
||||
/** Additional extension paths to load (merged with discovery). */
|
||||
additionalExtensionPaths?: string[];
|
||||
/** Pre-loaded extensions (skips loading, used when extensions were loaded early for CLI flags). */
|
||||
/**
|
||||
* Pre-loaded extensions (skips file discovery).
|
||||
* @internal Used by CLI when extensions are loaded early to parse custom flags.
|
||||
*/
|
||||
preloadedExtensions?: LoadedExtension[];
|
||||
|
||||
/** Shared event bus for tool/extension communication. Default: creates new bus. */
|
||||
|
|
@ -447,6 +454,42 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
}
|
||||
}
|
||||
|
||||
// Load inline extensions from factories
|
||||
if (options.extensions && options.extensions.length > 0) {
|
||||
// Create shared UI context holder that will be set later
|
||||
const uiHolder: { ui: any; hasUI: boolean } = {
|
||||
ui: {
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
setStatus: () => {},
|
||||
setWidget: () => {},
|
||||
setTitle: () => {},
|
||||
custom: async () => undefined as never,
|
||||
setEditorText: () => {},
|
||||
getEditorText: () => "",
|
||||
editor: async () => undefined,
|
||||
get theme() {
|
||||
return {} as any;
|
||||
},
|
||||
},
|
||||
hasUI: false,
|
||||
};
|
||||
for (let i = 0; i < options.extensions.length; i++) {
|
||||
const factory = options.extensions[i];
|
||||
const loaded = loadExtensionFromFactory(factory, cwd, eventBus, uiHolder, `<inline-${i}>`);
|
||||
extensionsResult.extensions.push(loaded);
|
||||
}
|
||||
// Extend setUIContext to update inline extensions too
|
||||
const originalSetUIContext = extensionsResult.setUIContext;
|
||||
extensionsResult.setUIContext = (uiContext, hasUI) => {
|
||||
originalSetUIContext(uiContext, hasUI);
|
||||
uiHolder.ui = uiContext;
|
||||
uiHolder.hasUI = hasUI;
|
||||
};
|
||||
}
|
||||
|
||||
// Create extension runner if we have extensions
|
||||
let extensionRunner: ExtensionRunner | undefined;
|
||||
if (extensionsResult.extensions.length > 0) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue