refactor(coding-agent): replace AgentSessionRuntimeHost with closure-based AgentSessionRuntime
- Replace AgentSessionRuntimeHost and bootstrap abstractions with AgentSessionRuntime - Runtime creation is now closure-based via CreateAgentSessionRuntimeFactory - Factory closes over process-global fixed inputs, recreates cwd-bound services per effective cwd - Session config (model, thinking, tools, scoped models) re-resolved per target cwd - CLI resource paths resolved once at startup as absolute paths - Swap lifecycle: teardown old, create next, apply next (hard fail on creation error) - Unified diagnostics model (info/warning/error) for args, services, session resolution, resources - No logging or process exits inside creation/parsing logic - Removed session_directory support - Removed session_switch and session_fork extension events (use session_start with reason) - Moved package/config CLI to package-manager-cli.ts - Fixed theme init for --resume session picker - Fixed flaky reftable footer test (content-based polling) - Fixed silent drop of unknown single-dash CLI flags - Added error diagnostics for missing explicit CLI resource paths - Updated SDK docs, examples, plans, exports, tests, changelog fixes #2753
This commit is contained in:
@@ -226,9 +226,8 @@ Run `npm install` in the extension directory, then imports from `node_modules/`
|
||||
### Lifecycle Overview
|
||||
|
||||
```
|
||||
pi starts (CLI only)
|
||||
pi starts
|
||||
│
|
||||
├─► session_directory (CLI startup only, no ctx)
|
||||
├─► session_start { reason: "startup" }
|
||||
└─► resources_discover { reason: "startup" }
|
||||
│
|
||||
@@ -311,27 +310,6 @@ pi.on("resources_discover", async (event, _ctx) => {
|
||||
|
||||
See [session.md](session.md) for session storage internals and the SessionManager API.
|
||||
|
||||
#### session_directory
|
||||
|
||||
Fired by the `pi` CLI during startup session resolution, before the initial session manager is created.
|
||||
|
||||
This event is:
|
||||
- CLI-only. It is not emitted in SDK mode.
|
||||
- Startup-only. It is not emitted for later interactive `/new` or `/resume` actions.
|
||||
- Lower priority than `--session-dir` and `sessionDir` in `settings.json`.
|
||||
- Special-cased to receive no `ctx` argument.
|
||||
|
||||
If multiple extensions return `sessionDir`, the last one wins.
|
||||
Combined precedence is: `--session-dir` CLI flag, then `sessionDir` in settings, then extension `session_directory` hooks.
|
||||
|
||||
```typescript
|
||||
pi.on("session_directory", async (event) => {
|
||||
return {
|
||||
sessionDir: `/tmp/pi-sessions/${encodeURIComponent(event.cwd)}`,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
#### session_start
|
||||
|
||||
Fired when a session is started, loaded, or reloaded.
|
||||
@@ -759,9 +737,7 @@ Transforms chain across handlers. See [input-transform.ts](../examples/extension
|
||||
|
||||
## ExtensionContext
|
||||
|
||||
All handlers except `session_directory` receive `ctx: ExtensionContext`.
|
||||
|
||||
`session_directory` is a CLI startup hook and receives only the event.
|
||||
All handlers receive `ctx: ExtensionContext`.
|
||||
|
||||
### ctx.ui
|
||||
|
||||
|
||||
@@ -115,33 +115,46 @@ interface AgentSession {
|
||||
}
|
||||
```
|
||||
|
||||
Session replacement APIs such as new-session, resume, fork, and import live on `AgentSessionRuntimeHost`, not on `AgentSession`.
|
||||
Session replacement APIs such as new-session, resume, fork, and import live on `AgentSessionRuntime`, not on `AgentSession`.
|
||||
|
||||
### createAgentSessionRuntime() and AgentSessionRuntimeHost
|
||||
### createAgentSessionRuntime() and AgentSessionRuntime
|
||||
|
||||
Use the runtime API when you need to replace the active session and rebuild cwd-bound runtime state.
|
||||
This is the same layer used by the built-in interactive, print, and RPC modes.
|
||||
|
||||
`createAgentSessionRuntime()` takes a runtime factory plus the initial cwd/session target. The factory closes over process-global fixed inputs, recreates cwd-bound services for the effective cwd, resolves session options against those services, and returns a full runtime result.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const bootstrap = {
|
||||
// Optional: authStorage, model, thinkingLevel, tools, customTools, resourceLoader
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({ cwd });
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const runtime = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
sessionManager: SessionManager.create(process.cwd()),
|
||||
});
|
||||
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
```
|
||||
|
||||
`createAgentSessionRuntime()` returns an internal runtime bundle. `AgentSessionRuntimeHost` owns replacement of that bundle across:
|
||||
`AgentSessionRuntime` owns replacement of the active runtime across:
|
||||
|
||||
- `newSession()`
|
||||
- `switchSession()`
|
||||
@@ -150,18 +163,20 @@ const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `runtimeHost.session` changes after those operations
|
||||
- `runtime.session` changes after those operations
|
||||
- event subscriptions are attached to a specific `AgentSession`, so re-subscribe after replacement
|
||||
- if you use extensions, call `runtimeHost.session.bindExtensions(...)` again for the new session
|
||||
- if you use extensions, call `runtime.session.bindExtensions(...)` again for the new session
|
||||
- creation returns diagnostics on `runtime.diagnostics`
|
||||
- if runtime creation or replacement fails, the method throws and the caller decides how to handle it
|
||||
|
||||
```typescript
|
||||
let session = runtimeHost.session;
|
||||
let session = runtime.session;
|
||||
let unsubscribe = session.subscribe(() => {});
|
||||
|
||||
await runtimeHost.newSession();
|
||||
await runtime.newSession();
|
||||
|
||||
unsubscribe();
|
||||
session = runtimeHost.session;
|
||||
session = runtime.session;
|
||||
unsubscribe = session.subscribe(() => {});
|
||||
```
|
||||
|
||||
@@ -646,9 +661,12 @@ Sessions use a tree structure with `id`/`parentId` linking, enabling in-place br
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSession,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
@@ -680,21 +698,33 @@ const currentProjectSessions = await SessionManager.list(process.cwd());
|
||||
const allSessions = await SessionManager.listAll(process.cwd());
|
||||
|
||||
// Session replacement API for /new, /resume, /fork, and import flows.
|
||||
const bootstrap = {};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({ cwd });
|
||||
return {
|
||||
...(await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
})),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
|
||||
const runtime = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
sessionManager: SessionManager.create(process.cwd()),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
|
||||
// Replace the active session with a fresh one
|
||||
await runtimeHost.newSession();
|
||||
await runtime.newSession();
|
||||
|
||||
// Replace the active session with another saved session
|
||||
await runtimeHost.switchSession("/path/to/session.jsonl");
|
||||
await runtime.switchSession("/path/to/session.jsonl");
|
||||
|
||||
// Replace the active session with a fork from a specific entry
|
||||
await runtimeHost.fork("entry-id");
|
||||
await runtime.fork("entry-id");
|
||||
```
|
||||
|
||||
**SessionManager tree API:**
|
||||
@@ -916,20 +946,30 @@ Full TUI interactive mode with editor, chat history, and all built-in commands:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
InteractiveMode,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const bootstrap = {};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({ cwd });
|
||||
return {
|
||||
...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
sessionManager: SessionManager.create(process.cwd()),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
|
||||
const mode = new InteractiveMode(runtimeHost, {
|
||||
const mode = new InteractiveMode(runtime, {
|
||||
migratedProviders: [],
|
||||
modelFallbackMessage: undefined,
|
||||
initialMessage: "Hello",
|
||||
@@ -946,20 +986,30 @@ Single-shot mode: send prompts, output result, exit:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
runPrintMode,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const bootstrap = {};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({ cwd });
|
||||
return {
|
||||
...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
sessionManager: SessionManager.create(process.cwd()),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
|
||||
await runPrintMode(runtimeHost, {
|
||||
await runPrintMode(runtime, {
|
||||
mode: "text",
|
||||
initialMessage: "Hello",
|
||||
initialImages: [],
|
||||
@@ -973,20 +1023,30 @@ JSON-RPC mode for subprocess integration:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
AgentSessionRuntimeHost,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
runRpcMode,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const bootstrap = {};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
||||
const services = await createAgentSessionServices({ cwd });
|
||||
return {
|
||||
...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
|
||||
services,
|
||||
diagnostics: services.diagnostics,
|
||||
};
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(createRuntime, {
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
sessionManager: SessionManager.create(process.cwd()),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
|
||||
await runRpcMode(runtimeHost);
|
||||
await runRpcMode(runtime);
|
||||
```
|
||||
|
||||
See [RPC documentation](rpc.md) for the JSON protocol.
|
||||
@@ -1020,7 +1080,7 @@ The main entry point exports:
|
||||
// Factory
|
||||
createAgentSession
|
||||
createAgentSessionRuntime
|
||||
AgentSessionRuntimeHost
|
||||
AgentSessionRuntime
|
||||
|
||||
// Auth and Models
|
||||
AuthStorage
|
||||
|
||||
@@ -137,7 +137,7 @@ When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's
|
||||
{ "sessionDir": ".pi/sessions" }
|
||||
```
|
||||
|
||||
When multiple sources specify a session directory, `--session-dir` CLI flag takes precedence, then `sessionDir` in settings.json, then extension hooks.
|
||||
When multiple sources specify a session directory, `--session-dir` CLI flag takes precedence over `sessionDir` in settings.json.
|
||||
|
||||
### Model Cycling
|
||||
|
||||
|
||||
Reference in New Issue
Block a user