@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/names.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The GraphQL names a module's run surface is exposed under.
|
|
4
|
+
*
|
|
5
|
+
* The tables are provisioned per agent module (`agent_module.has_runs`), and a
|
|
6
|
+
* module carries its own table names — `agent_run`/`agent_event` at app scope,
|
|
7
|
+
* `org_agent_run`/`org_agent_event` at org scope — so a reader cannot hardcode
|
|
8
|
+
* the fields it queries. Everything here is derived from the two physical names
|
|
9
|
+
* the module loader already reports, using the same inflection the generated
|
|
10
|
+
* SDK targets use: `agent_event` → node `agentEvent`, connection `agentEvents`,
|
|
11
|
+
* mutation `createAgentEvent`, order-by `SEQ_ASC`.
|
|
12
|
+
*
|
|
13
|
+
* Derived rather than injected because the alternative is a table of names per
|
|
14
|
+
* tenant, which drifts the moment a module is provisioned under a new scope.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.DEFAULT_THREAD_TABLE = exports.DEFAULT_EVENT_TABLE = exports.DEFAULT_RUN_TABLE = void 0;
|
|
18
|
+
exports.runLogNames = runLogNames;
|
|
19
|
+
const camelize = (snake) => snake
|
|
20
|
+
.split('_')
|
|
21
|
+
.filter((part) => part.length > 0)
|
|
22
|
+
.map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))
|
|
23
|
+
.join('');
|
|
24
|
+
const pascalize = (snake) => {
|
|
25
|
+
const camel = camelize(snake);
|
|
26
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Graphile's pluralisation for the shapes a table name can take here. Only the
|
|
30
|
+
* regular cases and the two irregular endings a generated name can hit are
|
|
31
|
+
* handled: a table whose name pluralises irregularly would produce a field this
|
|
32
|
+
* cannot name, and that is a schema this reader must not silently mis-address.
|
|
33
|
+
*/
|
|
34
|
+
const pluralize = (word) => {
|
|
35
|
+
if (/(s|x|z|ch|sh)$/.test(word))
|
|
36
|
+
return `${word}es`;
|
|
37
|
+
if (/[^aeiou]y$/.test(word))
|
|
38
|
+
return `${word.slice(0, -1)}ies`;
|
|
39
|
+
return `${word}s`;
|
|
40
|
+
};
|
|
41
|
+
exports.DEFAULT_RUN_TABLE = 'agent_run';
|
|
42
|
+
exports.DEFAULT_EVENT_TABLE = 'agent_event';
|
|
43
|
+
exports.DEFAULT_THREAD_TABLE = 'agent_thread';
|
|
44
|
+
function runLogNames(runTable = exports.DEFAULT_RUN_TABLE, eventTable = exports.DEFAULT_EVENT_TABLE, threadTable = exports.DEFAULT_THREAD_TABLE) {
|
|
45
|
+
if (!runTable || !eventTable || !threadTable) {
|
|
46
|
+
throw new Error('runLogNames requires the run, event and thread table names; a module without a run surface has none of them');
|
|
47
|
+
}
|
|
48
|
+
const run = camelize(runTable);
|
|
49
|
+
const event = camelize(eventTable);
|
|
50
|
+
const thread = camelize(threadTable);
|
|
51
|
+
const runType = pascalize(runTable);
|
|
52
|
+
const eventType = pascalize(eventTable);
|
|
53
|
+
const threadType = pascalize(threadTable);
|
|
54
|
+
return {
|
|
55
|
+
runTable,
|
|
56
|
+
eventTable,
|
|
57
|
+
threadTable,
|
|
58
|
+
run,
|
|
59
|
+
event,
|
|
60
|
+
thread,
|
|
61
|
+
runs: pluralize(run),
|
|
62
|
+
events: pluralize(event),
|
|
63
|
+
threads: pluralize(thread),
|
|
64
|
+
runType,
|
|
65
|
+
eventType,
|
|
66
|
+
threadType,
|
|
67
|
+
createEvent: `create${eventType}`,
|
|
68
|
+
createRun: `create${runType}`,
|
|
69
|
+
createThread: `create${threadType}`,
|
|
70
|
+
updateRun: `update${runType}`,
|
|
71
|
+
runPatch: `${run}Patch`,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentic-kit/run-log-client",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"author": "Constructive <developers@constructive.io>",
|
|
5
|
+
"description": "The run log as a browser reads it: a tenant-GraphQL RunLogStore over a module's agent_run/agent_event pair, plus the follower loop every run surface uses",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"module": "esm/index.js",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"homepage": "https://github.com/constructive-io/constructive",
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"directory": "dist"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/constructive-io/constructive"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/constructive-io/constructive/issues"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"clean": "makage clean",
|
|
24
|
+
"prepack": "npm run build",
|
|
25
|
+
"build": "makage build",
|
|
26
|
+
"build:dev": "makage build --dev",
|
|
27
|
+
"lint": "eslint . --fix",
|
|
28
|
+
"test": "jest",
|
|
29
|
+
"test:watch": "jest --watch"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"agentic-kit",
|
|
33
|
+
"pi",
|
|
34
|
+
"coding-agent",
|
|
35
|
+
"run-log",
|
|
36
|
+
"constructive"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@agentic-kit/run-log": "^0.6.0"
|
|
40
|
+
},
|
|
41
|
+
"gitHead": "9bba983b38395277e0400294aaa380e586b049d4"
|
|
42
|
+
}
|
package/realtime.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The push wakeup, when the app has one.
|
|
3
|
+
*
|
|
4
|
+
* A run surface is correct without this: `follow` polls, and every read is a
|
|
5
|
+
* cursor read, so nothing is missed by a reader that woke late. What realtime
|
|
6
|
+
* buys is latency, and it costs nothing in correctness — which is why it is a
|
|
7
|
+
* wakeup here rather than a delivery mechanism. Records are never taken from a
|
|
8
|
+
* subscription payload: the notification says *something changed*, the cursor
|
|
9
|
+
* read says *what*, so a dropped, duplicated or out-of-order notification cannot
|
|
10
|
+
* corrupt a transcript, and a reconnecting client catches up by reading rather
|
|
11
|
+
* than by replaying a socket.
|
|
12
|
+
*
|
|
13
|
+
* This is the platform's existing DataRealtime lane (statement-level triggers →
|
|
14
|
+
* `change_log` → the `on<Table>Changed` subscription field), not a new transport.
|
|
15
|
+
* The event table only carries it when the app declares DataRealtime on it, so
|
|
16
|
+
* the subscription is optional everywhere in this package.
|
|
17
|
+
*/
|
|
18
|
+
import type { RunLogNames } from './names';
|
|
19
|
+
interface WsSink {
|
|
20
|
+
next(value: {
|
|
21
|
+
data?: unknown;
|
|
22
|
+
errors?: readonly {
|
|
23
|
+
message: string;
|
|
24
|
+
}[];
|
|
25
|
+
}): void;
|
|
26
|
+
error(error: unknown): void;
|
|
27
|
+
complete(): void;
|
|
28
|
+
}
|
|
29
|
+
/** The graphql-ws surface this package uses — supplied by the host, never imported. */
|
|
30
|
+
export interface RunLogWsClient {
|
|
31
|
+
subscribe(payload: {
|
|
32
|
+
query: string;
|
|
33
|
+
variables?: Record<string, unknown>;
|
|
34
|
+
}, sink: WsSink): () => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `on<Type>Changed`, the name the platform's codegen gives a DataRealtime table's
|
|
38
|
+
* subscription field. A host that reads the field out of `_meta`
|
|
39
|
+
* (`MetaRealtime.subscriptionFieldName`) can pass it instead.
|
|
40
|
+
*/
|
|
41
|
+
export declare const changedSubscriptionField: (names: RunLogNames) => string;
|
|
42
|
+
/**
|
|
43
|
+
* The generated subscription takes no arguments and is per-table, not per-run, so
|
|
44
|
+
* a wakeup may be for somebody else's run — harmless, because the follower's
|
|
45
|
+
* cursor read is what decides whether there is anything new for *this* one.
|
|
46
|
+
*/
|
|
47
|
+
export declare const changedSubscriptionDocument: (names: RunLogNames, field?: string) => string;
|
|
48
|
+
export interface RealtimeWakeupOptions {
|
|
49
|
+
client: RunLogWsClient;
|
|
50
|
+
names: RunLogNames;
|
|
51
|
+
/** Override when the host resolves the field name from `_meta` instead. */
|
|
52
|
+
subscriptionField?: string;
|
|
53
|
+
/** Surfaced to the caller; a failed socket must not silently become silence. */
|
|
54
|
+
onError?: (error: Error) => void;
|
|
55
|
+
}
|
|
56
|
+
export interface RealtimeWakeup {
|
|
57
|
+
/** Passed to `follow` as `waitForChange`. */
|
|
58
|
+
waitForChange: (signal?: AbortSignal) => Promise<void>;
|
|
59
|
+
close: () => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One subscription, many waits: the socket stays open across follow iterations
|
|
63
|
+
* and each `waitForChange` resolves on the next notification (or immediately, if
|
|
64
|
+
* one arrived while the follower was busy reading — otherwise a change landing
|
|
65
|
+
* mid-read would leave the follower asleep until the poll timer).
|
|
66
|
+
*/
|
|
67
|
+
export declare function createRealtimeWakeup(options: RealtimeWakeupOptions): RealtimeWakeup;
|
|
68
|
+
export {};
|
package/realtime.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The push wakeup, when the app has one.
|
|
4
|
+
*
|
|
5
|
+
* A run surface is correct without this: `follow` polls, and every read is a
|
|
6
|
+
* cursor read, so nothing is missed by a reader that woke late. What realtime
|
|
7
|
+
* buys is latency, and it costs nothing in correctness — which is why it is a
|
|
8
|
+
* wakeup here rather than a delivery mechanism. Records are never taken from a
|
|
9
|
+
* subscription payload: the notification says *something changed*, the cursor
|
|
10
|
+
* read says *what*, so a dropped, duplicated or out-of-order notification cannot
|
|
11
|
+
* corrupt a transcript, and a reconnecting client catches up by reading rather
|
|
12
|
+
* than by replaying a socket.
|
|
13
|
+
*
|
|
14
|
+
* This is the platform's existing DataRealtime lane (statement-level triggers →
|
|
15
|
+
* `change_log` → the `on<Table>Changed` subscription field), not a new transport.
|
|
16
|
+
* The event table only carries it when the app declares DataRealtime on it, so
|
|
17
|
+
* the subscription is optional everywhere in this package.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.changedSubscriptionDocument = exports.changedSubscriptionField = void 0;
|
|
21
|
+
exports.createRealtimeWakeup = createRealtimeWakeup;
|
|
22
|
+
/**
|
|
23
|
+
* `on<Type>Changed`, the name the platform's codegen gives a DataRealtime table's
|
|
24
|
+
* subscription field. A host that reads the field out of `_meta`
|
|
25
|
+
* (`MetaRealtime.subscriptionFieldName`) can pass it instead.
|
|
26
|
+
*/
|
|
27
|
+
const changedSubscriptionField = (names) => `on${names.eventType}Changed`;
|
|
28
|
+
exports.changedSubscriptionField = changedSubscriptionField;
|
|
29
|
+
/**
|
|
30
|
+
* The generated subscription takes no arguments and is per-table, not per-run, so
|
|
31
|
+
* a wakeup may be for somebody else's run — harmless, because the follower's
|
|
32
|
+
* cursor read is what decides whether there is anything new for *this* one.
|
|
33
|
+
*/
|
|
34
|
+
const changedSubscriptionDocument = (names, field = (0, exports.changedSubscriptionField)(names)) => `
|
|
35
|
+
subscription RunEventChanged {
|
|
36
|
+
${field} {
|
|
37
|
+
event
|
|
38
|
+
${names.event} { __typename }
|
|
39
|
+
timestamp
|
|
40
|
+
}
|
|
41
|
+
}`;
|
|
42
|
+
exports.changedSubscriptionDocument = changedSubscriptionDocument;
|
|
43
|
+
/**
|
|
44
|
+
* One subscription, many waits: the socket stays open across follow iterations
|
|
45
|
+
* and each `waitForChange` resolves on the next notification (or immediately, if
|
|
46
|
+
* one arrived while the follower was busy reading — otherwise a change landing
|
|
47
|
+
* mid-read would leave the follower asleep until the poll timer).
|
|
48
|
+
*/
|
|
49
|
+
function createRealtimeWakeup(options) {
|
|
50
|
+
let pendingChange = false;
|
|
51
|
+
let wake = null;
|
|
52
|
+
let closed = false;
|
|
53
|
+
const signalChange = () => {
|
|
54
|
+
pendingChange = true;
|
|
55
|
+
const resume = wake;
|
|
56
|
+
wake = null;
|
|
57
|
+
if (resume)
|
|
58
|
+
resume();
|
|
59
|
+
};
|
|
60
|
+
const unsubscribe = options.client.subscribe({
|
|
61
|
+
query: (0, exports.changedSubscriptionDocument)(options.names, options.subscriptionField),
|
|
62
|
+
}, {
|
|
63
|
+
next: (result) => {
|
|
64
|
+
if (result.errors && result.errors.length > 0) {
|
|
65
|
+
options.onError?.(new Error(result.errors.map((e) => e.message).join('; ')));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
signalChange();
|
|
69
|
+
},
|
|
70
|
+
error: (error) => {
|
|
71
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
72
|
+
// Wake the follower so it falls back to polling instead of waiting on a
|
|
73
|
+
// socket that is gone.
|
|
74
|
+
signalChange();
|
|
75
|
+
},
|
|
76
|
+
complete: () => signalChange(),
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
waitForChange: (signal) => {
|
|
80
|
+
if (pendingChange || closed) {
|
|
81
|
+
pendingChange = false;
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
}
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
wake = resolve;
|
|
86
|
+
signal?.addEventListener('abort', () => resolve(), { once: true });
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
close: () => {
|
|
90
|
+
closed = true;
|
|
91
|
+
unsubscribe();
|
|
92
|
+
signalChange();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
package/surface.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling "this database has no run surface" apart from "the read failed".
|
|
3
|
+
*
|
|
4
|
+
* A database whose agent module was provisioned without `has_runs` has no
|
|
5
|
+
* `agent_run`/`agent_event` at all, so the schema rejects the query outright
|
|
6
|
+
* rather than returning zero rows. That is a configuration fact about the
|
|
7
|
+
* database, not a failure of the read, and a raw GraphQL validation dump is a
|
|
8
|
+
* poor way to tell an operator so. The error is still an error — it is only
|
|
9
|
+
* classified here, never swallowed.
|
|
10
|
+
*/
|
|
11
|
+
import type { RunLogNames } from './names';
|
|
12
|
+
/**
|
|
13
|
+
* Matched on the validation messages the tenant API returns, because the absence
|
|
14
|
+
* of the tables is only observable through the schema: the run collection is not
|
|
15
|
+
* a field and the run filter is not a type.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isMissingRunSurface(error: unknown, names: RunLogNames): boolean;
|
package/surface.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telling "this database has no run surface" apart from "the read failed".
|
|
4
|
+
*
|
|
5
|
+
* A database whose agent module was provisioned without `has_runs` has no
|
|
6
|
+
* `agent_run`/`agent_event` at all, so the schema rejects the query outright
|
|
7
|
+
* rather than returning zero rows. That is a configuration fact about the
|
|
8
|
+
* database, not a failure of the read, and a raw GraphQL validation dump is a
|
|
9
|
+
* poor way to tell an operator so. The error is still an error — it is only
|
|
10
|
+
* classified here, never swallowed.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.isMissingRunSurface = isMissingRunSurface;
|
|
14
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
15
|
+
/**
|
|
16
|
+
* Matched on the validation messages the tenant API returns, because the absence
|
|
17
|
+
* of the tables is only observable through the schema: the run collection is not
|
|
18
|
+
* a field and the run filter is not a type.
|
|
19
|
+
*/
|
|
20
|
+
function isMissingRunSurface(error, names) {
|
|
21
|
+
const message = messageOf(error);
|
|
22
|
+
return (message.includes(`Cannot query field "${names.runs}"`) ||
|
|
23
|
+
message.includes(`Unknown type "${names.runType}Filter"`));
|
|
24
|
+
}
|
package/transport.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GraphQL transport, kept behind one function so the store never knows how
|
|
3
|
+
* a host authenticates.
|
|
4
|
+
*
|
|
5
|
+
* The run surface lives in the tenant's own database, so this addresses the
|
|
6
|
+
* tenant API endpoint the platform UI already resolves per database, with the
|
|
7
|
+
* user's bearer token — the reader is the user, RLS decides what a run shows,
|
|
8
|
+
* and no service credential is involved anywhere in this lane.
|
|
9
|
+
*/
|
|
10
|
+
export type GraphqlRequest = <T>(query: string, variables?: Record<string, unknown>) => Promise<T>;
|
|
11
|
+
export interface GraphqlTransportOptions {
|
|
12
|
+
endpoint: string;
|
|
13
|
+
/** Resolved per call, so a rotated token is picked up without rebuilding. */
|
|
14
|
+
headers?: () => Record<string, string>;
|
|
15
|
+
fetch?: typeof globalThis.fetch;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A GraphQL error is thrown, never folded into an empty page: a transcript that
|
|
20
|
+
* failed to load and a run with no events look identical to a renderer, and the
|
|
21
|
+
* second is a lie a user cannot detect.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createGraphqlRequest(options: GraphqlTransportOptions): GraphqlRequest;
|
package/transport.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The GraphQL transport, kept behind one function so the store never knows how
|
|
4
|
+
* a host authenticates.
|
|
5
|
+
*
|
|
6
|
+
* The run surface lives in the tenant's own database, so this addresses the
|
|
7
|
+
* tenant API endpoint the platform UI already resolves per database, with the
|
|
8
|
+
* user's bearer token — the reader is the user, RLS decides what a run shows,
|
|
9
|
+
* and no service credential is involved anywhere in this lane.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createGraphqlRequest = createGraphqlRequest;
|
|
13
|
+
/**
|
|
14
|
+
* A GraphQL error is thrown, never folded into an empty page: a transcript that
|
|
15
|
+
* failed to load and a run with no events look identical to a renderer, and the
|
|
16
|
+
* second is a lie a user cannot detect.
|
|
17
|
+
*/
|
|
18
|
+
function createGraphqlRequest(options) {
|
|
19
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
20
|
+
if (!doFetch) {
|
|
21
|
+
throw new Error('run-log-client needs a fetch implementation');
|
|
22
|
+
}
|
|
23
|
+
return async (query, variables) => {
|
|
24
|
+
const res = await doFetch(options.endpoint, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
...(options.headers ? options.headers() : {}),
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ query, variables: variables ?? {} }),
|
|
31
|
+
signal: options.signal,
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`run log request failed: ${res.status} ${await res.text()}`);
|
|
35
|
+
}
|
|
36
|
+
const body = (await res.json());
|
|
37
|
+
if (body.errors && body.errors.length > 0) {
|
|
38
|
+
throw new Error(`run log request failed: ${body.errors.map((e) => e.message).join('; ')}`);
|
|
39
|
+
}
|
|
40
|
+
if (!body.data) {
|
|
41
|
+
throw new Error('run log request returned no data');
|
|
42
|
+
}
|
|
43
|
+
return body.data;
|
|
44
|
+
};
|
|
45
|
+
}
|
package/view.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run view: everything a surface draws, derived from the log and nothing
|
|
3
|
+
* else.
|
|
4
|
+
*
|
|
5
|
+
* Kept as a pure reducer over records so the rendering layer holds no
|
|
6
|
+
* transcript logic of its own — the same reduction runs in a React panel, a
|
|
7
|
+
* test, or a CLI tail. Records are merged by `seq` rather than appended, which
|
|
8
|
+
* is what makes a reconnect safe: catching up re-reads a window that may overlap
|
|
9
|
+
* what is already held, and an overlapping read must be idempotent rather than
|
|
10
|
+
* duplicating turns.
|
|
11
|
+
*/
|
|
12
|
+
import { type Conversation, type RunEventRecord, type RunLogCursor, type RunUsage, type ToolStateProjection, type TranscriptReaderRegistry } from '@agentic-kit/run-log';
|
|
13
|
+
import { type RunSummary } from './client';
|
|
14
|
+
export type RunViewPhase = 'loading' | 'live' | 'terminal' | 'error';
|
|
15
|
+
export interface RunView {
|
|
16
|
+
run?: RunSummary;
|
|
17
|
+
/** Records held, by ascending `seq`, without duplicates. */
|
|
18
|
+
records: RunEventRecord[];
|
|
19
|
+
cursor: RunLogCursor;
|
|
20
|
+
conversation: Conversation;
|
|
21
|
+
tools: ToolStateProjection;
|
|
22
|
+
usage: RunUsage;
|
|
23
|
+
phase: RunViewPhase;
|
|
24
|
+
/** Set when a read or a subscription failed; never hidden behind an empty view. */
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Which transcript formats this view can read. Absent means the default
|
|
29
|
+
* registry, so a surface that only ever sees one format configures nothing;
|
|
30
|
+
* a host wired to a second harness passes its registry and the same reducers
|
|
31
|
+
* project its records.
|
|
32
|
+
*/
|
|
33
|
+
export interface RunViewProjection {
|
|
34
|
+
readers?: TranscriptReaderRegistry;
|
|
35
|
+
}
|
|
36
|
+
export declare const emptyRunView: () => RunView;
|
|
37
|
+
/**
|
|
38
|
+
* Merge by `seq`: a later read of the same position replaces the earlier copy
|
|
39
|
+
* (the row is immutable, so they are the same record), and order is by position
|
|
40
|
+
* rather than arrival.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeRecords(existing: readonly RunEventRecord[], incoming: readonly RunEventRecord[]): RunEventRecord[];
|
|
43
|
+
export declare function applyRecords(view: RunView, incoming: readonly RunEventRecord[], projection?: RunViewProjection): RunView;
|
|
44
|
+
export declare function applyRun(view: RunView, run: RunSummary, projection?: RunViewProjection): RunView;
|
|
45
|
+
/**
|
|
46
|
+
* A failed read is a visible error, not an empty transcript: a surface that
|
|
47
|
+
* cannot reach the log must say so, because "no events yet" and "I could not
|
|
48
|
+
* read your events" look identical to a user otherwise.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyError(view: RunView, error: unknown, projection?: RunViewProjection): RunView;
|
|
51
|
+
/** True while the run may still append — the only reason to keep following. */
|
|
52
|
+
export declare const isFollowable: (view: RunView) => boolean;
|
package/view.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The run view: everything a surface draws, derived from the log and nothing
|
|
4
|
+
* else.
|
|
5
|
+
*
|
|
6
|
+
* Kept as a pure reducer over records so the rendering layer holds no
|
|
7
|
+
* transcript logic of its own — the same reduction runs in a React panel, a
|
|
8
|
+
* test, or a CLI tail. Records are merged by `seq` rather than appended, which
|
|
9
|
+
* is what makes a reconnect safe: catching up re-reads a window that may overlap
|
|
10
|
+
* what is already held, and an overlapping read must be idempotent rather than
|
|
11
|
+
* duplicating turns.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.isFollowable = exports.emptyRunView = void 0;
|
|
15
|
+
exports.mergeRecords = mergeRecords;
|
|
16
|
+
exports.applyRecords = applyRecords;
|
|
17
|
+
exports.applyRun = applyRun;
|
|
18
|
+
exports.applyError = applyError;
|
|
19
|
+
const run_log_1 = require("@agentic-kit/run-log");
|
|
20
|
+
const client_1 = require("./client");
|
|
21
|
+
const project = (records, run, phase, error, projection = {}) => ({
|
|
22
|
+
run,
|
|
23
|
+
records,
|
|
24
|
+
cursor: records.length > 0
|
|
25
|
+
? { afterSeq: records[records.length - 1].seq }
|
|
26
|
+
: run_log_1.START,
|
|
27
|
+
conversation: (0, run_log_1.projectParts)(records, projection),
|
|
28
|
+
tools: (0, run_log_1.projectToolState)(records, projection),
|
|
29
|
+
usage: (0, run_log_1.projectUsage)(records, projection),
|
|
30
|
+
phase,
|
|
31
|
+
error,
|
|
32
|
+
});
|
|
33
|
+
const emptyRunView = () => project([], undefined, 'loading');
|
|
34
|
+
exports.emptyRunView = emptyRunView;
|
|
35
|
+
/**
|
|
36
|
+
* Merge by `seq`: a later read of the same position replaces the earlier copy
|
|
37
|
+
* (the row is immutable, so they are the same record), and order is by position
|
|
38
|
+
* rather than arrival.
|
|
39
|
+
*/
|
|
40
|
+
function mergeRecords(existing, incoming) {
|
|
41
|
+
if (incoming.length === 0)
|
|
42
|
+
return [...existing];
|
|
43
|
+
const bySeq = new Map();
|
|
44
|
+
for (const record of existing)
|
|
45
|
+
bySeq.set(record.seq, record);
|
|
46
|
+
for (const record of incoming)
|
|
47
|
+
bySeq.set(record.seq, record);
|
|
48
|
+
return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
49
|
+
}
|
|
50
|
+
const phaseFor = (run) => run && (0, client_1.isTerminalStatus)(run.status) ? 'terminal' : 'live';
|
|
51
|
+
function applyRecords(view, incoming, projection) {
|
|
52
|
+
const records = mergeRecords(view.records, incoming);
|
|
53
|
+
return project(records, view.run, phaseFor(view.run), view.error, projection);
|
|
54
|
+
}
|
|
55
|
+
function applyRun(view, run, projection) {
|
|
56
|
+
return project(view.records, run, phaseFor(run), view.error, projection);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A failed read is a visible error, not an empty transcript: a surface that
|
|
60
|
+
* cannot reach the log must say so, because "no events yet" and "I could not
|
|
61
|
+
* read your events" look identical to a user otherwise.
|
|
62
|
+
*/
|
|
63
|
+
function applyError(view, error, projection) {
|
|
64
|
+
return project(view.records, view.run, 'error', error instanceof Error ? error.message : String(error), projection);
|
|
65
|
+
}
|
|
66
|
+
/** True while the run may still append — the only reason to keep following. */
|
|
67
|
+
const isFollowable = (view) => view.phase === 'live' || view.phase === 'loading';
|
|
68
|
+
exports.isFollowable = isFollowable;
|