@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 +23 -0
- package/README.md +82 -0
- package/client.d.ts +185 -0
- package/client.js +291 -0
- package/documents.d.ts +35 -0
- package/documents.js +117 -0
- package/esm/client.d.ts +185 -0
- package/esm/client.js +284 -0
- package/esm/documents.d.ts +35 -0
- package/esm/documents.js +107 -0
- package/esm/follow-run.d.ts +38 -0
- package/esm/follow-run.js +91 -0
- package/esm/index.d.ts +22 -0
- package/esm/index.js +21 -0
- package/esm/local-run.d.ts +49 -0
- package/esm/local-run.js +69 -0
- package/esm/names.d.ts +47 -0
- package/esm/names.js +69 -0
- package/esm/realtime.d.ts +68 -0
- package/esm/realtime.js +89 -0
- package/esm/surface.d.ts +17 -0
- package/esm/surface.js +21 -0
- package/esm/transport.d.ts +23 -0
- package/esm/transport.js +42 -0
- package/esm/view.d.ts +52 -0
- package/esm/view.js +59 -0
- package/follow-run.d.ts +38 -0
- package/follow-run.js +94 -0
- package/index.d.ts +22 -0
- package/index.js +60 -0
- package/local-run.d.ts +49 -0
- package/local-run.js +76 -0
- package/names.d.ts +47 -0
- package/names.js +73 -0
- package/package.json +42 -0
- package/realtime.d.ts +68 -0
- package/realtime.js +95 -0
- package/surface.d.ts +17 -0
- package/surface.js +24 -0
- package/transport.d.ts +23 -0
- package/transport.js +45 -0
- package/view.d.ts +52 -0
- package/view.js +68 -0
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;
|