@agentic-kit/run-log-client 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,82 @@
1
+ # @agentic-kit/run-log-client
2
+
3
+ The run log as a browser reads it.
4
+
5
+ This package provides a tenant-GraphQL `RunLogStore` over an agent run and its events.
6
+ It also includes the follower loop shared by each run surface.
7
+ Use it to read append-only run entries and project them into browser-facing views.
8
+ The client works with the published `@agentic-kit/run-log` data model.
9
+
10
+ ---
11
+
12
+ ## Education and Tutorials
13
+
14
+ 1. πŸš€ [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
15
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
16
+
17
+ 2. πŸ“¦ [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
18
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
19
+
20
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
21
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
22
+
23
+ 4. πŸ§ͺ [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
24
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
25
+
26
+ 5. ⚑ [Supabase Testing](https://constructive.io/learn/supabase)
27
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
28
+
29
+ 6. πŸ’§ [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
30
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
31
+
32
+ 7. πŸ”§ [Troubleshooting](https://constructive.io/learn/troubleshooting)
33
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
34
+
35
+ ## Related Constructive Tooling
36
+
37
+ ### πŸ“¦ Package Management
38
+
39
+ * [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.
40
+
41
+ ### πŸ§ͺ Testing
42
+
43
+ * [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.
44
+ * [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.
45
+ * [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.
46
+ * [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.
47
+ * [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.
48
+ * [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.
49
+
50
+ ### 🧠 Parsing & AST
51
+
52
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **πŸ”„ SQL conversion engine** that interprets and converts PostgreSQL syntax.
53
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **πŸŒ‰ Node.js bindings** for `libpg_query`, converting SQL into parse trees.
54
+ * [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.
55
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
56
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **πŸ“ Type definitions** for PostgreSQL AST nodes in TypeScript.
57
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **πŸ› οΈ AST utilities** for constructing and transforming PostgreSQL syntax trees.
58
+
59
+ ### πŸ“š Documentation & Skills
60
+
61
+ * [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.
62
+
63
+ Install skills for AI coding agents:
64
+
65
+ ```bash
66
+ # All platform skills (security, blueprints, codegen, billing, etc.)
67
+ npx skills add constructive-io/constructive-skills
68
+
69
+ # Individual repo skills (pgpm, testing, CLI, search, etc.)
70
+ npx skills add https://github.com/constructive-io/constructive --skill pgpm
71
+ npx skills add https://github.com/constructive-io/constructive --skill constructive-testing
72
+ ```
73
+
74
+ ## Credits
75
+
76
+ **πŸ›  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).**
77
+
78
+ ## Disclaimer
79
+
80
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
81
+
82
+ 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.
package/client.d.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The run log, as a browser reads and writes it.
3
+ *
4
+ * A Job writes through the worker's callback lane because it holds no tenant
5
+ * credential; a user's browser is the opposite case β€” it already authenticates
6
+ * as the user against the tenant API, and RLS on `agent_run`/`agent_event` is
7
+ * exactly the visibility rule a run surface should obey. So this store speaks
8
+ * the tenant's own GraphQL and implements the same `RunLogStore` contract as
9
+ * every other host, which is what lets one renderer draw a local run, a cloud
10
+ * run and a replayed run without knowing which it has.
11
+ */
12
+ import { type AppendOptions, type ApprovalResolutionInput, type PiMessage, type RunEventRecord, type RunLogCursor, type RunLogPage, type RunLogStore, type TranscriptEntry, type TranscriptReaderRegistry } from '@agentic-kit/run-log';
13
+ import { type RunLogNames } from './names';
14
+ import type { GraphqlRequest } from './transport';
15
+ export type RunStatus = 'pending' | 'running' | 'waiting' | 'idle' | 'succeeded' | 'failed' | 'cancelled';
16
+ /** A run holding, or able to hold, compute. */
17
+ export declare const ATTACHABLE_STATUSES: readonly RunStatus[];
18
+ /** A run that will never append again. */
19
+ export declare const TERMINAL_STATUSES: readonly RunStatus[];
20
+ export declare const isTerminalStatus: (status: string) => boolean;
21
+ /**
22
+ * The run row as a surface reads it. `executionId` is the *currently attached*
23
+ * execution and is null for an idle run β€” the attach/detach history lives in the
24
+ * log, so a null here means "holds no compute right now", never "never ran".
25
+ */
26
+ export interface RunSummary {
27
+ id: string;
28
+ threadId: string | null;
29
+ /** Who invoked the run. */
30
+ actorId: string | null;
31
+ /** Who is billed for it β€” the actor themselves for a personal run. */
32
+ entityId: string | null;
33
+ status: string;
34
+ placement: string | null;
35
+ executionId: string | null;
36
+ repoUrl: string | null;
37
+ branch: string | null;
38
+ baseCommit: string | null;
39
+ headCommit: string | null;
40
+ lastEventSeq: number;
41
+ attempt: number | null;
42
+ parentRunId: string | null;
43
+ error: string | null;
44
+ artifacts: unknown;
45
+ deadlineAt: string | null;
46
+ startedAt: string | null;
47
+ finishedAt: string | null;
48
+ createdAt: string | null;
49
+ }
50
+ /**
51
+ * What a host chooses when it opens a run.
52
+ *
53
+ * Deliberately holds no identity: `actor_id`, `principal_id` and `entity_id`
54
+ * default from the claims of the transaction the tenant API opens for this
55
+ * request, so who the run belongs to is the authenticated session and not
56
+ * something a caller can name.
57
+ */
58
+ export interface CreateRunInput {
59
+ threadId: string;
60
+ /**
61
+ * `local` for a run executed by a host on the user's machine, `cloud` for a
62
+ * Job. The column defaults to `cloud`, so a local host states it.
63
+ */
64
+ placement: 'cloud' | 'local';
65
+ status?: RunStatus;
66
+ repoUrl?: string;
67
+ branch?: string;
68
+ }
69
+ /** The lifecycle columns a host that owns a run moves as it runs. */
70
+ export interface RunPatch {
71
+ status?: RunStatus;
72
+ error?: string | null;
73
+ startedAt?: string | null;
74
+ finishedAt?: string | null;
75
+ }
76
+ export interface CreateThreadInput {
77
+ title?: string;
78
+ /** `agent` for a tool-enabled run, which is what a coding host opens. */
79
+ mode?: 'ask' | 'agent';
80
+ }
81
+ export interface ThreadSummary {
82
+ id: string;
83
+ title: string | null;
84
+ createdAt: string | null;
85
+ }
86
+ export interface ListRunsArgs {
87
+ first?: number;
88
+ threadId?: string;
89
+ status?: string;
90
+ }
91
+ export interface RunLogClientOptions {
92
+ request: GraphqlRequest;
93
+ /** From the module loader; defaults to the app-scope names. */
94
+ runTable?: string;
95
+ eventTable?: string;
96
+ threadTable?: string;
97
+ /**
98
+ * A browser has no row lock, so two writers can pick the same next `seq`;
99
+ * `UNIQUE (run_id, seq)` rejects the loser and it re-reads and retries. Bounded
100
+ * so a genuinely broken write surfaces as an error instead of spinning.
101
+ */
102
+ maxAppendAttempts?: number;
103
+ newId?: () => string;
104
+ now?: () => Date;
105
+ /**
106
+ * Readers that validate the entries this client writes and reads. Defaults to
107
+ * the process registry, so a host that registered a second harness's reader
108
+ * at startup reads that harness's runs without passing anything here.
109
+ */
110
+ readers?: TranscriptReaderRegistry;
111
+ }
112
+ /**
113
+ * A conflict on `(run_id, seq)` β€” the only error this store retries. Matched on
114
+ * the message because the tenant API reports it as a GraphQL error string; any
115
+ * other failure is rethrown untouched.
116
+ */
117
+ export declare const isSeqConflict: (error: unknown) => boolean;
118
+ export declare class GraphqlRunLogClient implements RunLogStore {
119
+ readonly names: RunLogNames;
120
+ /**
121
+ * Read by a follower so the view projects exactly the formats this client
122
+ * accepts β€” one registry decides both what a run may store and what a surface
123
+ * can draw.
124
+ */
125
+ readonly readers: TranscriptReaderRegistry;
126
+ private readonly request;
127
+ private readonly maxAppendAttempts;
128
+ private readonly newId;
129
+ private readonly now;
130
+ constructor(options: RunLogClientOptions);
131
+ listRuns(args?: ListRunsArgs): Promise<RunSummary[]>;
132
+ /**
133
+ * Open a thread. A cloud run is opened against a thread the web UI already
134
+ * created; a host on the user's machine is the first writer of its own
135
+ * conversation, so it creates one and then hangs its runs off it.
136
+ */
137
+ createThread(input?: CreateThreadInput): Promise<ThreadSummary>;
138
+ /** Open a run. See {@link CreateRunInput} on why identity is not an argument. */
139
+ createRun(input: CreateRunInput): Promise<RunSummary>;
140
+ /**
141
+ * Move a run's lifecycle columns. The transcript is the history; this row is
142
+ * the current state a list view reads without replaying it.
143
+ */
144
+ updateRun(runId: string, patch: RunPatch): Promise<RunSummary>;
145
+ getRun(runId: string): Promise<RunSummary>;
146
+ read(runId: string, cursor?: RunLogCursor, limit?: number): Promise<RunLogPage>;
147
+ /**
148
+ * Append entries, allocating `seq` from the run's cursor and advancing it.
149
+ *
150
+ * Two writers racing is normal here (a user sends a message while the Job
151
+ * appends a result), so the sequence is not reserved up front: each insert is
152
+ * attempted at the next free position and a rejected one re-reads the run.
153
+ * Entries already present by entry id are skipped, so a retried call is a
154
+ * no-op rather than a duplicated turn.
155
+ */
156
+ append(runId: string, entries: readonly TranscriptEntry[], options?: AppendOptions): Promise<RunEventRecord[]>;
157
+ /**
158
+ * Append one pi message as an entry, chained to the run's current tail.
159
+ *
160
+ * The composer's own turn is the one place this store still writes a
161
+ * transcript rather than relaying one, so it writes the format it declares in
162
+ * `insertEvent`; a second harness contributes its own message builder beside
163
+ * this one and appends through the neutral `append`.
164
+ */
165
+ appendMessage(runId: string, message: PiMessage, options?: AppendOptions): Promise<RunEventRecord[]>;
166
+ /** The user's turn: what a run surface's composer sends. */
167
+ prompt(runId: string, text: string): Promise<RunEventRecord[]>;
168
+ /**
169
+ * Answer a pending approval. The decision is a log entry like any other, so
170
+ * the Job sees it on its next cursor read and the history records who allowed
171
+ * what β€” there is no side channel to reconcile.
172
+ */
173
+ resolveApproval(runId: string, input: ApprovalResolutionInput): Promise<RunEventRecord[]>;
174
+ private insertEvent;
175
+ private advanceRun;
176
+ /**
177
+ * Skip entries a previous attempt already wrote. Only the tail is inspected β€”
178
+ * as many records as the call is trying to append β€” because that is where a
179
+ * partially applied attempt of *this* call can be, and scanning the whole run
180
+ * to make a retry idempotent would make every append cost the run's length.
181
+ */
182
+ private dropAlreadyWritten;
183
+ private tailEntryId;
184
+ }
185
+ export declare const createRunLogClient: (options: RunLogClientOptions) => GraphqlRunLogClient;
package/client.js ADDED
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ /**
3
+ * The run log, as a browser reads and writes it.
4
+ *
5
+ * A Job writes through the worker's callback lane because it holds no tenant
6
+ * credential; a user's browser is the opposite case β€” it already authenticates
7
+ * as the user against the tenant API, and RLS on `agent_run`/`agent_event` is
8
+ * exactly the visibility rule a run surface should obey. So this store speaks
9
+ * the tenant's own GraphQL and implements the same `RunLogStore` contract as
10
+ * every other host, which is what lets one renderer draw a local run, a cloud
11
+ * run and a replayed run without knowing which it has.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.createRunLogClient = exports.GraphqlRunLogClient = exports.isSeqConflict = exports.isTerminalStatus = exports.TERMINAL_STATUSES = exports.ATTACHABLE_STATUSES = void 0;
15
+ const run_log_1 = require("@agentic-kit/run-log");
16
+ const documents_1 = require("./documents");
17
+ const names_1 = require("./names");
18
+ /** A run holding, or able to hold, compute. */
19
+ exports.ATTACHABLE_STATUSES = [
20
+ 'pending',
21
+ 'running',
22
+ 'waiting',
23
+ 'idle',
24
+ ];
25
+ /** A run that will never append again. */
26
+ exports.TERMINAL_STATUSES = [
27
+ 'succeeded',
28
+ 'failed',
29
+ 'cancelled',
30
+ ];
31
+ const isTerminalStatus = (status) => exports.TERMINAL_STATUSES.includes(status);
32
+ exports.isTerminalStatus = isTerminalStatus;
33
+ const DEFAULT_MAX_APPEND_ATTEMPTS = 5;
34
+ const DEFAULT_READ_LIMIT = 200;
35
+ const DEFAULT_RUN_LIMIT = 50;
36
+ /**
37
+ * A conflict on `(run_id, seq)` β€” the only error this store retries. Matched on
38
+ * the message because the tenant API reports it as a GraphQL error string; any
39
+ * other failure is rethrown untouched.
40
+ */
41
+ const isSeqConflict = (error) => {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ return /duplicate key|unique constraint|already exists/i.test(message);
44
+ };
45
+ exports.isSeqConflict = isSeqConflict;
46
+ const entryId = (entry) => typeof entry.id === 'string' ? entry.id : undefined;
47
+ class GraphqlRunLogClient {
48
+ names;
49
+ /**
50
+ * Read by a follower so the view projects exactly the formats this client
51
+ * accepts β€” one registry decides both what a run may store and what a surface
52
+ * can draw.
53
+ */
54
+ readers;
55
+ request;
56
+ maxAppendAttempts;
57
+ newId;
58
+ now;
59
+ constructor(options) {
60
+ this.request = options.request;
61
+ this.names = (0, names_1.runLogNames)(options.runTable, options.eventTable, options.threadTable);
62
+ this.maxAppendAttempts =
63
+ options.maxAppendAttempts ?? DEFAULT_MAX_APPEND_ATTEMPTS;
64
+ this.newId = options.newId ?? (() => globalThis.crypto.randomUUID());
65
+ this.now = options.now ?? (() => new Date());
66
+ this.readers = options.readers ?? run_log_1.transcriptReaders;
67
+ }
68
+ async listRuns(args = {}) {
69
+ const where = {};
70
+ if (args.threadId)
71
+ where.threadId = { equalTo: args.threadId };
72
+ if (args.status)
73
+ where.status = { equalTo: args.status };
74
+ const data = await this.request((0, documents_1.runsDocument)(this.names), {
75
+ first: args.first ?? DEFAULT_RUN_LIMIT,
76
+ where: Object.keys(where).length > 0 ? where : undefined,
77
+ });
78
+ return data[this.names.runs].nodes;
79
+ }
80
+ /**
81
+ * Open a thread. A cloud run is opened against a thread the web UI already
82
+ * created; a host on the user's machine is the first writer of its own
83
+ * conversation, so it creates one and then hangs its runs off it.
84
+ */
85
+ async createThread(input = {}) {
86
+ const data = await this.request((0, documents_1.createThreadDocument)(this.names), {
87
+ input: {
88
+ [this.names.thread]: {
89
+ ...(input.title === undefined ? {} : { title: input.title }),
90
+ mode: input.mode ?? 'agent',
91
+ },
92
+ },
93
+ });
94
+ const thread = data[this.names.createThread]?.[this.names.thread];
95
+ if (!thread) {
96
+ throw new Error(`${this.names.createThread} returned no thread; the run surface is not writable by this user`);
97
+ }
98
+ return thread;
99
+ }
100
+ /** Open a run. See {@link CreateRunInput} on why identity is not an argument. */
101
+ async createRun(input) {
102
+ const data = await this.request((0, documents_1.createRunDocument)(this.names), {
103
+ input: {
104
+ [this.names.run]: {
105
+ threadId: input.threadId,
106
+ placement: input.placement,
107
+ ...(input.status === undefined ? {} : { status: input.status }),
108
+ ...(input.repoUrl === undefined ? {} : { repoUrl: input.repoUrl }),
109
+ ...(input.branch === undefined ? {} : { branch: input.branch }),
110
+ },
111
+ },
112
+ });
113
+ const run = data[this.names.createRun]?.[this.names.run];
114
+ if (!run) {
115
+ throw new Error(`${this.names.createRun} returned no run; the run surface is not writable by this user`);
116
+ }
117
+ return run;
118
+ }
119
+ /**
120
+ * Move a run's lifecycle columns. The transcript is the history; this row is
121
+ * the current state a list view reads without replaying it.
122
+ */
123
+ async updateRun(runId, patch) {
124
+ if (Object.keys(patch).length === 0) {
125
+ throw new Error(`run ${runId}: updateRun called with an empty patch`);
126
+ }
127
+ const data = await this.request((0, documents_1.advanceRunDocument)(this.names), {
128
+ input: { id: runId, [this.names.runPatch]: patch },
129
+ });
130
+ const run = data[this.names.updateRun]?.[this.names.run];
131
+ if (!run) {
132
+ throw new Error(`run ${runId} was not updated; it is not visible or not writable by this user`);
133
+ }
134
+ return run;
135
+ }
136
+ async getRun(runId) {
137
+ const data = await this.request((0, documents_1.runDocument)(this.names), { where: { id: { equalTo: runId } } });
138
+ const run = data[this.names.runs].nodes[0];
139
+ if (!run) {
140
+ throw new Error(`run ${runId} not found, or not visible to this user`);
141
+ }
142
+ return run;
143
+ }
144
+ async read(runId, cursor = run_log_1.START, limit) {
145
+ const data = await this.request((0, documents_1.eventsDocument)(this.names), {
146
+ first: limit ?? DEFAULT_READ_LIMIT,
147
+ where: {
148
+ runId: { equalTo: runId },
149
+ seq: { greaterThan: cursor.afterSeq },
150
+ },
151
+ });
152
+ const records = data[this.names.events].nodes.map((node) => (0, run_log_1.assertRunEventRecord)(node, this.readers));
153
+ return {
154
+ records,
155
+ cursor: {
156
+ afterSeq: records.length > 0
157
+ ? records[records.length - 1].seq
158
+ : cursor.afterSeq,
159
+ },
160
+ };
161
+ }
162
+ /**
163
+ * Append entries, allocating `seq` from the run's cursor and advancing it.
164
+ *
165
+ * Two writers racing is normal here (a user sends a message while the Job
166
+ * appends a result), so the sequence is not reserved up front: each insert is
167
+ * attempted at the next free position and a rejected one re-reads the run.
168
+ * Entries already present by entry id are skipped, so a retried call is a
169
+ * no-op rather than a duplicated turn.
170
+ */
171
+ async append(runId, entries, options) {
172
+ if (entries.length === 0)
173
+ return [];
174
+ const written = [];
175
+ let pending = [...entries];
176
+ for (let attempt = 0; attempt < this.maxAppendAttempts; attempt += 1) {
177
+ const run = await this.getRun(runId);
178
+ pending = await this.dropAlreadyWritten(runId, run, pending);
179
+ if (pending.length === 0)
180
+ break;
181
+ let seq = run.lastEventSeq;
182
+ try {
183
+ while (pending.length > 0) {
184
+ seq += 1;
185
+ written.push(await this.insertEvent(runId, seq, pending[0], options));
186
+ pending = pending.slice(1);
187
+ }
188
+ }
189
+ catch (error) {
190
+ if (!(0, exports.isSeqConflict)(error))
191
+ throw error;
192
+ continue;
193
+ }
194
+ await this.advanceRun(runId, seq);
195
+ return written;
196
+ }
197
+ if (pending.length > 0) {
198
+ throw new Error(`run ${runId}: could not append ${pending.length} entr${pending.length === 1 ? 'y' : 'ies'} after ${this.maxAppendAttempts} attempts β€” the run's sequence is contended`);
199
+ }
200
+ return written;
201
+ }
202
+ /**
203
+ * Append one pi message as an entry, chained to the run's current tail.
204
+ *
205
+ * The composer's own turn is the one place this store still writes a
206
+ * transcript rather than relaying one, so it writes the format it declares in
207
+ * `insertEvent`; a second harness contributes its own message builder beside
208
+ * this one and appends through the neutral `append`.
209
+ */
210
+ async appendMessage(runId, message, options) {
211
+ const parentId = await this.tailEntryId(runId);
212
+ const entry = {
213
+ id: this.newId(),
214
+ parentId,
215
+ timestamp: this.now().toISOString(),
216
+ type: 'message',
217
+ message,
218
+ };
219
+ return this.append(runId, [entry], options);
220
+ }
221
+ /** The user's turn: what a run surface's composer sends. */
222
+ async prompt(runId, text) {
223
+ if (!text.trim())
224
+ throw new Error('cannot append an empty prompt');
225
+ return this.appendMessage(runId, {
226
+ role: 'user',
227
+ content: text,
228
+ timestamp: this.now().getTime(),
229
+ });
230
+ }
231
+ /**
232
+ * Answer a pending approval. The decision is a log entry like any other, so
233
+ * the Job sees it on its next cursor read and the history records who allowed
234
+ * what β€” there is no side channel to reconcile.
235
+ */
236
+ async resolveApproval(runId, input) {
237
+ return this.appendMessage(runId, (0, run_log_1.approvalResolutionMessage)(input));
238
+ }
239
+ async insertEvent(runId, seq, entry, options) {
240
+ const data = await this.request((0, documents_1.appendDocument)(this.names), {
241
+ input: {
242
+ [this.names.event]: {
243
+ runId,
244
+ seq,
245
+ recordedAt: options?.recordedAt ?? this.now().toISOString(),
246
+ // A caller that read its entries out of a harness names that
247
+ // harness's format; only the composer path below leaves it unset,
248
+ // which is why the fallback is pi's.
249
+ transcriptFormat: options?.transcriptFormat ?? run_log_1.PI_TRANSCRIPT_FORMAT,
250
+ transcriptVersion: options?.transcriptVersion ?? run_log_1.SUPPORTED_PI_TRANSCRIPT_VERSION,
251
+ entry,
252
+ },
253
+ },
254
+ });
255
+ return (0, run_log_1.assertRunEventRecord)(data[this.names.createEvent][this.names.event], this.readers);
256
+ }
257
+ async advanceRun(runId, seq) {
258
+ await this.request((0, documents_1.advanceRunDocument)(this.names), {
259
+ input: { id: runId, [this.names.runPatch]: { lastEventSeq: seq } },
260
+ });
261
+ }
262
+ /**
263
+ * Skip entries a previous attempt already wrote. Only the tail is inspected β€”
264
+ * as many records as the call is trying to append β€” because that is where a
265
+ * partially applied attempt of *this* call can be, and scanning the whole run
266
+ * to make a retry idempotent would make every append cost the run's length.
267
+ */
268
+ async dropAlreadyWritten(runId, run, pending) {
269
+ const ids = pending.map(entryId).filter((id) => !!id);
270
+ if (ids.length === 0 || run.lastEventSeq === 0)
271
+ return [...pending];
272
+ const window = Math.max(0, run.lastEventSeq - pending.length);
273
+ const page = await this.read(runId, { afterSeq: window }, pending.length);
274
+ const present = new Set(page.records.map((record) => entryId(record.entry)).filter(Boolean));
275
+ return pending.filter((entry) => {
276
+ const id = entryId(entry);
277
+ return !id || !present.has(id);
278
+ });
279
+ }
280
+ async tailEntryId(runId) {
281
+ const run = await this.getRun(runId);
282
+ if (run.lastEventSeq === 0)
283
+ return null;
284
+ const page = await this.read(runId, { afterSeq: run.lastEventSeq - 1 }, 1);
285
+ const last = page.records[page.records.length - 1];
286
+ return last ? (entryId(last.entry) ?? null) : null;
287
+ }
288
+ }
289
+ exports.GraphqlRunLogClient = GraphqlRunLogClient;
290
+ const createRunLogClient = (options) => new GraphqlRunLogClient(options);
291
+ exports.createRunLogClient = createRunLogClient;
package/documents.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The documents this lane needs, built from a module's names.
3
+ *
4
+ * Written as strings rather than generated: the run surface belongs to a
5
+ * tenant's schema, so there is no committed SDK target that carries it, and the
6
+ * fields queried here are exactly the columns the run generator emits.
7
+ *
8
+ * The shapes follow the conventions the generated targets in `sdk/` use against
9
+ * this platform's GraphQL: a collection takes `where: <Type>Filter` (not
10
+ * `filter`), there is no single-node query field β€” one row is a `first: 1`
11
+ * collection read β€” and an update takes `{ id, <singular>Patch }`.
12
+ */
13
+ import type { RunLogNames } from './names';
14
+ export declare const RUN_FIELDS = "\n id\n threadId\n actorId\n entityId\n status\n placement\n executionId\n repoUrl\n branch\n baseCommit\n headCommit\n lastEventSeq\n attempt\n parentRunId\n error\n artifacts\n deadlineAt\n startedAt\n finishedAt\n createdAt";
15
+ export declare const EVENT_FIELDS = "\n runId\n seq\n recordedAt\n transcriptFormat\n transcriptVersion\n entry";
16
+ export declare const runsDocument: (names: RunLogNames) => string;
17
+ export declare const runDocument: (names: RunLogNames) => string;
18
+ export declare const eventsDocument: (names: RunLogNames) => string;
19
+ export declare const appendDocument: (names: RunLogNames) => string;
20
+ /**
21
+ * Open a run.
22
+ *
23
+ * A cloud run is opened by the worker on the platform's side of the callback
24
+ * lane; a local run has no worker, so the host that executes it opens its own
25
+ * row here β€” as the user, over the tenant API, which is what makes `actor_id`,
26
+ * `principal_id` and `entity_id` the session's claims rather than anything this
27
+ * client could pass. Only the columns a host legitimately chooses are sent.
28
+ */
29
+ export declare const createRunDocument: (names: RunLogNames) => string;
30
+ /**
31
+ * Open the thread a run hangs from. `agent_run.thread_id` is required and a
32
+ * local host is the first writer of its own conversation, so it creates one.
33
+ */
34
+ export declare const createThreadDocument: (names: RunLogNames) => string;
35
+ export declare const advanceRunDocument: (names: RunLogNames) => string;