@agentic-kit/run-log 0.2.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ <p align="center" width="100%">
2
+ <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
3
+ </p>
4
+
5
+ # @agentic-kit/run-log
6
+
7
+ The append-only **run log**: one ordered record of what an agent run did, wherever it ran.
8
+
9
+ A coding agent run has to be observable from a desktop app, a web UI and a CLI; it has to be resumable after a crash, a restart, or a move from the cloud to a laptop; and it has to be meterable. Those are usually four subsystems. Here they are four *projections* of one log:
10
+
11
+ ```
12
+ pi session entries ──► run log (append-only) ──┬──► transcript parts (what a UI draws)
13
+ ├──► usage totals (what a run cost)
14
+ ├──► tool + approval (what is blocked)
15
+ └──► pi session file (how it resumes)
16
+ ```
17
+
18
+ The log stores **pi entries verbatim** under four platform-owned fields. pi already versions and migrates its own session format, so re-encoding it into a second semantic event model would mean maintaining a translation layer that silently loses whatever pi adds next.
19
+
20
+ ```ts
21
+ {
22
+ runId, // which run
23
+ seq, // 1-based, gapless, strictly increasing
24
+ recordedAt, // when the platform durably recorded it
25
+ piSessionVersion, // pi's session format version at write time
26
+ entry // the pi entry, byte-for-byte
27
+ }
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ npm install @agentic-kit/run-log
34
+ ```
35
+
36
+ ## Writing
37
+
38
+ Writing is an `append` against a store. Appends are **idempotent by pi entry id**, so a writer that restarts and replays its tail does not duplicate history.
39
+
40
+ ```ts
41
+ import { MemoryRunLogStore } from '@agentic-kit/run-log';
42
+
43
+ const store = new MemoryRunLogStore();
44
+ await store.append('run-1', [entry]); // → the records actually written
45
+ await store.append('run-1', [entry]); // → [] — already present
46
+ ```
47
+
48
+ Two stores ship here: `MemoryRunLogStore` (the reference implementation, and the test double) and `FileRunLogStore` from the node-only entry point:
49
+
50
+ ```ts
51
+ import { FileRunLogStore, writeSessionFile } from '@agentic-kit/run-log/file-store';
52
+ ```
53
+
54
+ The main entry is **browser-safe** — renderers import the projectors, so nothing in it may reach for a node builtin. Anything touching the filesystem lives behind `/file-store` (the same split, for the same reason, as `12factor-env/dotenv`).
55
+
56
+ The durable store is Postgres, in `constructive-db`: `append` becomes an insert, `read` a keyset scan. Neither is in this package, because the interface is the contract.
57
+
58
+ ## Reading
59
+
60
+ Every surface is the same reader: hold a cursor, ask for what came after it. That is what makes a local run and a cloud run indistinguishable to a UI.
61
+
62
+ ```ts
63
+ import { follow, projectParts, readAll } from '@agentic-kit/run-log';
64
+
65
+ const records = await readAll(store, 'run-1');
66
+
67
+ for await (const batch of follow(store, 'run-1', { waitForChange })) {
68
+ render(projectParts(batch).parts);
69
+ }
70
+ ```
71
+
72
+ `waitForChange` (Postgres `LISTEN/NOTIFY`, IPC, a websocket) is an *optimisation*: it races the poll delay, and without one `follow` degrades to polling. No new transport is required for a run to stream.
73
+
74
+ ## Projections
75
+
76
+ | Projection | Answers |
77
+ | --- | --- |
78
+ | `projectParts` | the renderable transcript — a tool call and its later result collapse into one part, so no renderer correlates messages itself |
79
+ | `projectUsage` | tokens and cost, per `provider/model` and per run, including nested tool usage and compaction calls |
80
+ | `projectToolState` | what is running, and what is waiting on a human (approvals ride in the log as pi `custom` messages, so a surface that reconnects hours later still sees a pending request) |
81
+ | `projectSession` | a pi session file — the resume path, cloud → local included |
82
+
83
+ All four are pure and total: the same records always produce the same output, whichever host wrote them, which is the property the tests assert as *placement invariance*.
84
+
85
+ Unreadable input fails loudly. A corrupt record, a mixed-version log, or a session whose header is not first throws rather than degrading into an empty transcript — a run that silently renders as blank is worse than one that reports it cannot be read. An *unknown* entry type is different: it is carried through as an `unknown` part, so a log written by a newer pi still renders.
86
+
87
+ ## Related
88
+
89
+ - [`@agentic-kit/pi`](../pi) — Constructive's typed db tools as a pi extension
90
+ - [`@agentic-kit/harness`](../harness) — host-neutral gates and policy
91
+ - [`agentic-server`](../agentic-server) — the metered inference gateway
92
+
93
+ ## License
94
+
95
+ SEE LICENSE IN LICENSE
96
+
97
+ ---
98
+
99
+ ## Education and Tutorials
100
+
101
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
102
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
103
+
104
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
105
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
106
+
107
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
108
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
109
+
110
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
111
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
112
+
113
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
114
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
115
+
116
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
117
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
118
+
119
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
120
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
121
+
122
+ ## Related Constructive Tooling
123
+
124
+ ### 📦 Package Management
125
+
126
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
127
+
128
+ ### 🧪 Testing
129
+
130
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
131
+ * [pglite-test](https://github.com/constructive-io/constructive/tree/main/postgres/pglite-test): **🪶 Drop-in pgsql-test replacement backed by PGlite** — in-process Postgres, no server required, instance-per-suite isolation.
132
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
133
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
134
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
135
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
136
+
137
+ ### 🧠 Parsing & AST
138
+
139
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
140
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
141
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
142
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
143
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
144
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
145
+
146
+ ### 📚 Documentation & Skills
147
+
148
+ * [constructive-skills](https://github.com/constructive-io/constructive-skills): **📖 Platform documentation and AI agent skills** — feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
149
+
150
+ Install skills for AI coding agents:
151
+
152
+ ```bash
153
+ # All platform skills (security, blueprints, codegen, billing, etc.)
154
+ npx skills add constructive-io/constructive-skills
155
+
156
+ # Individual repo skills (pgpm, testing, CLI, search, etc.)
157
+ npx skills add https://github.com/constructive-io/constructive --skill pgpm
158
+ npx skills add https://github.com/constructive-io/constructive --skill constructive-testing
159
+ ```
160
+
161
+ ## Credits
162
+
163
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
164
+
165
+ ## Disclaimer
166
+
167
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
168
+
169
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `@agentic-kit/run-log/file-store` — the node-only JSONL store.
3
+ *
4
+ * A separate entry point because the package's main entry is imported by
5
+ * browsers, Electron renderers and Next client components; a `node:fs` import
6
+ * there breaks those bundles. Same split, same reason, as `12factor-env/dotenv`.
7
+ *
8
+ * The file holds one wrapped record per line, so a run is recoverable with
9
+ * `tail -f` and a partially-written last line is detectable rather than silently
10
+ * dropped. This is the store a local run uses before (or without) a database,
11
+ * and the one tests use when they want the log to survive a process restart.
12
+ */
13
+ import type { PiSessionEntry } from './pi-entry';
14
+ import { type SessionProjectionOptions } from './projectors/session';
15
+ import { type RunEventRecord } from './record';
16
+ import { type AppendOptions, type RunLogCursor, type RunLogPage, type RunLogStore } from './store';
17
+ export interface FileRunLogStoreOptions {
18
+ /** Absolute path of the log file. Parent directories are created. */
19
+ path: string;
20
+ }
21
+ export declare class FileRunLogStore implements RunLogStore {
22
+ private readonly path;
23
+ constructor(options: FileRunLogStoreOptions);
24
+ private load;
25
+ append(runId: string, entries: readonly PiSessionEntry[], options?: AppendOptions): Promise<RunEventRecord[]>;
26
+ read(runId: string, cursor?: RunLogCursor, limit?: number): Promise<RunLogPage>;
27
+ }
28
+ /**
29
+ * Write the run's pi session file so `SessionManager.open` can resume it. This
30
+ * is the cloud→local (and local→local restart) resume path: project, write,
31
+ * hand the path to pi.
32
+ */
33
+ export declare function writeSessionFile(path: string, records: readonly RunEventRecord[], options?: SessionProjectionOptions): string;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `@agentic-kit/run-log/file-store` — the node-only JSONL store.
3
+ *
4
+ * A separate entry point because the package's main entry is imported by
5
+ * browsers, Electron renderers and Next client components; a `node:fs` import
6
+ * there breaks those bundles. Same split, same reason, as `12factor-env/dotenv`.
7
+ *
8
+ * The file holds one wrapped record per line, so a run is recoverable with
9
+ * `tail -f` and a partially-written last line is detectable rather than silently
10
+ * dropped. This is the store a local run uses before (or without) a database,
11
+ * and the one tests use when they want the log to survive a process restart.
12
+ */
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { dirname } from 'node:path';
15
+ import { projectSession } from './projectors/session';
16
+ import { assertRunEventRecord, idempotencyKey, SUPPORTED_PI_SESSION_VERSION, wrapEntry } from './record';
17
+ import { cursorAfter, START } from './store';
18
+ export class FileRunLogStore {
19
+ path;
20
+ constructor(options) {
21
+ this.path = options.path;
22
+ }
23
+ load() {
24
+ if (!existsSync(this.path))
25
+ return [];
26
+ const contents = readFileSync(this.path, 'utf8');
27
+ const records = [];
28
+ const lines = contents.split('\n');
29
+ for (let i = 0; i < lines.length; i += 1) {
30
+ const line = lines[i].trim();
31
+ if (line.length === 0)
32
+ continue;
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(line);
36
+ }
37
+ catch (error) {
38
+ throw new Error(`run log ${this.path} line ${String(i + 1)} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
39
+ }
40
+ records.push(assertRunEventRecord(parsed));
41
+ }
42
+ return records;
43
+ }
44
+ async append(runId, entries, options = {}) {
45
+ const existing = this.load().filter((record) => record.runId === runId);
46
+ const seen = new Set(existing.map((record) => idempotencyKey(runId, record.entry)));
47
+ const written = [];
48
+ for (const entry of entries) {
49
+ const key = idempotencyKey(runId, entry);
50
+ if (seen.has(key))
51
+ continue;
52
+ written.push(wrapEntry({
53
+ runId,
54
+ seq: existing.length + written.length + 1,
55
+ entry,
56
+ ...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
57
+ piSessionVersion: options.piSessionVersion ?? SUPPORTED_PI_SESSION_VERSION
58
+ }));
59
+ seen.add(key);
60
+ }
61
+ if (written.length === 0)
62
+ return written;
63
+ mkdirSync(dirname(this.path), { recursive: true });
64
+ if (!existsSync(this.path))
65
+ writeFileSync(this.path, '', { mode: 0o600 });
66
+ appendFileSync(this.path, written.map((record) => JSON.stringify(record)).join('\n') + '\n');
67
+ return written;
68
+ }
69
+ async read(runId, cursor = START, limit) {
70
+ const after = this.load().filter((record) => record.runId === runId && record.seq > cursor.afterSeq);
71
+ const records = typeof limit === 'number' ? after.slice(0, limit) : after;
72
+ return { records, cursor: cursorAfter(records, cursor) };
73
+ }
74
+ }
75
+ /**
76
+ * Write the run's pi session file so `SessionManager.open` can resume it. This
77
+ * is the cloud→local (and local→local restart) resume path: project, write,
78
+ * hand the path to pi.
79
+ */
80
+ export function writeSessionFile(path, records, options = {}) {
81
+ const projection = projectSession(records, options);
82
+ mkdirSync(dirname(path), { recursive: true });
83
+ writeFileSync(path, projection.jsonl, { mode: 0o600 });
84
+ return path;
85
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Following a run log.
3
+ *
4
+ * Every surface — a desktop chat pane, a web execution view, a CLI tail, the
5
+ * resume path — is the same reader: hold a cursor, ask for what came after it.
6
+ * That is what makes local and cloud runs indistinguishable to a UI, so this is
7
+ * the only reader loop in the system.
8
+ *
9
+ * A push wakeup (LISTEN/NOTIFY, IPC, websocket) is an optimisation, not a
10
+ * requirement: `waitForChange` short-circuits the delay when it resolves, and
11
+ * without one the loop degrades to polling at `pollIntervalMs`.
12
+ */
13
+ import type { RunEventRecord } from './record';
14
+ import { type RunLogCursor, type RunLogReadStore } from './store';
15
+ export interface FollowOptions {
16
+ cursor?: RunLogCursor;
17
+ /** Polling delay when no wakeup arrives. */
18
+ pollIntervalMs?: number;
19
+ /** Records per read. */
20
+ limit?: number;
21
+ /** Resolves when the run may have new records; races the poll delay. */
22
+ waitForChange?: (signal?: AbortSignal) => Promise<void>;
23
+ /** Stop following once this returns true for the batch just yielded. */
24
+ isTerminal?: (records: readonly RunEventRecord[]) => boolean;
25
+ signal?: AbortSignal;
26
+ /** Injectable for tests; defaults to `setTimeout`. */
27
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
28
+ }
29
+ /**
30
+ * Yield every batch of new records until the run reaches a terminal state or
31
+ * the caller aborts. Batches are yielded as read, so a caller can project
32
+ * incrementally rather than re-projecting the whole run per frame.
33
+ */
34
+ export declare function follow(store: RunLogReadStore, runId: string, options?: FollowOptions): AsyncGenerator<RunEventRecord[], void, void>;
35
+ /** Read a run to its current end in one pass. */
36
+ export declare function readAll(store: RunLogReadStore, runId: string, cursor?: RunLogCursor, pageLimit?: number): Promise<RunEventRecord[]>;
package/esm/follow.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Following a run log.
3
+ *
4
+ * Every surface — a desktop chat pane, a web execution view, a CLI tail, the
5
+ * resume path — is the same reader: hold a cursor, ask for what came after it.
6
+ * That is what makes local and cloud runs indistinguishable to a UI, so this is
7
+ * the only reader loop in the system.
8
+ *
9
+ * A push wakeup (LISTEN/NOTIFY, IPC, websocket) is an optimisation, not a
10
+ * requirement: `waitForChange` short-circuits the delay when it resolves, and
11
+ * without one the loop degrades to polling at `pollIntervalMs`.
12
+ */
13
+ import { cursorAfter, START } from './store';
14
+ const defaultSleep = (ms, signal) => new Promise((resolve) => {
15
+ const timer = setTimeout(resolve, ms);
16
+ signal?.addEventListener('abort', () => {
17
+ clearTimeout(timer);
18
+ resolve();
19
+ }, { once: true });
20
+ });
21
+ /**
22
+ * Yield every batch of new records until the run reaches a terminal state or
23
+ * the caller aborts. Batches are yielded as read, so a caller can project
24
+ * incrementally rather than re-projecting the whole run per frame.
25
+ */
26
+ export async function* follow(store, runId, options = {}) {
27
+ const pollIntervalMs = options.pollIntervalMs ?? 500;
28
+ const sleep = options.sleep ?? defaultSleep;
29
+ let cursor = options.cursor ?? START;
30
+ while (!options.signal?.aborted) {
31
+ const page = await store.read(runId, cursor, options.limit);
32
+ if (page.records.length > 0) {
33
+ cursor = cursorAfter(page.records, cursor);
34
+ yield page.records;
35
+ if (options.isTerminal?.(page.records))
36
+ return;
37
+ // Drain before waiting: a burst of tool output should not be paced by the
38
+ // poll interval.
39
+ continue;
40
+ }
41
+ if (options.waitForChange) {
42
+ await Promise.race([
43
+ options.waitForChange(options.signal),
44
+ sleep(pollIntervalMs, options.signal)
45
+ ]);
46
+ }
47
+ else {
48
+ await sleep(pollIntervalMs, options.signal);
49
+ }
50
+ }
51
+ }
52
+ /** Read a run to its current end in one pass. */
53
+ export async function readAll(store, runId, cursor = START, pageLimit = 500) {
54
+ const records = [];
55
+ let position = cursor;
56
+ for (;;) {
57
+ const page = await store.read(runId, position, pageLimit);
58
+ if (page.records.length === 0)
59
+ return records;
60
+ records.push(...page.records);
61
+ position = cursorAfter(page.records, position);
62
+ if (page.records.length < pageLimit)
63
+ return records;
64
+ }
65
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@agentic-kit/run-log` — the append-only run log: one ordered record of what
3
+ * an agent run did, wherever it ran.
4
+ *
5
+ * Browser-safe on purpose: renderers import the projectors, so nothing here may
6
+ * reach for a node builtin. The filesystem store lives behind
7
+ * `@agentic-kit/run-log/file-store`.
8
+ */
9
+ export { follow, type FollowOptions, readAll } from './follow';
10
+ export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, type PiAssistantMessage, type PiBashExecutionMessage, type PiBranchSummaryEntry, type PiCompactionEntry, type PiContent, type PiCustomMessage, type PiEntryBase, type PiImageContent, type PiMessage, type PiMessageEntry, type PiOtherEntry, type PiSessionEntry, type PiSessionHeader, type PiSummaryMessage, type PiTextContent, type PiThinkingContent, type PiToolCallContent, type PiToolResultMessage, type PiUsage, type PiUsageCost, type PiUserMessage, toolCalls } from './pi-entry';
11
+ export { type BashPart, type Conversation, type ConversationPart, type CustomPart, projectParts, type SummaryPart, type TextPart, type ThinkingPart, type ToolPart, type ToolStatus, type UnknownPart } from './projectors/parts';
12
+ export { parseSessionJsonl, projectSession, type SessionProjection, type SessionProjectionOptions } from './projectors/session';
13
+ export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, type ApprovalRequestInput, approvalRequestMessage, type ApprovalResolutionInput, approvalResolutionMessage, type ApprovalState, projectToolState, type ToolCallState, type ToolCallStatus, type ToolStateProjection } from './projectors/tool-state';
14
+ export { modelKey, type ModelUsage, projectUsage, type RunUsage, type UsageTotals } from './projectors/usage';
15
+ export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, SUPPORTED_PI_SESSION_VERSION, wrapEntry, type WrapEntryOptions } from './record';
16
+ export { type AppendOptions, cursorAfter, MemoryRunLogStore, type RunLogAppendStore, type RunLogCursor, type RunLogPage, type RunLogReadStore, type RunLogStore, START } from './store';
package/esm/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@agentic-kit/run-log` — the append-only run log: one ordered record of what
3
+ * an agent run did, wherever it ran.
4
+ *
5
+ * Browser-safe on purpose: renderers import the projectors, so nothing here may
6
+ * reach for a node builtin. The filesystem store lives behind
7
+ * `@agentic-kit/run-log/file-store`.
8
+ */
9
+ export { follow, readAll } from './follow';
10
+ export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './pi-entry';
11
+ export { projectParts } from './projectors/parts';
12
+ export { parseSessionJsonl, projectSession } from './projectors/session';
13
+ export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, projectToolState } from './projectors/tool-state';
14
+ export { modelKey, projectUsage } from './projectors/usage';
15
+ export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, SUPPORTED_PI_SESSION_VERSION, wrapEntry } from './record';
16
+ export { cursorAfter, MemoryRunLogStore, START } from './store';
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The structural subset of pi's session entries this package reads.
3
+ *
4
+ * Deliberately structural, not imported from pi: a run log stores pi entries
5
+ * *verbatim*, so the types here describe what the projectors read rather than
6
+ * re-declaring pi's format. Every interface keeps an index signature so an
7
+ * entry produced by a newer pi still parses, and unknown `type` values are
8
+ * carried through untouched instead of being dropped.
9
+ *
10
+ * Reference: `@earendil-works/pi-coding-agent` `docs/session-format.md`
11
+ * (session file version 3).
12
+ */
13
+ export interface PiUsageCost {
14
+ input?: number;
15
+ output?: number;
16
+ cacheRead?: number;
17
+ cacheWrite?: number;
18
+ total?: number;
19
+ }
20
+ export interface PiUsage {
21
+ input?: number;
22
+ output?: number;
23
+ cacheRead?: number;
24
+ cacheWrite?: number;
25
+ totalTokens?: number;
26
+ cost?: PiUsageCost;
27
+ [key: string]: unknown;
28
+ }
29
+ export interface PiTextContent {
30
+ type: 'text';
31
+ text: string;
32
+ }
33
+ export interface PiThinkingContent {
34
+ type: 'thinking';
35
+ thinking: string;
36
+ }
37
+ export interface PiImageContent {
38
+ type: 'image';
39
+ data: string;
40
+ mimeType: string;
41
+ }
42
+ export interface PiToolCallContent {
43
+ type: 'toolCall';
44
+ id: string;
45
+ name: string;
46
+ arguments?: Record<string, unknown>;
47
+ }
48
+ export type PiContent = PiTextContent | PiThinkingContent | PiImageContent | PiToolCallContent;
49
+ export interface PiUserMessage {
50
+ role: 'user';
51
+ content: string | PiContent[];
52
+ timestamp?: number;
53
+ [key: string]: unknown;
54
+ }
55
+ export interface PiAssistantMessage {
56
+ role: 'assistant';
57
+ content: PiContent[];
58
+ api?: string;
59
+ provider?: string;
60
+ model?: string;
61
+ usage?: PiUsage;
62
+ stopReason?: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted';
63
+ errorMessage?: string;
64
+ timestamp?: number;
65
+ [key: string]: unknown;
66
+ }
67
+ export interface PiToolResultMessage {
68
+ role: 'toolResult';
69
+ toolCallId: string;
70
+ toolName: string;
71
+ content: PiContent[];
72
+ details?: unknown;
73
+ usage?: PiUsage;
74
+ isError?: boolean;
75
+ timestamp?: number;
76
+ [key: string]: unknown;
77
+ }
78
+ export interface PiBashExecutionMessage {
79
+ role: 'bashExecution';
80
+ command: string;
81
+ output: string;
82
+ exitCode?: number;
83
+ cancelled?: boolean;
84
+ truncated?: boolean;
85
+ timestamp?: number;
86
+ [key: string]: unknown;
87
+ }
88
+ export interface PiCustomMessage {
89
+ role: 'custom';
90
+ customType: string;
91
+ content: string | PiContent[];
92
+ display?: boolean;
93
+ details?: unknown;
94
+ timestamp?: number;
95
+ [key: string]: unknown;
96
+ }
97
+ export interface PiSummaryMessage {
98
+ role: 'branchSummary' | 'compactionSummary';
99
+ summary: string;
100
+ timestamp?: number;
101
+ [key: string]: unknown;
102
+ }
103
+ export type PiMessage = PiUserMessage | PiAssistantMessage | PiToolResultMessage | PiBashExecutionMessage | PiCustomMessage | PiSummaryMessage;
104
+ export interface PiSessionHeader {
105
+ type: 'session';
106
+ version: number;
107
+ id: string;
108
+ timestamp: string;
109
+ cwd?: string;
110
+ parentSession?: string;
111
+ [key: string]: unknown;
112
+ }
113
+ export interface PiEntryBase {
114
+ id: string;
115
+ parentId: string | null;
116
+ timestamp: string;
117
+ }
118
+ export interface PiMessageEntry extends PiEntryBase {
119
+ type: 'message';
120
+ message: PiMessage;
121
+ [key: string]: unknown;
122
+ }
123
+ export interface PiCompactionEntry extends PiEntryBase {
124
+ type: 'compaction';
125
+ summary: string;
126
+ tokensBefore?: number;
127
+ usage?: PiUsage;
128
+ [key: string]: unknown;
129
+ }
130
+ export interface PiBranchSummaryEntry extends PiEntryBase {
131
+ type: 'branch_summary';
132
+ summary: string;
133
+ fromId: string;
134
+ usage?: PiUsage;
135
+ [key: string]: unknown;
136
+ }
137
+ export interface PiOtherEntry extends PiEntryBase {
138
+ type: string;
139
+ [key: string]: unknown;
140
+ }
141
+ export type PiSessionEntry = PiSessionHeader | PiMessageEntry | PiCompactionEntry | PiBranchSummaryEntry | PiOtherEntry;
142
+ export declare const isPiSessionHeader: (entry: PiSessionEntry) => entry is PiSessionHeader;
143
+ export declare const isPiMessageEntry: (entry: PiSessionEntry) => entry is PiMessageEntry;
144
+ export declare const isPiCompactionEntry: (entry: PiSessionEntry) => entry is PiCompactionEntry;
145
+ export declare const isPiBranchSummaryEntry: (entry: PiSessionEntry) => entry is PiBranchSummaryEntry;
146
+ export declare const isAssistantMessage: (message: PiMessage) => message is PiAssistantMessage;
147
+ export declare const isToolResultMessage: (message: PiMessage) => message is PiToolResultMessage;
148
+ /**
149
+ * Narrow an untrusted value (a database JSONB column, an HTTP body) to a pi
150
+ * entry. Throws rather than returning null: a log row that cannot be read is a
151
+ * corrupted log, never an empty one.
152
+ */
153
+ export declare function assertPiSessionEntry(value: unknown): PiSessionEntry;
154
+ /** The text of a message's content, whether it is a string or a block array. */
155
+ export declare function contentText(content: string | PiContent[] | undefined): string;
156
+ /** Tool calls requested by an assistant message, in order. */
157
+ export declare function toolCalls(message: PiAssistantMessage): PiToolCallContent[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The structural subset of pi's session entries this package reads.
3
+ *
4
+ * Deliberately structural, not imported from pi: a run log stores pi entries
5
+ * *verbatim*, so the types here describe what the projectors read rather than
6
+ * re-declaring pi's format. Every interface keeps an index signature so an
7
+ * entry produced by a newer pi still parses, and unknown `type` values are
8
+ * carried through untouched instead of being dropped.
9
+ *
10
+ * Reference: `@earendil-works/pi-coding-agent` `docs/session-format.md`
11
+ * (session file version 3).
12
+ */
13
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ export const isPiSessionHeader = (entry) => entry.type === 'session';
15
+ export const isPiMessageEntry = (entry) => entry.type === 'message' && isRecord(entry.message);
16
+ export const isPiCompactionEntry = (entry) => entry.type === 'compaction';
17
+ export const isPiBranchSummaryEntry = (entry) => entry.type === 'branch_summary';
18
+ export const isAssistantMessage = (message) => message.role === 'assistant';
19
+ export const isToolResultMessage = (message) => message.role === 'toolResult';
20
+ /**
21
+ * Narrow an untrusted value (a database JSONB column, an HTTP body) to a pi
22
+ * entry. Throws rather than returning null: a log row that cannot be read is a
23
+ * corrupted log, never an empty one.
24
+ */
25
+ export function assertPiSessionEntry(value) {
26
+ if (!isRecord(value)) {
27
+ throw new TypeError(`pi session entry must be an object, received ${typeof value}`);
28
+ }
29
+ if (typeof value.type !== 'string' || value.type.length === 0) {
30
+ throw new TypeError('pi session entry must carry a non-empty string `type`');
31
+ }
32
+ if (value.type !== 'session') {
33
+ if (typeof value.id !== 'string' || value.id.length === 0) {
34
+ throw new TypeError(`pi ${value.type} entry must carry a non-empty string \`id\``);
35
+ }
36
+ if (typeof value.timestamp !== 'string') {
37
+ throw new TypeError(`pi ${value.type} entry must carry an ISO string \`timestamp\``);
38
+ }
39
+ if (!('parentId' in value)) {
40
+ throw new TypeError(`pi ${value.type} entry must carry \`parentId\` (null for the first entry)`);
41
+ }
42
+ }
43
+ return value;
44
+ }
45
+ /** The text of a message's content, whether it is a string or a block array. */
46
+ export function contentText(content) {
47
+ if (typeof content === 'string')
48
+ return content;
49
+ if (!Array.isArray(content))
50
+ return '';
51
+ return content
52
+ .filter((block) => isRecord(block) && block.type === 'text')
53
+ .map((block) => block.text)
54
+ .join('');
55
+ }
56
+ /** Tool calls requested by an assistant message, in order. */
57
+ export function toolCalls(message) {
58
+ if (!Array.isArray(message.content))
59
+ return [];
60
+ return message.content.filter((block) => isRecord(block) && block.type === 'toolCall');
61
+ }