@agentic-kit/pi 0.15.0 → 0.16.0

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/README.md CHANGED
@@ -12,7 +12,19 @@
12
12
  <a href="https://www.npmjs.com/package/@agentic-kit/pi"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=agentic%2Fpi%2Fpackage.json"/></a>
13
13
  </p>
14
14
 
15
- The [pi coding agent](https://github.com/badlogic/pi-mono) adapter for **agentic-kit**: the Constructive typed database tools and the [`@agentic-kit/harness`](https://www.npmjs.com/package/@agentic-kit/harness) confirm gate, packaged as a pi extension any host can register — Constructive Desktop, the [`agent` CLI](https://www.npmjs.com/package/@agentic-kit/cli), or your own pi-based agent.
15
+ The **one** [pi coding agent](https://github.com/badlogic/pi-mono) adapter for **agentic-kit**: the Constructive typed database tools, and everything that attaches a platform run to a pi session — the run gate, the run log, the metering gateway and usage reporting — for any host to register: Constructive Desktop, the [`agent` CLI](https://www.npmjs.com/package/@agentic-kit/cli), or your own pi-based agent.
16
+
17
+ What a run *does* is harness-neutral and lives elsewhere; only its attachment to pi lives here.
18
+
19
+ ```
20
+ neutral contracts this package pi
21
+ ──────────────────────────────────── ───────────────────────── ────────────────────
22
+ @agentic-kit/harness (gate, adapter) tool_call ─► RunGate pi.on('tool_call')
23
+ @agentic-kit/metering (gateway, usage) MeteredGateway ─► provider pi.registerProvider
24
+ @agentic-kit/run-log (append-only) SessionMirror pi's session manager
25
+ ```
26
+
27
+ So a second, compatible harness is a *sibling adapter* — not a fork of the lanes.
16
28
 
17
29
  ```bash
18
30
  npm install @agentic-kit/pi
@@ -22,6 +34,8 @@ npm install @agentic-kit/pi
22
34
 
23
35
  - **16 typed db tools** — `provision_database`, `provision_blueprint`, `describe_schema`, `add_relation`, `delete_table`, `create_field` / `update_field` / `delete_field`, `add_policies`, `add_records`, `run_codegen`, and the template suite (`list` / `create` / `apply` / `update` / `delete`). Tool schemas are authored in [zod](https://zod.dev) and emitted as plain JSON Schema at pi's tool boundary.
24
36
  - **Confirm gate** — the harness's host-neutral gate wired to pi's `tool_call` events. Hosts with a rich confirm surface (Desktop) expose `confirmTool`/`notifyToolSkipped` on `ctx.ui`; everyone else gets pi's built-in `ui.confirm` dialog.
37
+ - **Run lanes** — `composeRun` / `startRun` build a pi session with the run's lanes attached: `createRunLogExtension` mirrors pi's session entries into the run log under `transcriptFormat: 'pi'`, `createMeteredModelExtension` points pi at the Constructive gateway so usage cannot be under-reported, `createUsageReportExtension` self-reports usage when the host owns the provider keys, and `createGateExtension` suspends a gated tool call until a human answers.
38
+ - **`piHarness`** — the `HarnessAdapter` face of the above: an `id`, the `transcriptFormat` its entries are written under, and `startRun`.
25
39
  - **Host injection** — credentials, backend endpoints, and data-plane tokens come from your host, not from the package:
26
40
 
27
41
  ```ts
@@ -88,7 +102,9 @@ For local development, the cold path and the warm pool depend on the backend's j
88
102
 
89
103
  ## Related
90
104
 
91
- - [`@agentic-kit/harness`](https://www.npmjs.com/package/@agentic-kit/harness) — host-neutral skills, gating, and blueprint core
105
+ - [`@agentic-kit/harness`](https://www.npmjs.com/package/@agentic-kit/harness) — host-neutral skills, gating, blueprint core, and the `HarnessAdapter` contract
106
+ - [`@agentic-kit/metering`](https://www.npmjs.com/package/@agentic-kit/metering) — the metered gateway and usage reporting
107
+ - [`@agentic-kit/run-log`](https://www.npmjs.com/package/@agentic-kit/run-log) — the append-only run log
92
108
  - [`@agentic-kit/cli`](https://www.npmjs.com/package/@agentic-kit/cli) — `agent`, a local secure-by-default coding agent
93
109
  - [`agentic-kit`](https://www.npmjs.com/package/agentic-kit) — the umbrella package
94
110
 
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Compose a run's lanes into the extension list pi loads.
3
+ *
4
+ * A run's placement — a developer's laptop, a long-running cloud job — is *only*
5
+ * a difference in these options: which store the log appends to, whether the
6
+ * model calls leave through the metered gateway or a local key, and where an
7
+ * approval question is asked. The composed session is otherwise identical, which
8
+ * is the whole point of the package.
9
+ */
10
+ import type { MeteredIdentity } from '@agentic-kit/metering';
11
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
12
+ import { type GateExtension, type GateExtensionOptions } from '../extensions/gate';
13
+ import { type MeteredModelExtension, type MeteredModelExtensionOptions } from '../extensions/metered-model';
14
+ import { type RunLogExtension, type RunLogExtensionOptions } from '../extensions/run-log';
15
+ import { type UsageReportExtension, type UsageReportExtensionOptions } from '../extensions/usage-report';
16
+ /** Run log lane — `runId` comes from the run, not from here. */
17
+ export type RunLogLane = Omit<RunLogExtensionOptions, 'runId'>;
18
+ /**
19
+ * Metering lane. `gateway` is authoritative (the gateway meters what it proxies);
20
+ * `self-report` is the own-provider-key lane and is only as trustworthy as the
21
+ * agent reporting it. They are mutually exclusive on purpose: routing through the
22
+ * gateway *and* self-reporting would double-count the same tokens.
23
+ */
24
+ export type MeteringLane = ({
25
+ mode: 'gateway';
26
+ } & MeteredModelExtensionOptions) | ({
27
+ mode: 'self-report';
28
+ } & UsageReportExtensionOptions);
29
+ /** Approval lane — `runId` comes from the run. */
30
+ export type GateLane = Omit<GateExtensionOptions, 'runId'>;
31
+ export interface ComposeRunOptions {
32
+ runId: string;
33
+ log?: RunLogLane;
34
+ metering?: MeteringLane;
35
+ gate?: GateLane;
36
+ /**
37
+ * The host's own extensions — workspace tools, prompts, UI glue. Loaded after
38
+ * the lanes so a host tool call is already gated and already logged.
39
+ */
40
+ extensions?: readonly ExtensionFactory[];
41
+ }
42
+ export interface ComposedLanes {
43
+ log?: RunLogExtension;
44
+ meteredModel?: MeteredModelExtension;
45
+ usageReport?: UsageReportExtension;
46
+ gate?: GateExtension;
47
+ }
48
+ export interface ComposedRun {
49
+ runId: string;
50
+ /** In load order: log, metering, gate, then the host's own. */
51
+ extensions: ExtensionFactory[];
52
+ lanes: ComposedLanes;
53
+ /**
54
+ * Drain every lane that buffers, and throw the first failure. A host calls
55
+ * this before it exits; the extensions also flush themselves on
56
+ * `session_shutdown`, so this is for hosts that end a run without one.
57
+ */
58
+ flush(): Promise<void>;
59
+ }
60
+ export declare function composeRun(options: ComposeRunOptions): ComposedRun;
61
+ /**
62
+ * The identity every metered lane needs. Exported so a host can build it once
63
+ * from its resolved project context and hand the same value to both lanes.
64
+ */
65
+ export type { MeteredIdentity };
package/embed/lanes.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Compose a run's lanes into the extension list pi loads.
4
+ *
5
+ * A run's placement — a developer's laptop, a long-running cloud job — is *only*
6
+ * a difference in these options: which store the log appends to, whether the
7
+ * model calls leave through the metered gateway or a local key, and where an
8
+ * approval question is asked. The composed session is otherwise identical, which
9
+ * is the whole point of the package.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.composeRun = composeRun;
13
+ const gate_1 = require("../extensions/gate");
14
+ const metered_model_1 = require("../extensions/metered-model");
15
+ const run_log_1 = require("../extensions/run-log");
16
+ const usage_report_1 = require("../extensions/usage-report");
17
+ function composeRun(options) {
18
+ const lanes = {};
19
+ const extensions = [];
20
+ if (options.log) {
21
+ lanes.log = (0, run_log_1.createRunLogExtension)({ runId: options.runId, ...options.log });
22
+ extensions.push(lanes.log.extension);
23
+ }
24
+ const metering = options.metering;
25
+ if (metering?.mode === 'gateway') {
26
+ const { mode: _mode, ...meteredOptions } = metering;
27
+ lanes.meteredModel = (0, metered_model_1.createMeteredModelExtension)(meteredOptions);
28
+ extensions.push(lanes.meteredModel.extension);
29
+ }
30
+ else if (metering?.mode === 'self-report') {
31
+ const { mode: _mode, ...reportOptions } = metering;
32
+ lanes.usageReport = (0, usage_report_1.createUsageReportExtension)(reportOptions);
33
+ extensions.push(lanes.usageReport.extension);
34
+ }
35
+ if (options.gate) {
36
+ lanes.gate = (0, gate_1.createGateExtension)({ runId: options.runId, ...options.gate });
37
+ extensions.push(lanes.gate.extension);
38
+ }
39
+ if (options.extensions)
40
+ extensions.push(...options.extensions);
41
+ return {
42
+ runId: options.runId,
43
+ extensions,
44
+ lanes,
45
+ flush: async () => {
46
+ // Sequential, log first: a usage failure must not cost us the transcript.
47
+ if (lanes.log)
48
+ await lanes.log.flush();
49
+ if (lanes.usageReport)
50
+ await lanes.usageReport.flush();
51
+ }
52
+ };
53
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Start a pi session with a run's lanes attached.
3
+ *
4
+ * pi ships as ESM only, and every host already imports it dynamically (the
5
+ * desktop main process does this in `bootstrap`, before pi's storage paths are
6
+ * resolved). So the module is *injected* rather than imported here: this package
7
+ * stays CJS+ESM publishable, the host keeps control of when pi loads, and these
8
+ * paths are testable without a model, a network or a filesystem.
9
+ *
10
+ * pi takes extensions through a `ResourceLoader`, not through
11
+ * `createAgentSession`, so embedding means handing the composed factories to a
12
+ * loader — pi's default one, or the host's own (the desktop harness builds one
13
+ * that layers its skills, prompts and templates).
14
+ */
15
+ import type { CreateAgentSessionOptions, CreateAgentSessionResult, ExtensionFactory, ResourceLoader } from '@earendil-works/pi-coding-agent';
16
+ import { type ComposedRun, type ComposeRunOptions } from './lanes';
17
+ export interface ResourceLoaderRequest {
18
+ /** The lane extensions, in load order. Give these to the loader. */
19
+ extensionFactories: ExtensionFactory[];
20
+ cwd: string;
21
+ agentDir: string;
22
+ }
23
+ export type CreateResourceLoader = (request: ResourceLoaderRequest) => ResourceLoader | Promise<ResourceLoader>;
24
+ /** The slice of the pi module this package needs, so a host can inject it. */
25
+ export interface PiModule {
26
+ createAgentSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
27
+ DefaultResourceLoader: new (options: {
28
+ cwd: string;
29
+ agentDir: string;
30
+ extensionFactories?: ExtensionFactory[];
31
+ }) => ResourceLoader;
32
+ getAgentDir?: () => string;
33
+ }
34
+ export interface StartRunOptions extends ComposeRunOptions {
35
+ /** `await import('@earendil-works/pi-coding-agent')`. */
36
+ pi: PiModule;
37
+ cwd?: string;
38
+ /** pi's config/storage dir. Defaults to `pi.getAgentDir()` when available. */
39
+ agentDir?: string;
40
+ /** Passed through to pi, minus the ones this package owns. */
41
+ session?: Omit<CreateAgentSessionOptions, 'cwd' | 'agentDir' | 'resourceLoader'>;
42
+ /** Layer the lanes onto a host's own loader instead of pi's default. */
43
+ createResourceLoader?: CreateResourceLoader;
44
+ }
45
+ export interface EmbeddedRun {
46
+ run: ComposedRun;
47
+ session: CreateAgentSessionResult['session'];
48
+ extensionsResult: CreateAgentSessionResult['extensionsResult'];
49
+ modelFallbackMessage?: string;
50
+ resourceLoader: ResourceLoader;
51
+ /**
52
+ * Flush the lanes, then dispose the session. Flush first and let a failure
53
+ * propagate *after* disposal, so a delivery error is never traded for a leaked
54
+ * session — and never swallowed into a clean-looking shutdown.
55
+ */
56
+ close(): Promise<void>;
57
+ }
58
+ export declare function startRun(options: StartRunOptions): Promise<EmbeddedRun>;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Start a pi session with a run's lanes attached.
4
+ *
5
+ * pi ships as ESM only, and every host already imports it dynamically (the
6
+ * desktop main process does this in `bootstrap`, before pi's storage paths are
7
+ * resolved). So the module is *injected* rather than imported here: this package
8
+ * stays CJS+ESM publishable, the host keeps control of when pi loads, and these
9
+ * paths are testable without a model, a network or a filesystem.
10
+ *
11
+ * pi takes extensions through a `ResourceLoader`, not through
12
+ * `createAgentSession`, so embedding means handing the composed factories to a
13
+ * loader — pi's default one, or the host's own (the desktop harness builds one
14
+ * that layers its skills, prompts and templates).
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.startRun = startRun;
18
+ const lanes_1 = require("./lanes");
19
+ async function startRun(options) {
20
+ const cwd = options.cwd ?? process.cwd();
21
+ const agentDir = options.agentDir ?? options.pi.getAgentDir?.();
22
+ if (agentDir === undefined) {
23
+ throw new Error('pi: agentDir is required when the injected pi module has no getAgentDir()');
24
+ }
25
+ const run = (0, lanes_1.composeRun)(options);
26
+ const resourceLoader = await (options.createResourceLoader ?? defaultResourceLoader(options.pi))({
27
+ extensionFactories: run.extensions,
28
+ cwd,
29
+ agentDir
30
+ });
31
+ const result = await options.pi.createAgentSession({ ...options.session, cwd, agentDir, resourceLoader });
32
+ // pi emits `session_start` from `bindExtensions`, which its own CLI modes call
33
+ // once their UI exists — `createAgentSession` never does. An embedder that
34
+ // skips it loads the extensions but never starts them: the metered lane never
35
+ // selects the gateway model, so a cloud run's calls leave on whatever provider
36
+ // key the process happens to hold, unmetered, and the log lane never binds on
37
+ // resume. So the embedding, not the host, fires it.
38
+ await result.session.bindExtensions({});
39
+ assertMeteredModelSelected(run, result.session);
40
+ return {
41
+ run,
42
+ session: result.session,
43
+ extensionsResult: result.extensionsResult,
44
+ ...(result.modelFallbackMessage === undefined ? {} : { modelFallbackMessage: result.modelFallbackMessage }),
45
+ resourceLoader,
46
+ close: async () => {
47
+ let failure;
48
+ try {
49
+ await run.flush();
50
+ }
51
+ catch (error) {
52
+ failure = error;
53
+ }
54
+ result.session.dispose();
55
+ if (failure !== undefined)
56
+ throw failure;
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * The metered lane's whole purpose is that usage cannot be under-reported, so a
62
+ * session that ended up on some other provider must fail the run rather than
63
+ * quietly bill nothing.
64
+ */
65
+ function assertMeteredModelSelected(run, session) {
66
+ const lane = run.lanes.meteredModel;
67
+ if (!lane?.selectedModel)
68
+ return;
69
+ const model = session.model;
70
+ if (model?.provider === lane.providerName && model.id === lane.selectedModel)
71
+ return;
72
+ throw new Error(`pi: the metered lane selected "${lane.providerName}/${lane.selectedModel}" but the ` +
73
+ `session is on "${model ? `${model.provider}/${model.id}` : 'no model'}" — model calls would ` +
74
+ 'leave outside the gateway and go unmetered');
75
+ }
76
+ const defaultResourceLoader = (pi) => async (request) => {
77
+ const loader = new pi.DefaultResourceLoader({
78
+ cwd: request.cwd,
79
+ agentDir: request.agentDir,
80
+ extensionFactories: request.extensionFactories
81
+ });
82
+ // pi's loader discovers nothing until it is reloaded once.
83
+ await loader.reload();
84
+ return loader;
85
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Compose a run's lanes into the extension list pi loads.
3
+ *
4
+ * A run's placement — a developer's laptop, a long-running cloud job — is *only*
5
+ * a difference in these options: which store the log appends to, whether the
6
+ * model calls leave through the metered gateway or a local key, and where an
7
+ * approval question is asked. The composed session is otherwise identical, which
8
+ * is the whole point of the package.
9
+ */
10
+ import type { MeteredIdentity } from '@agentic-kit/metering';
11
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
12
+ import { type GateExtension, type GateExtensionOptions } from '../extensions/gate';
13
+ import { type MeteredModelExtension, type MeteredModelExtensionOptions } from '../extensions/metered-model';
14
+ import { type RunLogExtension, type RunLogExtensionOptions } from '../extensions/run-log';
15
+ import { type UsageReportExtension, type UsageReportExtensionOptions } from '../extensions/usage-report';
16
+ /** Run log lane — `runId` comes from the run, not from here. */
17
+ export type RunLogLane = Omit<RunLogExtensionOptions, 'runId'>;
18
+ /**
19
+ * Metering lane. `gateway` is authoritative (the gateway meters what it proxies);
20
+ * `self-report` is the own-provider-key lane and is only as trustworthy as the
21
+ * agent reporting it. They are mutually exclusive on purpose: routing through the
22
+ * gateway *and* self-reporting would double-count the same tokens.
23
+ */
24
+ export type MeteringLane = ({
25
+ mode: 'gateway';
26
+ } & MeteredModelExtensionOptions) | ({
27
+ mode: 'self-report';
28
+ } & UsageReportExtensionOptions);
29
+ /** Approval lane — `runId` comes from the run. */
30
+ export type GateLane = Omit<GateExtensionOptions, 'runId'>;
31
+ export interface ComposeRunOptions {
32
+ runId: string;
33
+ log?: RunLogLane;
34
+ metering?: MeteringLane;
35
+ gate?: GateLane;
36
+ /**
37
+ * The host's own extensions — workspace tools, prompts, UI glue. Loaded after
38
+ * the lanes so a host tool call is already gated and already logged.
39
+ */
40
+ extensions?: readonly ExtensionFactory[];
41
+ }
42
+ export interface ComposedLanes {
43
+ log?: RunLogExtension;
44
+ meteredModel?: MeteredModelExtension;
45
+ usageReport?: UsageReportExtension;
46
+ gate?: GateExtension;
47
+ }
48
+ export interface ComposedRun {
49
+ runId: string;
50
+ /** In load order: log, metering, gate, then the host's own. */
51
+ extensions: ExtensionFactory[];
52
+ lanes: ComposedLanes;
53
+ /**
54
+ * Drain every lane that buffers, and throw the first failure. A host calls
55
+ * this before it exits; the extensions also flush themselves on
56
+ * `session_shutdown`, so this is for hosts that end a run without one.
57
+ */
58
+ flush(): Promise<void>;
59
+ }
60
+ export declare function composeRun(options: ComposeRunOptions): ComposedRun;
61
+ /**
62
+ * The identity every metered lane needs. Exported so a host can build it once
63
+ * from its resolved project context and hand the same value to both lanes.
64
+ */
65
+ export type { MeteredIdentity };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Compose a run's lanes into the extension list pi loads.
3
+ *
4
+ * A run's placement — a developer's laptop, a long-running cloud job — is *only*
5
+ * a difference in these options: which store the log appends to, whether the
6
+ * model calls leave through the metered gateway or a local key, and where an
7
+ * approval question is asked. The composed session is otherwise identical, which
8
+ * is the whole point of the package.
9
+ */
10
+ import { createGateExtension } from '../extensions/gate';
11
+ import { createMeteredModelExtension } from '../extensions/metered-model';
12
+ import { createRunLogExtension } from '../extensions/run-log';
13
+ import { createUsageReportExtension } from '../extensions/usage-report';
14
+ export function composeRun(options) {
15
+ const lanes = {};
16
+ const extensions = [];
17
+ if (options.log) {
18
+ lanes.log = createRunLogExtension({ runId: options.runId, ...options.log });
19
+ extensions.push(lanes.log.extension);
20
+ }
21
+ const metering = options.metering;
22
+ if (metering?.mode === 'gateway') {
23
+ const { mode: _mode, ...meteredOptions } = metering;
24
+ lanes.meteredModel = createMeteredModelExtension(meteredOptions);
25
+ extensions.push(lanes.meteredModel.extension);
26
+ }
27
+ else if (metering?.mode === 'self-report') {
28
+ const { mode: _mode, ...reportOptions } = metering;
29
+ lanes.usageReport = createUsageReportExtension(reportOptions);
30
+ extensions.push(lanes.usageReport.extension);
31
+ }
32
+ if (options.gate) {
33
+ lanes.gate = createGateExtension({ runId: options.runId, ...options.gate });
34
+ extensions.push(lanes.gate.extension);
35
+ }
36
+ if (options.extensions)
37
+ extensions.push(...options.extensions);
38
+ return {
39
+ runId: options.runId,
40
+ extensions,
41
+ lanes,
42
+ flush: async () => {
43
+ // Sequential, log first: a usage failure must not cost us the transcript.
44
+ if (lanes.log)
45
+ await lanes.log.flush();
46
+ if (lanes.usageReport)
47
+ await lanes.usageReport.flush();
48
+ }
49
+ };
50
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Start a pi session with a run's lanes attached.
3
+ *
4
+ * pi ships as ESM only, and every host already imports it dynamically (the
5
+ * desktop main process does this in `bootstrap`, before pi's storage paths are
6
+ * resolved). So the module is *injected* rather than imported here: this package
7
+ * stays CJS+ESM publishable, the host keeps control of when pi loads, and these
8
+ * paths are testable without a model, a network or a filesystem.
9
+ *
10
+ * pi takes extensions through a `ResourceLoader`, not through
11
+ * `createAgentSession`, so embedding means handing the composed factories to a
12
+ * loader — pi's default one, or the host's own (the desktop harness builds one
13
+ * that layers its skills, prompts and templates).
14
+ */
15
+ import type { CreateAgentSessionOptions, CreateAgentSessionResult, ExtensionFactory, ResourceLoader } from '@earendil-works/pi-coding-agent';
16
+ import { type ComposedRun, type ComposeRunOptions } from './lanes';
17
+ export interface ResourceLoaderRequest {
18
+ /** The lane extensions, in load order. Give these to the loader. */
19
+ extensionFactories: ExtensionFactory[];
20
+ cwd: string;
21
+ agentDir: string;
22
+ }
23
+ export type CreateResourceLoader = (request: ResourceLoaderRequest) => ResourceLoader | Promise<ResourceLoader>;
24
+ /** The slice of the pi module this package needs, so a host can inject it. */
25
+ export interface PiModule {
26
+ createAgentSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
27
+ DefaultResourceLoader: new (options: {
28
+ cwd: string;
29
+ agentDir: string;
30
+ extensionFactories?: ExtensionFactory[];
31
+ }) => ResourceLoader;
32
+ getAgentDir?: () => string;
33
+ }
34
+ export interface StartRunOptions extends ComposeRunOptions {
35
+ /** `await import('@earendil-works/pi-coding-agent')`. */
36
+ pi: PiModule;
37
+ cwd?: string;
38
+ /** pi's config/storage dir. Defaults to `pi.getAgentDir()` when available. */
39
+ agentDir?: string;
40
+ /** Passed through to pi, minus the ones this package owns. */
41
+ session?: Omit<CreateAgentSessionOptions, 'cwd' | 'agentDir' | 'resourceLoader'>;
42
+ /** Layer the lanes onto a host's own loader instead of pi's default. */
43
+ createResourceLoader?: CreateResourceLoader;
44
+ }
45
+ export interface EmbeddedRun {
46
+ run: ComposedRun;
47
+ session: CreateAgentSessionResult['session'];
48
+ extensionsResult: CreateAgentSessionResult['extensionsResult'];
49
+ modelFallbackMessage?: string;
50
+ resourceLoader: ResourceLoader;
51
+ /**
52
+ * Flush the lanes, then dispose the session. Flush first and let a failure
53
+ * propagate *after* disposal, so a delivery error is never traded for a leaked
54
+ * session — and never swallowed into a clean-looking shutdown.
55
+ */
56
+ close(): Promise<void>;
57
+ }
58
+ export declare function startRun(options: StartRunOptions): Promise<EmbeddedRun>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Start a pi session with a run's lanes attached.
3
+ *
4
+ * pi ships as ESM only, and every host already imports it dynamically (the
5
+ * desktop main process does this in `bootstrap`, before pi's storage paths are
6
+ * resolved). So the module is *injected* rather than imported here: this package
7
+ * stays CJS+ESM publishable, the host keeps control of when pi loads, and these
8
+ * paths are testable without a model, a network or a filesystem.
9
+ *
10
+ * pi takes extensions through a `ResourceLoader`, not through
11
+ * `createAgentSession`, so embedding means handing the composed factories to a
12
+ * loader — pi's default one, or the host's own (the desktop harness builds one
13
+ * that layers its skills, prompts and templates).
14
+ */
15
+ import { composeRun } from './lanes';
16
+ export async function startRun(options) {
17
+ const cwd = options.cwd ?? process.cwd();
18
+ const agentDir = options.agentDir ?? options.pi.getAgentDir?.();
19
+ if (agentDir === undefined) {
20
+ throw new Error('pi: agentDir is required when the injected pi module has no getAgentDir()');
21
+ }
22
+ const run = composeRun(options);
23
+ const resourceLoader = await (options.createResourceLoader ?? defaultResourceLoader(options.pi))({
24
+ extensionFactories: run.extensions,
25
+ cwd,
26
+ agentDir
27
+ });
28
+ const result = await options.pi.createAgentSession({ ...options.session, cwd, agentDir, resourceLoader });
29
+ // pi emits `session_start` from `bindExtensions`, which its own CLI modes call
30
+ // once their UI exists — `createAgentSession` never does. An embedder that
31
+ // skips it loads the extensions but never starts them: the metered lane never
32
+ // selects the gateway model, so a cloud run's calls leave on whatever provider
33
+ // key the process happens to hold, unmetered, and the log lane never binds on
34
+ // resume. So the embedding, not the host, fires it.
35
+ await result.session.bindExtensions({});
36
+ assertMeteredModelSelected(run, result.session);
37
+ return {
38
+ run,
39
+ session: result.session,
40
+ extensionsResult: result.extensionsResult,
41
+ ...(result.modelFallbackMessage === undefined ? {} : { modelFallbackMessage: result.modelFallbackMessage }),
42
+ resourceLoader,
43
+ close: async () => {
44
+ let failure;
45
+ try {
46
+ await run.flush();
47
+ }
48
+ catch (error) {
49
+ failure = error;
50
+ }
51
+ result.session.dispose();
52
+ if (failure !== undefined)
53
+ throw failure;
54
+ }
55
+ };
56
+ }
57
+ /**
58
+ * The metered lane's whole purpose is that usage cannot be under-reported, so a
59
+ * session that ended up on some other provider must fail the run rather than
60
+ * quietly bill nothing.
61
+ */
62
+ function assertMeteredModelSelected(run, session) {
63
+ const lane = run.lanes.meteredModel;
64
+ if (!lane?.selectedModel)
65
+ return;
66
+ const model = session.model;
67
+ if (model?.provider === lane.providerName && model.id === lane.selectedModel)
68
+ return;
69
+ throw new Error(`pi: the metered lane selected "${lane.providerName}/${lane.selectedModel}" but the ` +
70
+ `session is on "${model ? `${model.provider}/${model.id}` : 'no model'}" — model calls would ` +
71
+ 'leave outside the gateway and go unmetered');
72
+ }
73
+ const defaultResourceLoader = (pi) => async (request) => {
74
+ const loader = new pi.DefaultResourceLoader({
75
+ cwd: request.cwd,
76
+ agentDir: request.agentDir,
77
+ extensionFactories: request.extensionFactories
78
+ });
79
+ // pi's loader discovers nothing until it is reloaded once.
80
+ await loader.reload();
81
+ return loader;
82
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The pi side of the run gate: map pi's `tool_call` event onto the neutral
3
+ * `RunGate` and turn its outcome into pi's block shape.
4
+ *
5
+ * pi's `tool_call` handler can await, so an `ask` verdict simply suspends the
6
+ * tool until the approval channel resolves. That is the whole mechanism for
7
+ * "approve a cloud agent's `rm -rf` from a browser tab" — and it is all the
8
+ * harness-specific code the gate needs; the policy, the approval rendezvous and
9
+ * the audit record live in `@agentic-kit/harness`.
10
+ */
11
+ import { type ApprovalRequest, type RunGate, type RunGateOptions, type RunGatePolicy } from '@agentic-kit/harness';
12
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
13
+ export type GateExtensionOptions = RunGateOptions;
14
+ export interface GateExtension {
15
+ extension: ExtensionFactory;
16
+ policy: RunGatePolicy;
17
+ /** Requests currently waiting on a human. */
18
+ pending: ReadonlyMap<string, ApprovalRequest>;
19
+ gate: RunGate;
20
+ }
21
+ export declare function createGateExtension(options: GateExtensionOptions): GateExtension;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The pi side of the run gate: map pi's `tool_call` event onto the neutral
3
+ * `RunGate` and turn its outcome into pi's block shape.
4
+ *
5
+ * pi's `tool_call` handler can await, so an `ask` verdict simply suspends the
6
+ * tool until the approval channel resolves. That is the whole mechanism for
7
+ * "approve a cloud agent's `rm -rf` from a browser tab" — and it is all the
8
+ * harness-specific code the gate needs; the policy, the approval rendezvous and
9
+ * the audit record live in `@agentic-kit/harness`.
10
+ */
11
+ import { createRunGate } from '@agentic-kit/harness';
12
+ export function createGateExtension(options) {
13
+ const gate = createRunGate(options);
14
+ const extension = (pi) => {
15
+ pi.on('tool_call', async (event) => {
16
+ const outcome = await gate.decide({
17
+ toolCallId: event.toolCallId,
18
+ toolName: event.toolName,
19
+ input: (event.input ?? {})
20
+ });
21
+ if (outcome.allowed)
22
+ return {};
23
+ return { block: true, ...(outcome.reason === undefined ? {} : { reason: outcome.reason }) };
24
+ });
25
+ };
26
+ return { extension, policy: gate.policy, pending: gate.pending, gate };
27
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The pi side of the metering lane: register the resolved gateway as a pi
3
+ * provider, optionally as the session's model.
4
+ *
5
+ * Every model call then leaves the process through `agentic-server`, which is the
6
+ * billing authority, so usage cannot be under-reported by the agent. The
7
+ * endpoint itself is resolved by `@agentic-kit/metering`; only the registration
8
+ * shape below is pi's. The self-reporting lane (own provider keys) is
9
+ * `./usage-report`.
10
+ */
11
+ import { type MeteredGateway, type MeteredGatewayOptions } from '@agentic-kit/metering';
12
+ import type { ExtensionFactory, ProviderConfig } from '@earendil-works/pi-coding-agent';
13
+ export interface MeteredModelExtensionOptions extends MeteredGatewayOptions {
14
+ /**
15
+ * Model id to select once registered. Defaults to the first model; pass `false`
16
+ * to leave the host's model choice alone.
17
+ */
18
+ selectModel?: string | false;
19
+ }
20
+ export interface MeteredModelExtension {
21
+ extension: ExtensionFactory;
22
+ /** The config handed to pi — asserted in tests, useful for host logging. */
23
+ config: ProviderConfig;
24
+ /** The neutral endpoint the config was built from. */
25
+ gateway: MeteredGateway;
26
+ providerName: string;
27
+ /** Model id pi selects on session start, if any. */
28
+ selectedModel: string | undefined;
29
+ }
30
+ /** Shape a resolved gateway as pi's provider registration. */
31
+ export declare function piProviderConfig(gateway: MeteredGateway): ProviderConfig;
32
+ export declare function createMeteredModelExtension(options: MeteredModelExtensionOptions): MeteredModelExtension;