@agentic-kit/pi 0.15.1 → 0.17.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.
@@ -0,0 +1,50 @@
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 { DEFAULT_PROVIDER_NAME, resolveMeteredGateway } from '@agentic-kit/metering';
12
+ /** Shape a resolved gateway as pi's provider registration. */
13
+ export function piProviderConfig(gateway) {
14
+ return {
15
+ name: gateway.displayName,
16
+ baseUrl: gateway.baseUrl,
17
+ api: gateway.api,
18
+ headers: gateway.headers,
19
+ apiKey: gateway.apiKey,
20
+ models: gateway.models.map((model) => ({ ...model }))
21
+ };
22
+ }
23
+ export function createMeteredModelExtension(options) {
24
+ const providerName = options.providerName ?? DEFAULT_PROVIDER_NAME;
25
+ const gateway = resolveMeteredGateway(options);
26
+ const config = piProviderConfig(gateway);
27
+ const modelId = options.selectModel === false ? undefined : (options.selectModel ?? options.models[0].id);
28
+ if (modelId !== undefined && !options.models.some((model) => model.id === modelId)) {
29
+ // Selecting an unregistered id silently leaves pi on an unmetered model,
30
+ // which is the one failure this lane exists to prevent.
31
+ throw new Error(`metered model: selectModel "${modelId}" is not one of the registered models`);
32
+ }
33
+ const extension = (pi) => {
34
+ pi.registerProvider(providerName, config);
35
+ if (modelId === undefined)
36
+ return;
37
+ // `pi.setModel` takes a resolved model, and the registry that resolves it
38
+ // lives on the context — which an extension only gets on an event.
39
+ // `session_start` is the first, and fires before the first turn.
40
+ pi.on('session_start', async (_event, ctx) => {
41
+ const model = ctx.modelRegistry.find(providerName, modelId);
42
+ if (!model)
43
+ throw new Error(`metered model: provider "${providerName}" has no model "${modelId}" after registration`);
44
+ const ok = await pi.setModel(model);
45
+ if (!ok)
46
+ throw new Error(`metered model: pi refused model "${providerName}/${modelId}" (no usable credentials for the gateway)`);
47
+ });
48
+ };
49
+ return { extension, config, gateway, providerName, selectedModel: modelId };
50
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The pi side of the run log: drain the neutral session mirror after anything
3
+ * that can append an entry. Every listed event is a point where pi has just
4
+ * written to the session, so the run log trails the session by at most one event.
5
+ */
6
+ import { type RunEventRecord, type RunLogAppendStore, SessionMirror } from '@agentic-kit/run-log';
7
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
8
+ /**
9
+ * Events after which the session may have grown. `session_start` also covers
10
+ * resume (pi replays the file before the first event), so a resumed run
11
+ * re-appends its history and the store's idempotency discards the duplicates.
12
+ */
13
+ export declare const MIRROR_EVENTS: readonly ["session_start", "session_compact", "session_tree", "session_shutdown", "input", "before_agent_start", "message_end", "tool_execution_end", "turn_end", "agent_end", "model_select", "thinking_level_select"];
14
+ export type MirrorEvent = (typeof MIRROR_EVENTS)[number];
15
+ export interface RunLogExtensionOptions {
16
+ /** The run this session belongs to. Local and cloud runs differ only here. */
17
+ runId: string;
18
+ store: RunLogAppendStore;
19
+ /** pi session format version the entries are produced under. */
20
+ transcriptVersion?: number;
21
+ events?: readonly MirrorEvent[];
22
+ /**
23
+ * Called when a drain fails. Without it the failure is rethrown into pi's
24
+ * event dispatch: a run log that silently stops recording is worse than a
25
+ * loud one, so losing entries is never the default.
26
+ */
27
+ onError?: (error: unknown) => void;
28
+ }
29
+ export interface RunLogExtension {
30
+ extension: ExtensionFactory;
31
+ /** Drain now — for a host that wants the log flushed before it exits. */
32
+ flush(): Promise<RunEventRecord[]>;
33
+ mirror: SessionMirror;
34
+ }
35
+ export declare function createRunLogExtension(options: RunLogExtensionOptions): RunLogExtension;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The pi side of the run log: drain the neutral session mirror after anything
3
+ * that can append an entry. Every listed event is a point where pi has just
4
+ * written to the session, so the run log trails the session by at most one event.
5
+ */
6
+ import { PI_TRANSCRIPT_FORMAT, SessionMirror } from '@agentic-kit/run-log';
7
+ /**
8
+ * Events after which the session may have grown. `session_start` also covers
9
+ * resume (pi replays the file before the first event), so a resumed run
10
+ * re-appends its history and the store's idempotency discards the duplicates.
11
+ */
12
+ export const MIRROR_EVENTS = [
13
+ 'session_start',
14
+ 'session_compact',
15
+ 'session_tree',
16
+ 'session_shutdown',
17
+ 'input',
18
+ 'before_agent_start',
19
+ 'message_end',
20
+ 'tool_execution_end',
21
+ 'turn_end',
22
+ 'agent_end',
23
+ 'model_select',
24
+ 'thinking_level_select'
25
+ ];
26
+ export function createRunLogExtension(options) {
27
+ const mirror = new SessionMirror({
28
+ runId: options.runId,
29
+ store: options.store,
30
+ transcriptFormat: PI_TRANSCRIPT_FORMAT,
31
+ ...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
32
+ });
33
+ const events = options.events ?? MIRROR_EVENTS;
34
+ const drain = async () => {
35
+ try {
36
+ return await mirror.drain();
37
+ }
38
+ catch (error) {
39
+ if (!options.onError)
40
+ throw error;
41
+ options.onError(error);
42
+ return [];
43
+ }
44
+ };
45
+ const extension = (pi) => {
46
+ for (const event of events) {
47
+ // Each overload of `on` is typed for its own handler; the handler here
48
+ // ignores the event and only reads the context, so one cast at the
49
+ // registration boundary keeps the loop.
50
+ pi.on(event, async (_event, ctx) => {
51
+ mirror.bind(ctx.sessionManager);
52
+ await drain();
53
+ });
54
+ }
55
+ };
56
+ return { extension, flush: drain, mirror };
57
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The pi side of the self-reporting lane: report each assistant message's usage
3
+ * to the gateway.
4
+ *
5
+ * This is the local / own-provider-key lane. The agent's own numbers are the only
6
+ * source, so a report is *self-reported* — good for reconciliation and for usage
7
+ * visibility on a developer's own key, not tamper-proof billing. When the run
8
+ * must be billed authoritatively, route the model calls through the gateway with
9
+ * `./metered-model` instead; the gateway then meters itself and this extension is
10
+ * unnecessary.
11
+ *
12
+ * Everything but the two event hooks below is `@agentic-kit/metering`.
13
+ */
14
+ import { type MeteredIdentity, type UsageReport, UsageReporter } from '@agentic-kit/metering';
15
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
16
+ export interface UsageReportExtensionOptions {
17
+ identity: MeteredIdentity;
18
+ /** Gateway root. Required unless a custom `sink` is supplied. */
19
+ gatewayUrl?: string;
20
+ /** Delivery override — e.g. an in-process sink, or a queue. */
21
+ sink?: (report: UsageReport) => Promise<void>;
22
+ fetch?: typeof globalThis.fetch;
23
+ /** `operation` column value; defaults to `agent/chat`. */
24
+ operation?: string;
25
+ /** Report failures here instead of having `flush()` rethrow them. */
26
+ onError?: (error: unknown, report: UsageReport) => void;
27
+ }
28
+ export interface UsageReportExtension {
29
+ extension: ExtensionFactory;
30
+ /** Drain the queue and rethrow the first delivery failure. */
31
+ flush(): Promise<void>;
32
+ reporter: UsageReporter;
33
+ }
34
+ export declare function createUsageReportExtension(options: UsageReportExtensionOptions): UsageReportExtension;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The pi side of the self-reporting lane: report each assistant message's usage
3
+ * to the gateway.
4
+ *
5
+ * This is the local / own-provider-key lane. The agent's own numbers are the only
6
+ * source, so a report is *self-reported* — good for reconciliation and for usage
7
+ * visibility on a developer's own key, not tamper-proof billing. When the run
8
+ * must be billed authoritatively, route the model calls through the gateway with
9
+ * `./metered-model` instead; the gateway then meters itself and this extension is
10
+ * unnecessary.
11
+ *
12
+ * Everything but the two event hooks below is `@agentic-kit/metering`.
13
+ */
14
+ import { httpUsageSink, isAssistantMessage, toUsageReport, UsageReporter } from '@agentic-kit/metering';
15
+ export function createUsageReportExtension(options) {
16
+ const sink = options.sink ??
17
+ httpUsageSink({
18
+ gatewayUrl: requireGatewayUrl(options),
19
+ identity: options.identity,
20
+ ...(options.fetch ? { fetch: options.fetch } : {})
21
+ });
22
+ const reporter = new UsageReporter({ sink, ...(options.onError ? { onError: options.onError } : {}) });
23
+ // pi can emit `message_end` more than once for the same response (a rewritten
24
+ // message, a replayed entry on resume), and each emission would otherwise bill
25
+ // again. `responseId` is the provider's identity for the response, so it is
26
+ // what dedupes; messages without one are reported as-is.
27
+ const seen = new Set();
28
+ const extension = (pi) => {
29
+ pi.on('message_end', (event) => {
30
+ const message = event.message;
31
+ if (!isAssistantMessage(message))
32
+ return;
33
+ const responseId = message.responseId;
34
+ if (typeof responseId === 'string' && responseId.length > 0) {
35
+ if (seen.has(responseId))
36
+ return;
37
+ seen.add(responseId);
38
+ }
39
+ const report = toUsageReport(message, options.operation === undefined ? {} : { operation: options.operation });
40
+ if (report)
41
+ reporter.enqueue(report);
42
+ });
43
+ pi.on('session_shutdown', async () => {
44
+ await reporter.flush();
45
+ });
46
+ };
47
+ return { extension, flush: () => reporter.flush(), reporter };
48
+ }
49
+ function requireGatewayUrl(options) {
50
+ if (!options.gatewayUrl)
51
+ throw new Error('usage report: gatewayUrl is required unless a custom sink is provided');
52
+ return options.gatewayUrl;
53
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * pi as a `HarnessAdapter`.
3
+ *
4
+ * The adapter is thin by design: everything a run actually does — gating, run
5
+ * log, metering — already lives behind neutral contracts, so all that is left
6
+ * here is pi's identity, the transcript it writes, and its way of attaching
7
+ * extensions (a resource loader). A second harness implements the same three
8
+ * things against its own plugin surface, and hosts keep calling `startRun`.
9
+ */
10
+ import type { HarnessAdapter, HarnessRun } from '@agentic-kit/harness';
11
+ import { type EmbeddedRun, type StartRunOptions } from './embed/session';
12
+ export declare const PI_HARNESS_ID = "pi";
13
+ export interface PiHarnessRun extends EmbeddedRun, HarnessRun {
14
+ readonly runId: string;
15
+ }
16
+ export declare const piHarness: HarnessAdapter<StartRunOptions, PiHarnessRun>;
package/esm/harness.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * pi as a `HarnessAdapter`.
3
+ *
4
+ * The adapter is thin by design: everything a run actually does — gating, run
5
+ * log, metering — already lives behind neutral contracts, so all that is left
6
+ * here is pi's identity, the transcript it writes, and its way of attaching
7
+ * extensions (a resource loader). A second harness implements the same three
8
+ * things against its own plugin surface, and hosts keep calling `startRun`.
9
+ */
10
+ import { PI_TRANSCRIPT_FORMAT } from '@agentic-kit/run-log';
11
+ import { startRun } from './embed/session';
12
+ export const PI_HARNESS_ID = 'pi';
13
+ export const piHarness = {
14
+ id: PI_HARNESS_ID,
15
+ transcriptFormat: PI_TRANSCRIPT_FORMAT,
16
+ async startRun(options) {
17
+ const embedded = await startRun(options);
18
+ return {
19
+ ...embedded,
20
+ runId: embedded.run.runId,
21
+ flush: () => embedded.run.flush()
22
+ };
23
+ }
24
+ };
package/esm/index.d.ts CHANGED
@@ -5,6 +5,13 @@ export declare const dbTools: ExtensionFactory;
5
5
  export declare function createDbTools(host: PiToolsHost): ExtensionFactory;
6
6
  export { type ConfirmGate, type ConfirmGateDeps, type ConfirmGateOptions, createConfirmGate, } from './confirm-gate';
7
7
  export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, type ContextEnvKey, type ContextSource, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, type ModulesClient, type ProjectContext, type ProjectContextFailureCode, resolveDataToken, resolveProjectContext, } from './context';
8
+ export { type ComposedLanes, type ComposedRun, composeRun, type ComposeRunOptions, type GateLane, type MeteringLane, type RunLogLane, } from './embed/lanes';
9
+ export { type CreateResourceLoader, type EmbeddedRun, type PiModule, type ResourceLoaderRequest, startRun, type StartRunOptions, } from './embed/session';
10
+ export { createGateExtension, type GateExtension, type GateExtensionOptions, } from './extensions/gate';
11
+ export { createMeteredModelExtension, type MeteredModelExtension, type MeteredModelExtensionOptions, piProviderConfig, } from './extensions/metered-model';
12
+ export { createRunLogExtension, MIRROR_EVENTS, type MirrorEvent, type RunLogExtension, type RunLogExtensionOptions, } from './extensions/run-log';
13
+ export { createUsageReportExtension, type UsageReportExtension, type UsageReportExtensionOptions, } from './extensions/usage-report';
14
+ export { PI_HARNESS_ID, piHarness, type PiHarnessRun } from './harness';
8
15
  export { type ActiveDataToken, configureHost, type DataAuthBroker, getHost, type HostAccount, type HostBackendConfig, type HostProvisionOverlay, type PiToolsHost, type PreviewToken, } from './host';
9
16
  export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, type ProvisionManifest, } from './provision-database/manifest';
10
17
  export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, type ModulePreset, type ProvisionModule, } from './provision-database/presets';
