mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 22:02:38 +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/`
|
- 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
|
- 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: `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`
|
- SDK: `additionalExtensionPaths` replaces both `additionalHookPaths` and `additionalCustomToolPaths`
|
||||||
|
|
||||||
## [0.34.2] - 2026-01-04
|
## [0.34.2] - 2026-01-04
|
||||||
|
|
|
||||||
|
|
@ -468,12 +468,31 @@ Custom tools passed via `customTools` are combined with extension-registered too
|
||||||
|
|
||||||
### Extensions
|
### 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
|
```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({
|
const { session } = await createAgentSession({
|
||||||
additionalExtensionPaths: ["/path/to/my-extension.ts"],
|
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 1**: Linear entry sequence (legacy, auto-migrated on load)
|
||||||
- **Version 2**: Tree structure with `id`/`parentId` linking
|
- **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
|
## Type Definitions
|
||||||
|
|
||||||
- [`src/core/session-manager.ts`](../src/core/session-manager.ts) - Session entry types
|
- [`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/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`
|
- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`, `ImageContent`, `TextContent`
|
||||||
|
|
||||||
## Entry Base
|
## Entry Base
|
||||||
|
|
||||||
|
|
@ -45,13 +46,13 @@ interface SessionEntryBase {
|
||||||
First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
|
First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
|
||||||
|
|
||||||
```json
|
```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 })`):
|
For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):
|
||||||
|
|
||||||
```json
|
```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
|
### SessionMessageEntry
|
||||||
|
|
@ -89,8 +90,8 @@ Created when context is compacted. Stores a summary of earlier messages.
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional fields:
|
Optional fields:
|
||||||
- `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for custom hook implementations)
|
- `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for extension implementations)
|
||||||
- `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated
|
- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated
|
||||||
|
|
||||||
### BranchSummaryEntry
|
### BranchSummaryEntry
|
||||||
|
|
||||||
|
|
@ -102,30 +103,30 @@ Created when switching branches via `/tree` with an LLM generated summary of the
|
||||||
|
|
||||||
Optional fields:
|
Optional fields:
|
||||||
- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default implementation, arbitrary for custom implementation
|
- `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
|
### CustomEntry
|
||||||
|
|
||||||
Hook state persistence. Does NOT participate in LLM context.
|
Extension state persistence. Does NOT participate in LLM context.
|
||||||
|
|
||||||
```json
|
```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
|
### CustomMessageEntry
|
||||||
|
|
||||||
Hook-injected messages that DO participate in LLM context.
|
Extension-injected messages that DO participate in LLM context.
|
||||||
|
|
||||||
```json
|
```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:
|
Fields:
|
||||||
- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
|
- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
|
||||||
- `display`: `true` = show in TUI with purple styling, `false` = hidden
|
- `display`: `true` = show in TUI with distinct styling, `false` = hidden
|
||||||
- `details`: Optional hook-specific metadata (not sent to LLM)
|
- `details`: Optional extension-specific metadata (not sent to LLM)
|
||||||
|
|
||||||
### LabelEntry
|
### LabelEntry
|
||||||
|
|
||||||
|
|
@ -190,7 +191,7 @@ for (const line of lines) {
|
||||||
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
|
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
|
||||||
break;
|
break;
|
||||||
case "custom_message":
|
case "custom_message":
|
||||||
console.log(`[${entry.id}] Hook message (${entry.customType}): ${entry.content}`);
|
console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
|
||||||
break;
|
break;
|
||||||
case "label":
|
case "label":
|
||||||
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
|
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
|
- `appendThinkingLevelChange(level)` - Record thinking change
|
||||||
- `appendModelChange(provider, modelId)` - Record model change
|
- `appendModelChange(provider, modelId)` - Record model change
|
||||||
- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
|
- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
|
||||||
- `appendCustomEntry(customType, data?)` - Hook state (not in context)
|
- `appendCustomEntry(customType, data?)` - Extension state (not in context)
|
||||||
- `appendCustomMessageEntry(customType, content, display, details?)` - Hook message (in context)
|
- `appendCustomMessageEntry(customType, content, display, details?)` - Extension message (in context)
|
||||||
- `appendLabelChange(targetId, label)` - Set/clear label
|
- `appendLabelChange(targetId, label)` - Set/clear label
|
||||||
|
|
||||||
### Tree Navigation
|
### Tree Navigation
|
||||||
- `getLeafId()` - Current position
|
- `getLeafId()` - Current position
|
||||||
|
- `getLeafEntry()` - Get current leaf entry
|
||||||
- `getEntry(id)` - Get entry by ID
|
- `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
|
- `getTree()` - Get full tree structure
|
||||||
- `getChildren(parentId)` - Get direct children
|
- `getChildren(parentId)` - Get direct children
|
||||||
- `getLabel(id)` - Get label for entry
|
- `getLabel(id)` - Get label for entry
|
||||||
- `branch(entryId)` - Move leaf to earlier 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
|
- `branchWithSummary(entryId, summary, details?, fromHook?)` - Branch with context summary
|
||||||
|
|
||||||
### Context
|
### Context
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Extension system for lifecycle events and custom tools.
|
* 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 type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler } from "./runner.js";
|
||||||
export { ExtensionRunner } from "./runner.js";
|
export { ExtensionRunner } from "./runner.js";
|
||||||
export type {
|
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.
|
* Load extensions from paths.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,11 @@ import { AuthStorage } from "./auth-storage.js";
|
||||||
import { createEventBus, type EventBus } from "./event-bus.js";
|
import { createEventBus, type EventBus } from "./event-bus.js";
|
||||||
import {
|
import {
|
||||||
discoverAndLoadExtensions,
|
discoverAndLoadExtensions,
|
||||||
|
type ExtensionFactory,
|
||||||
ExtensionRunner,
|
ExtensionRunner,
|
||||||
type LoadExtensionsResult,
|
type LoadExtensionsResult,
|
||||||
type LoadedExtension,
|
type LoadedExtension,
|
||||||
|
loadExtensionFromFactory,
|
||||||
type ToolDefinition,
|
type ToolDefinition,
|
||||||
wrapRegisteredTools,
|
wrapRegisteredTools,
|
||||||
wrapToolsWithExtensions,
|
wrapToolsWithExtensions,
|
||||||
|
|
@ -99,9 +101,14 @@ export interface CreateAgentSessionOptions {
|
||||||
tools?: Tool[];
|
tools?: Tool[];
|
||||||
/** Custom tools to register (in addition to built-in tools). */
|
/** Custom tools to register (in addition to built-in tools). */
|
||||||
customTools?: ToolDefinition[];
|
customTools?: ToolDefinition[];
|
||||||
|
/** Inline extensions (merged with discovery). */
|
||||||
|
extensions?: ExtensionFactory[];
|
||||||
/** Additional extension paths to load (merged with discovery). */
|
/** Additional extension paths to load (merged with discovery). */
|
||||||
additionalExtensionPaths?: string[];
|
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[];
|
preloadedExtensions?: LoadedExtension[];
|
||||||
|
|
||||||
/** Shared event bus for tool/extension communication. Default: creates new bus. */
|
/** 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
|
// Create extension runner if we have extensions
|
||||||
let extensionRunner: ExtensionRunner | undefined;
|
let extensionRunner: ExtensionRunner | undefined;
|
||||||
if (extensionsResult.extensions.length > 0) {
|
if (extensionsResult.extensions.length > 0) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue