@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/documents.js ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * The documents this lane needs, built from a module's names.
4
+ *
5
+ * Written as strings rather than generated: the run surface belongs to a
6
+ * tenant's schema, so there is no committed SDK target that carries it, and the
7
+ * fields queried here are exactly the columns the run generator emits.
8
+ *
9
+ * The shapes follow the conventions the generated targets in `sdk/` use against
10
+ * this platform's GraphQL: a collection takes `where: <Type>Filter` (not
11
+ * `filter`), there is no single-node query field — one row is a `first: 1`
12
+ * collection read — and an update takes `{ id, <singular>Patch }`.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.advanceRunDocument = exports.createThreadDocument = exports.createRunDocument = exports.appendDocument = exports.eventsDocument = exports.runDocument = exports.runsDocument = exports.EVENT_FIELDS = exports.RUN_FIELDS = void 0;
16
+ exports.RUN_FIELDS = `
17
+ id
18
+ threadId
19
+ actorId
20
+ entityId
21
+ status
22
+ placement
23
+ executionId
24
+ repoUrl
25
+ branch
26
+ baseCommit
27
+ headCommit
28
+ lastEventSeq
29
+ attempt
30
+ parentRunId
31
+ error
32
+ artifacts
33
+ deadlineAt
34
+ startedAt
35
+ finishedAt
36
+ createdAt`;
37
+ // The columns are named for what they describe rather than for the harness that
38
+ // writes them, which is the shape `@agentic-kit/run-log`'s record asserts.
39
+ exports.EVENT_FIELDS = `
40
+ runId
41
+ seq
42
+ recordedAt
43
+ transcriptFormat
44
+ transcriptVersion
45
+ entry`;
46
+ const runsDocument = (names) => `
47
+ query Runs($first: Int!, $where: ${names.runType}Filter) {
48
+ ${names.runs}(first: $first, where: $where, orderBy: [CREATED_AT_DESC]) {
49
+ nodes {${exports.RUN_FIELDS}
50
+ }
51
+ }
52
+ }`;
53
+ exports.runsDocument = runsDocument;
54
+ const runDocument = (names) => `
55
+ query Run($where: ${names.runType}Filter!) {
56
+ ${names.runs}(first: 1, where: $where) {
57
+ nodes {${exports.RUN_FIELDS}
58
+ }
59
+ }
60
+ }`;
61
+ exports.runDocument = runDocument;
62
+ const eventsDocument = (names) => `
63
+ query RunEvents($first: Int!, $where: ${names.eventType}Filter!) {
64
+ ${names.events}(first: $first, where: $where, orderBy: [SEQ_ASC]) {
65
+ nodes {${exports.EVENT_FIELDS}
66
+ }
67
+ }
68
+ }`;
69
+ exports.eventsDocument = eventsDocument;
70
+ const appendDocument = (names) => `
71
+ mutation AppendRunEvent($input: Create${names.eventType}Input!) {
72
+ ${names.createEvent}(input: $input) {
73
+ ${names.event} {${exports.EVENT_FIELDS}
74
+ }
75
+ }
76
+ }`;
77
+ exports.appendDocument = appendDocument;
78
+ /**
79
+ * Open a run.
80
+ *
81
+ * A cloud run is opened by the worker on the platform's side of the callback
82
+ * lane; a local run has no worker, so the host that executes it opens its own
83
+ * row here — as the user, over the tenant API, which is what makes `actor_id`,
84
+ * `principal_id` and `entity_id` the session's claims rather than anything this
85
+ * client could pass. Only the columns a host legitimately chooses are sent.
86
+ */
87
+ const createRunDocument = (names) => `
88
+ mutation CreateRun($input: Create${names.runType}Input!) {
89
+ ${names.createRun}(input: $input) {
90
+ ${names.run} {${exports.RUN_FIELDS}
91
+ }
92
+ }
93
+ }`;
94
+ exports.createRunDocument = createRunDocument;
95
+ /**
96
+ * Open the thread a run hangs from. `agent_run.thread_id` is required and a
97
+ * local host is the first writer of its own conversation, so it creates one.
98
+ */
99
+ const createThreadDocument = (names) => `
100
+ mutation CreateThread($input: Create${names.threadType}Input!) {
101
+ ${names.createThread}(input: $input) {
102
+ ${names.thread} {
103
+ id
104
+ title
105
+ createdAt
106
+ }
107
+ }
108
+ }`;
109
+ exports.createThreadDocument = createThreadDocument;
110
+ const advanceRunDocument = (names) => `
111
+ mutation AdvanceRun($input: Update${names.runType}Input!) {
112
+ ${names.updateRun}(input: $input) {
113
+ ${names.run} {${exports.RUN_FIELDS}
114
+ }
115
+ }
116
+ }`;
117
+ exports.advanceRunDocument = advanceRunDocument;
@@ -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/esm/client.js ADDED
@@ -0,0 +1,284 @@
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 { approvalResolutionMessage, assertRunEventRecord, PI_TRANSCRIPT_FORMAT, START, SUPPORTED_PI_TRANSCRIPT_VERSION, transcriptReaders, } from '@agentic-kit/run-log';
13
+ import { advanceRunDocument, appendDocument, createRunDocument, createThreadDocument, eventsDocument, runDocument, runsDocument, } from './documents';
14
+ import { runLogNames } from './names';
15
+ /** A run holding, or able to hold, compute. */
16
+ export const ATTACHABLE_STATUSES = [
17
+ 'pending',
18
+ 'running',
19
+ 'waiting',
20
+ 'idle',
21
+ ];
22
+ /** A run that will never append again. */
23
+ export const TERMINAL_STATUSES = [
24
+ 'succeeded',
25
+ 'failed',
26
+ 'cancelled',
27
+ ];
28
+ export const isTerminalStatus = (status) => TERMINAL_STATUSES.includes(status);
29
+ const DEFAULT_MAX_APPEND_ATTEMPTS = 5;
30
+ const DEFAULT_READ_LIMIT = 200;
31
+ const DEFAULT_RUN_LIMIT = 50;
32
+ /**
33
+ * A conflict on `(run_id, seq)` — the only error this store retries. Matched on
34
+ * the message because the tenant API reports it as a GraphQL error string; any
35
+ * other failure is rethrown untouched.
36
+ */
37
+ export const isSeqConflict = (error) => {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return /duplicate key|unique constraint|already exists/i.test(message);
40
+ };
41
+ const entryId = (entry) => typeof entry.id === 'string' ? entry.id : undefined;
42
+ export class GraphqlRunLogClient {
43
+ names;
44
+ /**
45
+ * Read by a follower so the view projects exactly the formats this client
46
+ * accepts — one registry decides both what a run may store and what a surface
47
+ * can draw.
48
+ */
49
+ readers;
50
+ request;
51
+ maxAppendAttempts;
52
+ newId;
53
+ now;
54
+ constructor(options) {
55
+ this.request = options.request;
56
+ this.names = runLogNames(options.runTable, options.eventTable, options.threadTable);
57
+ this.maxAppendAttempts =
58
+ options.maxAppendAttempts ?? DEFAULT_MAX_APPEND_ATTEMPTS;
59
+ this.newId = options.newId ?? (() => globalThis.crypto.randomUUID());
60
+ this.now = options.now ?? (() => new Date());
61
+ this.readers = options.readers ?? transcriptReaders;
62
+ }
63
+ async listRuns(args = {}) {
64
+ const where = {};
65
+ if (args.threadId)
66
+ where.threadId = { equalTo: args.threadId };
67
+ if (args.status)
68
+ where.status = { equalTo: args.status };
69
+ const data = await this.request(runsDocument(this.names), {
70
+ first: args.first ?? DEFAULT_RUN_LIMIT,
71
+ where: Object.keys(where).length > 0 ? where : undefined,
72
+ });
73
+ return data[this.names.runs].nodes;
74
+ }
75
+ /**
76
+ * Open a thread. A cloud run is opened against a thread the web UI already
77
+ * created; a host on the user's machine is the first writer of its own
78
+ * conversation, so it creates one and then hangs its runs off it.
79
+ */
80
+ async createThread(input = {}) {
81
+ const data = await this.request(createThreadDocument(this.names), {
82
+ input: {
83
+ [this.names.thread]: {
84
+ ...(input.title === undefined ? {} : { title: input.title }),
85
+ mode: input.mode ?? 'agent',
86
+ },
87
+ },
88
+ });
89
+ const thread = data[this.names.createThread]?.[this.names.thread];
90
+ if (!thread) {
91
+ throw new Error(`${this.names.createThread} returned no thread; the run surface is not writable by this user`);
92
+ }
93
+ return thread;
94
+ }
95
+ /** Open a run. See {@link CreateRunInput} on why identity is not an argument. */
96
+ async createRun(input) {
97
+ const data = await this.request(createRunDocument(this.names), {
98
+ input: {
99
+ [this.names.run]: {
100
+ threadId: input.threadId,
101
+ placement: input.placement,
102
+ ...(input.status === undefined ? {} : { status: input.status }),
103
+ ...(input.repoUrl === undefined ? {} : { repoUrl: input.repoUrl }),
104
+ ...(input.branch === undefined ? {} : { branch: input.branch }),
105
+ },
106
+ },
107
+ });
108
+ const run = data[this.names.createRun]?.[this.names.run];
109
+ if (!run) {
110
+ throw new Error(`${this.names.createRun} returned no run; the run surface is not writable by this user`);
111
+ }
112
+ return run;
113
+ }
114
+ /**
115
+ * Move a run's lifecycle columns. The transcript is the history; this row is
116
+ * the current state a list view reads without replaying it.
117
+ */
118
+ async updateRun(runId, patch) {
119
+ if (Object.keys(patch).length === 0) {
120
+ throw new Error(`run ${runId}: updateRun called with an empty patch`);
121
+ }
122
+ const data = await this.request(advanceRunDocument(this.names), {
123
+ input: { id: runId, [this.names.runPatch]: patch },
124
+ });
125
+ const run = data[this.names.updateRun]?.[this.names.run];
126
+ if (!run) {
127
+ throw new Error(`run ${runId} was not updated; it is not visible or not writable by this user`);
128
+ }
129
+ return run;
130
+ }
131
+ async getRun(runId) {
132
+ const data = await this.request(runDocument(this.names), { where: { id: { equalTo: runId } } });
133
+ const run = data[this.names.runs].nodes[0];
134
+ if (!run) {
135
+ throw new Error(`run ${runId} not found, or not visible to this user`);
136
+ }
137
+ return run;
138
+ }
139
+ async read(runId, cursor = START, limit) {
140
+ const data = await this.request(eventsDocument(this.names), {
141
+ first: limit ?? DEFAULT_READ_LIMIT,
142
+ where: {
143
+ runId: { equalTo: runId },
144
+ seq: { greaterThan: cursor.afterSeq },
145
+ },
146
+ });
147
+ const records = data[this.names.events].nodes.map((node) => assertRunEventRecord(node, this.readers));
148
+ return {
149
+ records,
150
+ cursor: {
151
+ afterSeq: records.length > 0
152
+ ? records[records.length - 1].seq
153
+ : cursor.afterSeq,
154
+ },
155
+ };
156
+ }
157
+ /**
158
+ * Append entries, allocating `seq` from the run's cursor and advancing it.
159
+ *
160
+ * Two writers racing is normal here (a user sends a message while the Job
161
+ * appends a result), so the sequence is not reserved up front: each insert is
162
+ * attempted at the next free position and a rejected one re-reads the run.
163
+ * Entries already present by entry id are skipped, so a retried call is a
164
+ * no-op rather than a duplicated turn.
165
+ */
166
+ async append(runId, entries, options) {
167
+ if (entries.length === 0)
168
+ return [];
169
+ const written = [];
170
+ let pending = [...entries];
171
+ for (let attempt = 0; attempt < this.maxAppendAttempts; attempt += 1) {
172
+ const run = await this.getRun(runId);
173
+ pending = await this.dropAlreadyWritten(runId, run, pending);
174
+ if (pending.length === 0)
175
+ break;
176
+ let seq = run.lastEventSeq;
177
+ try {
178
+ while (pending.length > 0) {
179
+ seq += 1;
180
+ written.push(await this.insertEvent(runId, seq, pending[0], options));
181
+ pending = pending.slice(1);
182
+ }
183
+ }
184
+ catch (error) {
185
+ if (!isSeqConflict(error))
186
+ throw error;
187
+ continue;
188
+ }
189
+ await this.advanceRun(runId, seq);
190
+ return written;
191
+ }
192
+ if (pending.length > 0) {
193
+ 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`);
194
+ }
195
+ return written;
196
+ }
197
+ /**
198
+ * Append one pi message as an entry, chained to the run's current tail.
199
+ *
200
+ * The composer's own turn is the one place this store still writes a
201
+ * transcript rather than relaying one, so it writes the format it declares in
202
+ * `insertEvent`; a second harness contributes its own message builder beside
203
+ * this one and appends through the neutral `append`.
204
+ */
205
+ async appendMessage(runId, message, options) {
206
+ const parentId = await this.tailEntryId(runId);
207
+ const entry = {
208
+ id: this.newId(),
209
+ parentId,
210
+ timestamp: this.now().toISOString(),
211
+ type: 'message',
212
+ message,
213
+ };
214
+ return this.append(runId, [entry], options);
215
+ }
216
+ /** The user's turn: what a run surface's composer sends. */
217
+ async prompt(runId, text) {
218
+ if (!text.trim())
219
+ throw new Error('cannot append an empty prompt');
220
+ return this.appendMessage(runId, {
221
+ role: 'user',
222
+ content: text,
223
+ timestamp: this.now().getTime(),
224
+ });
225
+ }
226
+ /**
227
+ * Answer a pending approval. The decision is a log entry like any other, so
228
+ * the Job sees it on its next cursor read and the history records who allowed
229
+ * what — there is no side channel to reconcile.
230
+ */
231
+ async resolveApproval(runId, input) {
232
+ return this.appendMessage(runId, approvalResolutionMessage(input));
233
+ }
234
+ async insertEvent(runId, seq, entry, options) {
235
+ const data = await this.request(appendDocument(this.names), {
236
+ input: {
237
+ [this.names.event]: {
238
+ runId,
239
+ seq,
240
+ recordedAt: options?.recordedAt ?? this.now().toISOString(),
241
+ // A caller that read its entries out of a harness names that
242
+ // harness's format; only the composer path below leaves it unset,
243
+ // which is why the fallback is pi's.
244
+ transcriptFormat: options?.transcriptFormat ?? PI_TRANSCRIPT_FORMAT,
245
+ transcriptVersion: options?.transcriptVersion ?? SUPPORTED_PI_TRANSCRIPT_VERSION,
246
+ entry,
247
+ },
248
+ },
249
+ });
250
+ return assertRunEventRecord(data[this.names.createEvent][this.names.event], this.readers);
251
+ }
252
+ async advanceRun(runId, seq) {
253
+ await this.request(advanceRunDocument(this.names), {
254
+ input: { id: runId, [this.names.runPatch]: { lastEventSeq: seq } },
255
+ });
256
+ }
257
+ /**
258
+ * Skip entries a previous attempt already wrote. Only the tail is inspected —
259
+ * as many records as the call is trying to append — because that is where a
260
+ * partially applied attempt of *this* call can be, and scanning the whole run
261
+ * to make a retry idempotent would make every append cost the run's length.
262
+ */
263
+ async dropAlreadyWritten(runId, run, pending) {
264
+ const ids = pending.map(entryId).filter((id) => !!id);
265
+ if (ids.length === 0 || run.lastEventSeq === 0)
266
+ return [...pending];
267
+ const window = Math.max(0, run.lastEventSeq - pending.length);
268
+ const page = await this.read(runId, { afterSeq: window }, pending.length);
269
+ const present = new Set(page.records.map((record) => entryId(record.entry)).filter(Boolean));
270
+ return pending.filter((entry) => {
271
+ const id = entryId(entry);
272
+ return !id || !present.has(id);
273
+ });
274
+ }
275
+ async tailEntryId(runId) {
276
+ const run = await this.getRun(runId);
277
+ if (run.lastEventSeq === 0)
278
+ return null;
279
+ const page = await this.read(runId, { afterSeq: run.lastEventSeq - 1 }, 1);
280
+ const last = page.records[page.records.length - 1];
281
+ return last ? (entryId(last.entry) ?? null) : null;
282
+ }
283
+ }
284
+ export const createRunLogClient = (options) => new GraphqlRunLogClient(options);
@@ -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;