package/esm/index.js CHANGED
@@ -46,6 +46,13 @@ export function createDbTools(host) {
46
46
  }
47
47
  export { createConfirmGate, } from './confirm-gate';
48
48
  export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, resolveDataToken, resolveProjectContext, } from './context';
49
+ export { composeRun, } from './embed/lanes';
50
+ export { startRun, } from './embed/session';
51
+ export { createGateExtension, } from './extensions/gate';
52
+ export { createMeteredModelExtension, piProviderConfig, } from './extensions/metered-model';
53
+ export { createRunLogExtension, MIRROR_EVENTS, } from './extensions/run-log';
54
+ export { createUsageReportExtension, } from './extensions/usage-report';
55
+ export { PI_HARNESS_ID, piHarness } from './harness';
49
56
  export { configureHost, getHost, } from './host';
50
57
  export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, } from './provision-database/manifest';
51
58
  export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, } from './provision-database/presets';
@@ -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,30 @@
1
+ "use strict";
2
+ /**
3
+ * The pi side of the run gate: map pi's `tool_call` event onto the neutral
4
+ * `RunGate` and turn its outcome into pi's block shape.
5
+ *
6
+ * pi's `tool_call` handler can await, so an `ask` verdict simply suspends the
7
+ * tool until the approval channel resolves. That is the whole mechanism for
8
+ * "approve a cloud agent's `rm -rf` from a browser tab" — and it is all the
9
+ * harness-specific code the gate needs; the policy, the approval rendezvous and
10
+ * the audit record live in `@agentic-kit/harness`.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.createGateExtension = createGateExtension;
14
+ const harness_1 = require("@agentic-kit/harness");
15
+ function createGateExtension(options) {
16
+ const gate = (0, harness_1.createRunGate)(options);
17
+ const extension = (pi) => {
18
+ pi.on('tool_call', async (event) => {
19
+ const outcome = await gate.decide({
20
+ toolCallId: event.toolCallId,
21
+ toolName: event.toolName,
22
+ input: (event.input ?? {})
23
+ });
24
+ if (outcome.allowed)
25
+ return {};
26
+ return { block: true, ...(outcome.reason === undefined ? {} : { reason: outcome.reason }) };
27
+ });
28
+ };
29
+ return { extension, policy: gate.policy, pending: gate.pending, gate };
30
+ }
@@ -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;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * The pi side of the metering lane: register the resolved gateway as a pi
4
+ * provider, optionally as the session's model.
5
+ *
6
+ * Every model call then leaves the process through `agentic-server`, which is the
7
+ * billing authority, so usage cannot be under-reported by the agent. The
8
+ * endpoint itself is resolved by `@agentic-kit/metering`; only the registration
9
+ * shape below is pi's. The self-reporting lane (own provider keys) is
10
+ * `./usage-report`.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.piProviderConfig = piProviderConfig;
14
+ exports.createMeteredModelExtension = createMeteredModelExtension;
15
+ const metering_1 = require("@agentic-kit/metering");
16
+ /** Shape a resolved gateway as pi's provider registration. */
17
+ function piProviderConfig(gateway) {
18
+ return {
19
+ name: gateway.displayName,
20
+ baseUrl: gateway.baseUrl,
21
+ api: gateway.api,
22
+ headers: gateway.headers,
23
+ apiKey: gateway.apiKey,
24
+ models: gateway.models.map((model) => ({ ...model }))
25
+ };
26
+ }
27
+ function createMeteredModelExtension(options) {
28
+ const providerName = options.providerName ?? metering_1.DEFAULT_PROVIDER_NAME;
29
+ const gateway = (0, metering_1.resolveMeteredGateway)(options);
30
+ const config = piProviderConfig(gateway);
31
+ const modelId = options.selectModel === false ? undefined : (options.selectModel ?? options.models[0].id);
32
+ if (modelId !== undefined && !options.models.some((model) => model.id === modelId)) {
33
+ // Selecting an unregistered id silently leaves pi on an unmetered model,
34
+ // which is the one failure this lane exists to prevent.
35
+ throw new Error(`metered model: selectModel "${modelId}" is not one of the registered models`);
36
+ }
37
+ const extension = (pi) => {
38
+ pi.registerProvider(providerName, config);
39
+ if (modelId === undefined)
40
+ return;
41
+ // `pi.setModel` takes a resolved model, and the registry that resolves it
42
+ // lives on the context — which an extension only gets on an event.
43
+ // `session_start` is the first, and fires before the first turn.
44
+ pi.on('session_start', async (_event, ctx) => {
45
+ const model = ctx.modelRegistry.find(providerName, modelId);
46
+ if (!model)
47
+ throw new Error(`metered model: provider "${providerName}" has no model "${modelId}" after registration`);
48
+ const ok = await pi.setModel(model);
49
+ if (!ok)
50
+ throw new Error(`metered model: pi refused model "${providerName}/${modelId}" (no usable credentials for the gateway)`);
51
+ });
52
+ };
53
+ return { extension, config, gateway, providerName, selectedModel: modelId };
54
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The pi side of the run log: drain the neutral session mirror after anything
3
+ * that can append an entry. Every listed event is a point where pi has just
4
+ * written to the session, so the run log trails the session by at most one event.
5
+ */
6
+ import { type RunEventRecord, type RunLogAppendStore, SessionMirror } from '@agentic-kit/run-log';
7
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
8
+ /**
9
+ * Events after which the session may have grown. `session_start` also covers
10
+ * resume (pi replays the file before the first event), so a resumed run
11
+ * re-appends its history and the store's idempotency discards the duplicates.
12
+ */
13
+ export declare const MIRROR_EVENTS: readonly ["session_start", "session_compact", "session_tree", "session_shutdown", "input", "before_agent_start", "message_end", "tool_execution_end", "turn_end", "agent_end", "model_select", "thinking_level_select"];
14
+ export type MirrorEvent = (typeof MIRROR_EVENTS)[number];
15
+ export interface RunLogExtensionOptions {
16
+ /** The run this session belongs to. Local and cloud runs differ only here. */
17
+ runId: string;
18
+ store: RunLogAppendStore;
19
+ /** pi session format version the entries are produced under. */
20
+ transcriptVersion?: number;
21
+ events?: readonly MirrorEvent[];
22
+ /**
23
+ * Called when a drain fails. Without it the failure is rethrown into pi's
24
+ * event dispatch: a run log that silently stops recording is worse than a
25
+ * loud one, so losing entries is never the default.
26
+ */
27
+ onError?: (error: unknown) => void;
28
+ }
29
+ export interface RunLogExtension {
30
+ extension: ExtensionFactory;
31
+ /** Drain now — for a host that wants the log flushed before it exits. */
32
+ flush(): Promise<RunEventRecord[]>;
33
+ mirror: SessionMirror;
34
+ }
35
+ export declare function createRunLogExtension(options: RunLogExtensionOptions): RunLogExtension;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * The pi side of the run log: drain the neutral session mirror after anything
4
+ * that can append an entry. Every listed event is a point where pi has just
5
+ * written to the session, so the run log trails the session by at most one event.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.MIRROR_EVENTS = void 0;
9
+ exports.createRunLogExtension = createRunLogExtension;
10
+ const run_log_1 = require("@agentic-kit/run-log");
11
+ /**
12
+ * Events after which the session may have grown. `session_start` also covers
13
+ * resume (pi replays the file before the first event), so a resumed run
14
+ * re-appends its history and the store's idempotency discards the duplicates.
15
+ */
16
+ exports.MIRROR_EVENTS = [
17
+ 'session_start',
18
+ 'session_compact',
19
+ 'session_tree',
20
+ 'session_shutdown',
21
+ 'input',
22
+ 'before_agent_start',
23
+ 'message_end',
24
+ 'tool_execution_end',
25
+ 'turn_end',
26
+ 'agent_end',
27
+ 'model_select',
28
+ 'thinking_level_select'
29
+ ];
30
+ function createRunLogExtension(options) {
31
+ const mirror = new run_log_1.SessionMirror({
32
+ runId: options.runId,
33
+ store: options.store,
34
+ transcriptFormat: run_log_1.PI_TRANSCRIPT_FORMAT,
35
+ ...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
36
+ });
37
+ const events = options.events ?? exports.MIRROR_EVENTS;
38
+ const drain = async () => {
39
+ try {
40
+ return await mirror.drain();
41
+ }
42
+ catch (error) {
43
+ if (!options.onError)
44
+ throw error;
45
+ options.onError(error);
46
+ return [];
47
+ }
48
+ };
49
+ const extension = (pi) => {
50
+ for (const event of events) {
51
+ // Each overload of `on` is typed for its own handler; the handler here
52
+ // ignores the event and only reads the context, so one cast at the
53
+ // registration boundary keeps the loop.
54
+ pi.on(event, async (_event, ctx) => {
55
+ mirror.bind(ctx.sessionManager);
56
+ await drain();
57
+ });
58
+ }
59
+ };
60
+ return { extension, flush: drain, mirror };
61
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The pi side of the self-reporting lane: report each assistant message's usage
3
+ * to the gateway.
4
+ *
5
+ * This is the local / own-provider-key lane. The agent's own numbers are the only
6
+ * source, so a report is *self-reported* — good for reconciliation and for usage
7
+ * visibility on a developer's own key, not tamper-proof billing. When the run
8
+ * must be billed authoritatively, route the model calls through the gateway with
9
+ * `./metered-model` instead; the gateway then meters itself and this extension is
10
+ * unnecessary.
11
+ *
12
+ * Everything but the two event hooks below is `@agentic-kit/metering`.
13
+ */
14
+ import { type MeteredIdentity, type UsageReport, UsageReporter } from '@agentic-kit/metering';
15
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
16
+ export interface UsageReportExtensionOptions {
17
+ identity: MeteredIdentity;
18
+ /** Gateway root. Required unless a custom `sink` is supplied. */
19
+ gatewayUrl?: string;
20
+ /** Delivery override — e.g. an in-process sink, or a queue. */
21
+ sink?: (report: UsageReport) => Promise<void>;
22
+ fetch?: typeof globalThis.fetch;
23
+ /** `operation` column value; defaults to `agent/chat`. */
24
+ operation?: string;
25
+ /** Report failures here instead of having `flush()` rethrow them. */
26
+ onError?: (error: unknown, report: UsageReport) => void;
27
+ }
28
+ export interface UsageReportExtension {
29
+ extension: ExtensionFactory;
30
+ /** Drain the queue and rethrow the first delivery failure. */
31
+ flush(): Promise<void>;
32
+ reporter: UsageReporter;
33
+ }
34
+ export declare function createUsageReportExtension(options: UsageReportExtensionOptions): UsageReportExtension;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * The pi side of the self-reporting lane: report each assistant message's usage
4
+ * to the gateway.
5
+ *
6
+ * This is the local / own-provider-key lane. The agent's own numbers are the only
7
+ * source, so a report is *self-reported* — good for reconciliation and for usage
8
+ * visibility on a developer's own key, not tamper-proof billing. When the run
9
+ * must be billed authoritatively, route the model calls through the gateway with
10
+ * `./metered-model` instead; the gateway then meters itself and this extension is
11
+ * unnecessary.
12
+ *
13
+ * Everything but the two event hooks below is `@agentic-kit/metering`.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createUsageReportExtension = createUsageReportExtension;
17
+ const metering_1 = require("@agentic-kit/metering");
18
+ function createUsageReportExtension(options) {
19
+ const sink = options.sink ??
20
+ (0, metering_1.httpUsageSink)({
21
+ gatewayUrl: requireGatewayUrl(options),
22
+ identity: options.identity,
23
+ ...(options.fetch ? { fetch: options.fetch } : {})
24
+ });
25
+ const reporter = new metering_1.UsageReporter({ sink, ...(options.onError ? { onError: options.onError } : {}) });
26
+ // pi can emit `message_end` more than once for the same response (a rewritten
27
+ // message, a replayed entry on resume), and each emission would otherwise bill
28
+ // again. `responseId` is the provider's identity for the response, so it is
29
+ // what dedupes; messages without one are reported as-is.
30
+ const seen = new Set();
31
+ const extension = (pi) => {
32
+ pi.on('message_end', (event) => {
33
+ const message = event.message;
34
+ if (!(0, metering_1.isAssistantMessage)(message))
35
+ return;
36
+ const responseId = message.responseId;
37
+ if (typeof responseId === 'string' && responseId.length > 0) {
38
+ if (seen.has(responseId))
39
+ return;
40
+ seen.add(responseId);
41
+ }
42
+ const report = (0, metering_1.toUsageReport)(message, options.operation === undefined ? {} : { operation: options.operation });
43
+ if (report)
44
+ reporter.enqueue(report);
45
+ });
46
+ pi.on('session_shutdown', async () => {
47
+ await reporter.flush();
48
+ });
49
+ };
50
+ return { extension, flush: () => reporter.flush(), reporter };
51
+ }
52
+ function requireGatewayUrl(options) {
53
+ if (!options.gatewayUrl)
54
+ throw new Error('usage report: gatewayUrl is required unless a custom sink is provided');
55
+ return options.gatewayUrl;
56
+ }
package/harness.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * pi as a `HarnessAdapter`.
3
+ *
4
+ * The adapter is thin by design: everything a run actually does — gating, run
5
+ * log, metering — already lives behind neutral contracts, so all that is left
6
+ * here is pi's identity, the transcript it writes, and its way of attaching
7
+ * extensions (a resource loader). A second harness implements the same three
8
+ * things against its own plugin surface, and hosts keep calling `startRun`.
9
+ */
10
+ import type { HarnessAdapter, HarnessRun } from '@agentic-kit/harness';
11
+ import { type EmbeddedRun, type StartRunOptions } from './embed/session';
12
+ export declare const PI_HARNESS_ID = "pi";
13
+ export interface PiHarnessRun extends EmbeddedRun, HarnessRun {
14
+ readonly runId: string;
15
+ }
16
+ export declare const piHarness: HarnessAdapter<StartRunOptions, PiHarnessRun>;