@agentic-kit/run-log 0.3.0 → 0.5.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/README.md +11 -9
- package/esm/file-store.d.ts +1 -1
- package/esm/file-store.js +3 -2
- package/esm/index.d.ts +4 -2
- package/esm/index.js +4 -2
- package/esm/mirror.d.ts +63 -0
- package/esm/mirror.js +87 -0
- package/esm/projectors/parts.d.ts +1 -1
- package/esm/projectors/parts.js +1 -1
- package/esm/projectors/session.d.ts +2 -2
- package/esm/projectors/session.js +8 -6
- package/esm/projectors/spans.d.ts +1 -1
- package/esm/projectors/spans.js +1 -1
- package/esm/projectors/tool-state.js +1 -1
- package/esm/projectors/usage.d.ts +1 -1
- package/esm/projectors/usage.js +2 -2
- package/esm/record.d.ts +17 -14
- package/esm/record.js +19 -14
- package/esm/store.d.ts +6 -3
- package/esm/store.js +3 -2
- package/esm/transcripts/format.d.ts +25 -0
- package/esm/transcripts/format.js +29 -0
- package/file-store.d.ts +1 -1
- package/file-store.js +2 -1
- package/index.d.ts +4 -2
- package/index.js +17 -12
- package/mirror.d.ts +63 -0
- package/mirror.js +91 -0
- package/package.json +2 -2
- package/projectors/parts.d.ts +1 -1
- package/projectors/parts.js +1 -1
- package/projectors/session.d.ts +2 -2
- package/projectors/session.js +7 -5
- package/projectors/spans.d.ts +1 -1
- package/projectors/spans.js +1 -1
- package/projectors/tool-state.js +1 -1
- package/projectors/usage.d.ts +1 -1
- package/projectors/usage.js +2 -2
- package/record.d.ts +17 -14
- package/record.js +20 -15
- package/store.d.ts +6 -3
- package/store.js +2 -1
- package/transcripts/format.d.ts +25 -0
- package/transcripts/format.js +33 -0
- /package/esm/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
- /package/esm/{pi-entry.js → transcripts/pi-entry.js} +0 -0
- /package/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
- /package/{pi-entry.js → transcripts/pi-entry.js} +0 -0
package/README.md
CHANGED
|
@@ -9,21 +9,22 @@ The append-only **run log**: one ordered record of what an agent run did, wherev
|
|
|
9
9
|
A coding agent run has to be observable from a desktop app, a web UI and a CLI; it has to be resumable after a crash, a restart, or a move from the cloud to a laptop; and it has to be meterable. Those are usually four subsystems. Here they are four *projections* of one log:
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
harness entries ──► run log (append-only) ──┬──► transcript parts (what a UI draws)
|
|
13
|
+
├──► usage totals (what a run cost)
|
|
14
|
+
├──► tool + approval (what is blocked)
|
|
15
|
+
└──► pi session file (how it resumes)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The log stores **
|
|
18
|
+
The log stores a harness's **entries verbatim** under five platform-owned fields. A harness already versions and migrates its own session format, so re-encoding it into a second semantic event model would mean maintaining a translation layer that silently loses whatever it adds next. `transcriptFormat` is what makes that safe with more than one harness: it travels with the record, so a reader picks its projector by format and only then migrates by version.
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
{
|
|
22
22
|
runId, // which run
|
|
23
23
|
seq, // 1-based, gapless, strictly increasing
|
|
24
24
|
recordedAt, // when the platform durably recorded it
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
transcriptFormat, // which transcript the entry is encoded in, e.g. `pi`
|
|
26
|
+
transcriptVersion, // that transcript's format version at write time
|
|
27
|
+
entry // the harness's entry, byte-for-byte
|
|
27
28
|
}
|
|
28
29
|
```
|
|
29
30
|
|
|
@@ -100,8 +101,9 @@ Unreadable input fails loudly. A corrupt record, a mixed-version log, or a sessi
|
|
|
100
101
|
|
|
101
102
|
## Related
|
|
102
103
|
|
|
103
|
-
- [`@agentic-kit/pi`](../pi) —
|
|
104
|
-
- [`@agentic-kit/harness`](../harness) — host-neutral gates and
|
|
104
|
+
- [`@agentic-kit/pi`](../pi) — the pi adapter: typed db tools, and the lane that mirrors pi's session entries into this log
|
|
105
|
+
- [`@agentic-kit/harness`](../harness) — host-neutral gates, policy, and the harness adapter contract
|
|
106
|
+
- [`@agentic-kit/metering`](../metering) — the metered gateway and usage reporting
|
|
105
107
|
- [`agentic-server`](../agentic-server) — the metered inference gateway
|
|
106
108
|
|
|
107
109
|
## License
|
package/esm/file-store.d.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* dropped. This is the store a local run uses before (or without) a database,
|
|
11
11
|
* and the one tests use when they want the log to survive a process restart.
|
|
12
12
|
*/
|
|
13
|
-
import type { PiSessionEntry } from './pi-entry';
|
|
14
13
|
import { type SessionProjectionOptions } from './projectors/session';
|
|
15
14
|
import { type RunEventRecord } from './record';
|
|
16
15
|
import { type AppendOptions, type RunLogCursor, type RunLogPage, type RunLogStore } from './store';
|
|
16
|
+
import type { PiSessionEntry } from './transcripts/pi-entry';
|
|
17
17
|
export interface FileRunLogStoreOptions {
|
|
18
18
|
/** Absolute path of the log file. Parent directories are created. */
|
|
19
19
|
path: string;
|
package/esm/file-store.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { dirname } from 'node:path';
|
|
15
15
|
import { projectSession } from './projectors/session';
|
|
16
|
-
import { assertRunEventRecord, idempotencyKey,
|
|
16
|
+
import { assertRunEventRecord, idempotencyKey, wrapEntry } from './record';
|
|
17
17
|
import { cursorAfter, START } from './store';
|
|
18
18
|
export class FileRunLogStore {
|
|
19
19
|
path;
|
|
@@ -54,7 +54,8 @@ export class FileRunLogStore {
|
|
|
54
54
|
seq: existing.length + written.length + 1,
|
|
55
55
|
entry,
|
|
56
56
|
...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
|
|
57
|
-
|
|
57
|
+
...(options.transcriptFormat === undefined ? {} : { transcriptFormat: options.transcriptFormat }),
|
|
58
|
+
...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
|
|
58
59
|
}));
|
|
59
60
|
seen.add(key);
|
|
60
61
|
}
|
package/esm/index.d.ts
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { constructiveEntryTypes, type EntryTypeDefinition, EntryTypeRegistry, type EntryVisibility } from './entry-types';
|
|
10
10
|
export { follow, type FollowOptions, readAll } from './follow';
|
|
11
|
-
export {
|
|
11
|
+
export { type SessionEntrySource, SessionMirror, type SessionMirrorOptions } from './mirror';
|
|
12
12
|
export { type BashPart, type Conversation, type ConversationPart, type CustomPart, projectParts, type SummaryPart, type TextPart, type ThinkingPart, type ToolPart, type ToolStatus, type UnknownPart } from './projectors/parts';
|
|
13
13
|
export { parseSessionJsonl, projectSession, type SessionProjection, type SessionProjectionOptions } from './projectors/session';
|
|
14
14
|
export { type ApprovalSpan, type ModelSpan, projectSpans, type Span, type SpanBase, type SpanKind, type SpanProjection, type SpanStatus, type SpanTiming, type ToolSpan } from './projectors/spans';
|
|
15
15
|
export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, type ApprovalRequestInput, approvalRequestMessage, type ApprovalResolutionInput, approvalResolutionMessage, type ApprovalState, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, type GateDecisionDetails, gateDecisionMessage, type GateDecisionState, projectToolState, type ToolCallState, type ToolCallStatus, type ToolStateProjection } from './projectors/tool-state';
|
|
16
16
|
export { modelKey, type ModelUsage, projectUsage, type RunUsage, type UsageTotals } from './projectors/usage';
|
|
17
|
-
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord,
|
|
17
|
+
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, wrapEntry, type WrapEntryOptions } from './record';
|
|
18
18
|
export { type AppendOptions, cursorAfter, MemoryRunLogStore, type RunLogAppendStore, type RunLogCursor, type RunLogPage, type RunLogReadStore, type RunLogStore, START } from './store';
|
|
19
|
+
export { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION, type TranscriptFormat } from './transcripts/format';
|
|
20
|
+
export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, type PiAssistantMessage, type PiBashExecutionMessage, type PiBranchSummaryEntry, type PiCompactionEntry, type PiContent, type PiCustomMessage, type PiEntryBase, type PiImageContent, type PiMessage, type PiMessageEntry, type PiOtherEntry, type PiSessionEntry, type PiSessionHeader, type PiSummaryMessage, type PiTextContent, type PiThinkingContent, type PiToolCallContent, type PiToolResultMessage, type PiUsage, type PiUsageCost, type PiUserMessage, toolCalls } from './transcripts/pi-entry';
|
package/esm/index.js
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { constructiveEntryTypes, EntryTypeRegistry } from './entry-types';
|
|
10
10
|
export { follow, readAll } from './follow';
|
|
11
|
-
export {
|
|
11
|
+
export { SessionMirror } from './mirror';
|
|
12
12
|
export { projectParts } from './projectors/parts';
|
|
13
13
|
export { parseSessionJsonl, projectSession } from './projectors/session';
|
|
14
14
|
export { projectSpans } from './projectors/spans';
|
|
15
15
|
export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, gateDecisionMessage, projectToolState } from './projectors/tool-state';
|
|
16
16
|
export { modelKey, projectUsage } from './projectors/usage';
|
|
17
|
-
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION,
|
|
17
|
+
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, wrapEntry } from './record';
|
|
18
18
|
export { cursorAfter, MemoryRunLogStore, START } from './store';
|
|
19
|
+
export { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from './transcripts/format';
|
|
20
|
+
export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './transcripts/pi-entry';
|
package/esm/mirror.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirroring a harness's session into the run log.
|
|
3
|
+
*
|
|
4
|
+
* The harness owns the session: it appends entries to its own append-only
|
|
5
|
+
* transcript. Harnesses generally expose no "entry appended" event to subscribe
|
|
6
|
+
* to, so the mirror drains instead — after anything that could have appended, it
|
|
7
|
+
* takes the entries it has not seen yet and appends them to the run log
|
|
8
|
+
* verbatim. Index-based draining is sound precisely because the transcript is
|
|
9
|
+
* append-only: entries are never rewritten or removed, only branched from.
|
|
10
|
+
*
|
|
11
|
+
* A switch/fork/new-session replaces the entry list under the same source, so the
|
|
12
|
+
* read position is keyed to the session header's id and resets when that id
|
|
13
|
+
* changes; entries carried into the new session are absorbed by the store's
|
|
14
|
+
* idempotency (entry ids).
|
|
15
|
+
*
|
|
16
|
+
* Harness-neutral: the adapter owns only the subscription that calls `drain()`
|
|
17
|
+
* and the format the entries are validated as, so this is testable without a
|
|
18
|
+
* running agent.
|
|
19
|
+
*/
|
|
20
|
+
import type { RunEventRecord } from './record';
|
|
21
|
+
import type { RunLogAppendStore } from './store';
|
|
22
|
+
import type { TranscriptFormat } from './transcripts/format';
|
|
23
|
+
import { type PiSessionEntry } from './transcripts/pi-entry';
|
|
24
|
+
/** The slice of a harness session manager the mirror needs. */
|
|
25
|
+
export interface SessionEntrySource {
|
|
26
|
+
getHeader(): unknown;
|
|
27
|
+
getEntries(): readonly unknown[];
|
|
28
|
+
}
|
|
29
|
+
export interface SessionMirrorOptions {
|
|
30
|
+
runId: string;
|
|
31
|
+
store: RunLogAppendStore;
|
|
32
|
+
/**
|
|
33
|
+
* Narrow each entry before it is appended. Defaults to the pi transcript
|
|
34
|
+
* reader, which is the only entry shape the record currently types; an adapter
|
|
35
|
+
* for another transcript supplies its own.
|
|
36
|
+
*/
|
|
37
|
+
assertEntry?: (entry: unknown) => PiSessionEntry;
|
|
38
|
+
/** Transcript format the entries are encoded in, e.g. `pi`. */
|
|
39
|
+
transcriptFormat?: TranscriptFormat;
|
|
40
|
+
/** Transcript format version the entries are produced under. */
|
|
41
|
+
transcriptVersion?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class SessionMirror {
|
|
44
|
+
private readonly runId;
|
|
45
|
+
private readonly store;
|
|
46
|
+
private readonly assertEntry;
|
|
47
|
+
private readonly transcriptFormat;
|
|
48
|
+
private readonly transcriptVersion;
|
|
49
|
+
private source;
|
|
50
|
+
private sessionId;
|
|
51
|
+
private consumed;
|
|
52
|
+
private headerMirrored;
|
|
53
|
+
private tail;
|
|
54
|
+
constructor(options: SessionMirrorOptions);
|
|
55
|
+
/** Point the mirror at a session source. Safe to call on every event. */
|
|
56
|
+
bind(source: SessionEntrySource): void;
|
|
57
|
+
/**
|
|
58
|
+
* Append everything the mirror has not seen yet. Drains are serialized, so
|
|
59
|
+
* concurrent callers cannot interleave batches and break run-log ordering.
|
|
60
|
+
*/
|
|
61
|
+
drain(): Promise<RunEventRecord[]>;
|
|
62
|
+
private flushOnce;
|
|
63
|
+
}
|
package/esm/mirror.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirroring a harness's session into the run log.
|
|
3
|
+
*
|
|
4
|
+
* The harness owns the session: it appends entries to its own append-only
|
|
5
|
+
* transcript. Harnesses generally expose no "entry appended" event to subscribe
|
|
6
|
+
* to, so the mirror drains instead — after anything that could have appended, it
|
|
7
|
+
* takes the entries it has not seen yet and appends them to the run log
|
|
8
|
+
* verbatim. Index-based draining is sound precisely because the transcript is
|
|
9
|
+
* append-only: entries are never rewritten or removed, only branched from.
|
|
10
|
+
*
|
|
11
|
+
* A switch/fork/new-session replaces the entry list under the same source, so the
|
|
12
|
+
* read position is keyed to the session header's id and resets when that id
|
|
13
|
+
* changes; entries carried into the new session are absorbed by the store's
|
|
14
|
+
* idempotency (entry ids).
|
|
15
|
+
*
|
|
16
|
+
* Harness-neutral: the adapter owns only the subscription that calls `drain()`
|
|
17
|
+
* and the format the entries are validated as, so this is testable without a
|
|
18
|
+
* running agent.
|
|
19
|
+
*/
|
|
20
|
+
import { assertPiSessionEntry } from './transcripts/pi-entry';
|
|
21
|
+
export class SessionMirror {
|
|
22
|
+
runId;
|
|
23
|
+
store;
|
|
24
|
+
assertEntry;
|
|
25
|
+
transcriptFormat;
|
|
26
|
+
transcriptVersion;
|
|
27
|
+
source = null;
|
|
28
|
+
sessionId = null;
|
|
29
|
+
consumed = 0;
|
|
30
|
+
headerMirrored = false;
|
|
31
|
+
tail = Promise.resolve();
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.runId = options.runId;
|
|
34
|
+
this.store = options.store;
|
|
35
|
+
this.assertEntry = options.assertEntry ?? assertPiSessionEntry;
|
|
36
|
+
this.transcriptFormat = options.transcriptFormat;
|
|
37
|
+
this.transcriptVersion = options.transcriptVersion;
|
|
38
|
+
}
|
|
39
|
+
/** Point the mirror at a session source. Safe to call on every event. */
|
|
40
|
+
bind(source) {
|
|
41
|
+
this.source = source;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Append everything the mirror has not seen yet. Drains are serialized, so
|
|
45
|
+
* concurrent callers cannot interleave batches and break run-log ordering.
|
|
46
|
+
*/
|
|
47
|
+
drain() {
|
|
48
|
+
const run = this.tail.then(() => this.flushOnce());
|
|
49
|
+
this.tail = run.then(() => undefined, () => undefined);
|
|
50
|
+
return run;
|
|
51
|
+
}
|
|
52
|
+
async flushOnce() {
|
|
53
|
+
const source = this.source;
|
|
54
|
+
if (!source)
|
|
55
|
+
return [];
|
|
56
|
+
const header = source.getHeader();
|
|
57
|
+
const sessionId = headerSessionId(header);
|
|
58
|
+
if (sessionId !== null && sessionId !== this.sessionId) {
|
|
59
|
+
this.sessionId = sessionId;
|
|
60
|
+
this.consumed = 0;
|
|
61
|
+
this.headerMirrored = false;
|
|
62
|
+
}
|
|
63
|
+
const batch = [];
|
|
64
|
+
if (!this.headerMirrored && header !== null && header !== undefined)
|
|
65
|
+
batch.push(this.assertEntry(header));
|
|
66
|
+
const entries = source.getEntries();
|
|
67
|
+
const upto = entries.length;
|
|
68
|
+
for (let i = this.consumed; i < upto; i += 1)
|
|
69
|
+
batch.push(this.assertEntry(entries[i]));
|
|
70
|
+
if (batch.length === 0)
|
|
71
|
+
return [];
|
|
72
|
+
const written = await this.store.append(this.runId, batch, {
|
|
73
|
+
...(this.transcriptFormat === undefined ? {} : { transcriptFormat: this.transcriptFormat }),
|
|
74
|
+
...(this.transcriptVersion === undefined ? {} : { transcriptVersion: this.transcriptVersion })
|
|
75
|
+
});
|
|
76
|
+
// Advance only after a successful append: a failed drain is retried whole.
|
|
77
|
+
this.headerMirrored = true;
|
|
78
|
+
this.consumed = upto;
|
|
79
|
+
return written;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function headerSessionId(header) {
|
|
83
|
+
if (typeof header !== 'object' || header === null)
|
|
84
|
+
return null;
|
|
85
|
+
const id = header.id;
|
|
86
|
+
return typeof id === 'string' && id.length > 0 ? id : null;
|
|
87
|
+
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* `unknown` part rather than disappearing — a log written by a newer pi still
|
|
8
8
|
* renders, minus the detail this version understands.
|
|
9
9
|
*/
|
|
10
|
-
import { type PiSessionEntry } from '../pi-entry';
|
|
11
10
|
import type { RunEventRecord } from '../record';
|
|
11
|
+
import { type PiSessionEntry } from '../transcripts/pi-entry';
|
|
12
12
|
export type ToolStatus = 'requested' | 'completed' | 'failed';
|
|
13
13
|
export interface PartBase {
|
|
14
14
|
/** Sequence of the record that introduced the part — a stable React key. */
|
package/esm/projectors/parts.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `unknown` part rather than disappearing — a log written by a newer pi still
|
|
8
8
|
* renders, minus the detail this version understands.
|
|
9
9
|
*/
|
|
10
|
-
import { contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from '../pi-entry';
|
|
10
|
+
import { contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
|
|
11
11
|
/**
|
|
12
12
|
* Project records into a conversation. Pure and total: the same records always
|
|
13
13
|
* produce the same parts, whichever host wrote them.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Returns a string rather than writing a file: the projection is pure, and the
|
|
10
10
|
* node-side write lives in `@agentic-kit/run-log/file-store`.
|
|
11
11
|
*/
|
|
12
|
-
import { type PiSessionEntry } from '../pi-entry';
|
|
13
12
|
import { type RunEventRecord } from '../record';
|
|
13
|
+
import { type PiSessionEntry } from '../transcripts/pi-entry';
|
|
14
14
|
export interface SessionProjectionOptions {
|
|
15
15
|
/** Used when the log carries no session header (a run logged headerless). */
|
|
16
16
|
sessionId?: string;
|
|
@@ -22,7 +22,7 @@ export interface SessionProjection {
|
|
|
22
22
|
jsonl: string;
|
|
23
23
|
/** Entries in file order, header first. */
|
|
24
24
|
entries: PiSessionEntry[];
|
|
25
|
-
|
|
25
|
+
transcriptVersion: number;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Project records into a pi session file. Throws when the records cannot form a
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Returns a string rather than writing a file: the projection is pure, and the
|
|
10
10
|
* node-side write lives in `@agentic-kit/run-log/file-store`.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { assertOrdered } from '../record';
|
|
13
|
+
import { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from '../transcripts/format';
|
|
14
|
+
import { isPiSessionHeader } from '../transcripts/pi-entry';
|
|
14
15
|
/**
|
|
15
16
|
* Project records into a pi session file. Throws when the records cannot form a
|
|
16
17
|
* loadable session — an unresumable session must fail at projection time, not
|
|
@@ -18,11 +19,12 @@ import { assertOrdered, SUPPORTED_PI_SESSION_VERSION } from '../record';
|
|
|
18
19
|
*/
|
|
19
20
|
export function projectSession(records, options = {}) {
|
|
20
21
|
assertOrdered(records);
|
|
21
|
-
|
|
22
|
+
assertTranscriptFormat(records, PI_TRANSCRIPT_FORMAT);
|
|
23
|
+
const versions = new Set(records.map((record) => record.transcriptVersion));
|
|
22
24
|
if (versions.size > 1) {
|
|
23
25
|
throw new Error(`run log mixes pi session versions (${Array.from(versions).sort().join(', ')}); migrate the older entries before projecting a session`);
|
|
24
26
|
}
|
|
25
|
-
const
|
|
27
|
+
const transcriptVersion = records[0]?.transcriptVersion ?? SUPPORTED_PI_TRANSCRIPT_VERSION;
|
|
26
28
|
const entries = records.map((record) => record.entry);
|
|
27
29
|
const headerIndex = entries.findIndex(isPiSessionHeader);
|
|
28
30
|
if (headerIndex > 0) {
|
|
@@ -33,7 +35,7 @@ export function projectSession(records, options = {}) {
|
|
|
33
35
|
? entries[0]
|
|
34
36
|
: {
|
|
35
37
|
type: 'session',
|
|
36
|
-
version:
|
|
38
|
+
version: transcriptVersion,
|
|
37
39
|
id: options.sessionId ?? records[0]?.runId ?? 'run-log',
|
|
38
40
|
timestamp: options.timestamp ?? records[0]?.recordedAt ?? new Date().toISOString(),
|
|
39
41
|
...(options.cwd ? { cwd: options.cwd } : {})
|
|
@@ -41,7 +43,7 @@ export function projectSession(records, options = {}) {
|
|
|
41
43
|
const ordered = [header, ...body];
|
|
42
44
|
return {
|
|
43
45
|
entries: ordered,
|
|
44
|
-
|
|
46
|
+
transcriptVersion,
|
|
45
47
|
jsonl: ordered.map((entry) => JSON.stringify(entry)).join('\n') + '\n'
|
|
46
48
|
};
|
|
47
49
|
}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* `open` with no duration rather than being closed at the last record: a run
|
|
16
16
|
* still in flight must not look like one that finished.
|
|
17
17
|
*/
|
|
18
|
-
import { type PiUsage } from '../pi-entry';
|
|
19
18
|
import type { RunEventRecord } from '../record';
|
|
19
|
+
import { type PiUsage } from '../transcripts/pi-entry';
|
|
20
20
|
export type SpanKind = 'model' | 'tool' | 'approval';
|
|
21
21
|
export type SpanStatus = 'open' | 'ok' | 'error' | 'denied';
|
|
22
22
|
/**
|
package/esm/projectors/spans.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `open` with no duration rather than being closed at the last record: a run
|
|
16
16
|
* still in flight must not look like one that finished.
|
|
17
17
|
*/
|
|
18
|
-
import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../pi-entry';
|
|
18
|
+
import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
|
|
19
19
|
import { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, assertGateDecisionDetails, GATE_DECISION_TYPE } from './tool-state';
|
|
20
20
|
const ms = (from, to) => {
|
|
21
21
|
const start = Date.parse(from);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* having been present when it was raised, and both placements then behave the
|
|
9
9
|
* same — the cloud already treats the conversation as the approval UI.
|
|
10
10
|
*/
|
|
11
|
-
import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../pi-entry';
|
|
11
|
+
import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
|
|
12
12
|
/** `customType` of an approval request written by the gate extension. */
|
|
13
13
|
export const APPROVAL_REQUEST_TYPE = 'constructive.approval.request';
|
|
14
14
|
/** `customType` of the human's answer to a request. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This projection describes what a run *observed*. It is reconciliation input,
|
|
11
11
|
* never the billing authority — a gateway-observed record is (see
|
|
12
|
-
* `@agentic-kit/
|
|
12
|
+
* `@agentic-kit/metering`).
|
|
13
13
|
*/
|
|
14
14
|
import type { RunEventRecord } from '../record';
|
|
15
15
|
export interface UsageTotals {
|
package/esm/projectors/usage.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This projection describes what a run *observed*. It is reconciliation input,
|
|
11
11
|
* never the billing authority — a gateway-observed record is (see
|
|
12
|
-
* `@agentic-kit/
|
|
12
|
+
* `@agentic-kit/metering`).
|
|
13
13
|
*/
|
|
14
|
-
import { isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isToolResultMessage } from '../pi-entry';
|
|
14
|
+
import { isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isToolResultMessage } from '../transcripts/pi-entry';
|
|
15
15
|
const empty = () => ({
|
|
16
16
|
input: 0,
|
|
17
17
|
output: 0,
|
package/esm/record.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The run log record:
|
|
2
|
+
* The run log record: five platform-owned fields around a verbatim harness entry.
|
|
3
3
|
*
|
|
4
|
-
* The wrapper exists to give an entry identity (which run)
|
|
5
|
-
* position) across hosts — nothing else. It
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* reader can
|
|
4
|
+
* The wrapper exists to give an entry identity (which run), order (which
|
|
5
|
+
* position) and readability (which transcript) across hosts — nothing else. It
|
|
6
|
+
* deliberately does not re-encode a harness's semantics, because a harness
|
|
7
|
+
* already versions and migrates its own session format; `transcriptFormat` says
|
|
8
|
+
* which reader can parse the entry, and `transcriptVersion` says which version
|
|
9
|
+
* that reader must migrate from.
|
|
9
10
|
*/
|
|
10
|
-
import { type
|
|
11
|
-
|
|
11
|
+
import { type TranscriptFormat } from './transcripts/format';
|
|
12
|
+
import { type PiSessionEntry } from './transcripts/pi-entry';
|
|
13
|
+
/** Version of the wrapper itself — bumped only if these fields change. */
|
|
12
14
|
export declare const RUN_LOG_WRAPPER_VERSION = 1;
|
|
13
|
-
/** The pi session format version this package projects without migration. */
|
|
14
|
-
export declare const SUPPORTED_PI_SESSION_VERSION = 3;
|
|
15
15
|
export interface RunEventRecord {
|
|
16
16
|
/** The run this entry belongs to. */
|
|
17
17
|
runId: string;
|
|
@@ -19,9 +19,11 @@ export interface RunEventRecord {
|
|
|
19
19
|
seq: number;
|
|
20
20
|
/** When the platform durably recorded the entry (ISO 8601). */
|
|
21
21
|
recordedAt: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
/**
|
|
22
|
+
/** The transcript the entry is encoded in, e.g. `pi`. */
|
|
23
|
+
transcriptFormat: TranscriptFormat;
|
|
24
|
+
/** That transcript's format version at write time. */
|
|
25
|
+
transcriptVersion: number;
|
|
26
|
+
/** The harness's session entry, byte-for-byte as it was produced. */
|
|
25
27
|
entry: PiSessionEntry;
|
|
26
28
|
}
|
|
27
29
|
export interface WrapEntryOptions {
|
|
@@ -29,7 +31,8 @@ export interface WrapEntryOptions {
|
|
|
29
31
|
seq: number;
|
|
30
32
|
entry: PiSessionEntry;
|
|
31
33
|
recordedAt?: string;
|
|
32
|
-
|
|
34
|
+
transcriptFormat?: TranscriptFormat;
|
|
35
|
+
transcriptVersion?: number;
|
|
33
36
|
}
|
|
34
37
|
export declare function wrapEntry(options: WrapEntryOptions): RunEventRecord;
|
|
35
38
|
/**
|
package/esm/record.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The run log record:
|
|
2
|
+
* The run log record: five platform-owned fields around a verbatim harness entry.
|
|
3
3
|
*
|
|
4
|
-
* The wrapper exists to give an entry identity (which run)
|
|
5
|
-
* position) across hosts — nothing else. It
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* reader can
|
|
4
|
+
* The wrapper exists to give an entry identity (which run), order (which
|
|
5
|
+
* position) and readability (which transcript) across hosts — nothing else. It
|
|
6
|
+
* deliberately does not re-encode a harness's semantics, because a harness
|
|
7
|
+
* already versions and migrates its own session format; `transcriptFormat` says
|
|
8
|
+
* which reader can parse the entry, and `transcriptVersion` says which version
|
|
9
|
+
* that reader must migrate from.
|
|
9
10
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
11
|
+
import { PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from './transcripts/format';
|
|
12
|
+
import { assertPiSessionEntry } from './transcripts/pi-entry';
|
|
13
|
+
/** Version of the wrapper itself — bumped only if these fields change. */
|
|
12
14
|
export const RUN_LOG_WRAPPER_VERSION = 1;
|
|
13
|
-
/** The pi session format version this package projects without migration. */
|
|
14
|
-
export const SUPPORTED_PI_SESSION_VERSION = 3;
|
|
15
15
|
export function wrapEntry(options) {
|
|
16
16
|
if (!options.runId)
|
|
17
17
|
throw new TypeError('a run log record needs a runId');
|
|
@@ -22,7 +22,8 @@ export function wrapEntry(options) {
|
|
|
22
22
|
runId: options.runId,
|
|
23
23
|
seq: options.seq,
|
|
24
24
|
recordedAt: options.recordedAt ?? new Date().toISOString(),
|
|
25
|
-
|
|
25
|
+
transcriptFormat: options.transcriptFormat ?? PI_TRANSCRIPT_FORMAT,
|
|
26
|
+
transcriptVersion: options.transcriptVersion ?? SUPPORTED_PI_TRANSCRIPT_VERSION,
|
|
26
27
|
entry: assertPiSessionEntry(options.entry)
|
|
27
28
|
};
|
|
28
29
|
}
|
|
@@ -45,14 +46,18 @@ export function assertRunEventRecord(value) {
|
|
|
45
46
|
if (typeof record.recordedAt !== 'string') {
|
|
46
47
|
throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry recordedAt`);
|
|
47
48
|
}
|
|
48
|
-
if (
|
|
49
|
-
throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry
|
|
49
|
+
if (typeof record.transcriptFormat !== 'string' || record.transcriptFormat.length === 0) {
|
|
50
|
+
throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry a non-empty transcriptFormat`);
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isInteger(record.transcriptVersion)) {
|
|
53
|
+
throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry an integer transcriptVersion`);
|
|
50
54
|
}
|
|
51
55
|
return {
|
|
52
56
|
runId: record.runId,
|
|
53
57
|
seq: record.seq,
|
|
54
58
|
recordedAt: record.recordedAt,
|
|
55
|
-
|
|
59
|
+
transcriptFormat: record.transcriptFormat,
|
|
60
|
+
transcriptVersion: record.transcriptVersion,
|
|
56
61
|
entry: assertPiSessionEntry(record.entry)
|
|
57
62
|
};
|
|
58
63
|
}
|
package/esm/store.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* with their storage — Postgres in constructive-db, JSONL in `./file-store`,
|
|
6
6
|
* memory here for tests and for a run that has not been persisted yet.
|
|
7
7
|
*/
|
|
8
|
-
import type { PiSessionEntry } from './pi-entry';
|
|
9
8
|
import { type RunEventRecord } from './record';
|
|
9
|
+
import type { TranscriptFormat } from './transcripts/format';
|
|
10
|
+
import type { PiSessionEntry } from './transcripts/pi-entry';
|
|
10
11
|
/** Read position: `afterSeq` is exclusive, so `0` means "from the start". */
|
|
11
12
|
export interface RunLogCursor {
|
|
12
13
|
afterSeq: number;
|
|
@@ -18,8 +19,10 @@ export interface RunLogPage {
|
|
|
18
19
|
cursor: RunLogCursor;
|
|
19
20
|
}
|
|
20
21
|
export interface AppendOptions {
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/** Transcript the entries are encoded in. Defaults to pi's. */
|
|
23
|
+
transcriptFormat?: TranscriptFormat;
|
|
24
|
+
/** That transcript's format version the entries were produced under. */
|
|
25
|
+
transcriptVersion?: number;
|
|
23
26
|
recordedAt?: string;
|
|
24
27
|
}
|
|
25
28
|
export interface RunLogAppendStore {
|
package/esm/store.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* with their storage — Postgres in constructive-db, JSONL in `./file-store`,
|
|
6
6
|
* memory here for tests and for a run that has not been persisted yet.
|
|
7
7
|
*/
|
|
8
|
-
import { assertOrdered, idempotencyKey,
|
|
8
|
+
import { assertOrdered, idempotencyKey, wrapEntry } from './record';
|
|
9
9
|
export const START = { afterSeq: 0 };
|
|
10
10
|
export const cursorAfter = (records, from = START) => records.length === 0 ? from : { afterSeq: records[records.length - 1].seq };
|
|
11
11
|
/** In-memory store: the reference implementation and the test double. */
|
|
@@ -25,7 +25,8 @@ export class MemoryRunLogStore {
|
|
|
25
25
|
seq: records.length + written.length + 1,
|
|
26
26
|
entry,
|
|
27
27
|
...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
|
|
28
|
-
|
|
28
|
+
...(options.transcriptFormat === undefined ? {} : { transcriptFormat: options.transcriptFormat }),
|
|
29
|
+
...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
|
|
29
30
|
});
|
|
30
31
|
written.push(record);
|
|
31
32
|
seen.add(key);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which transcript a record's entry is encoded in.
|
|
3
|
+
*
|
|
4
|
+
* The log stores a harness's own entries verbatim, so a reader cannot know how
|
|
5
|
+
* to project a record without being told what it is looking at. The format is
|
|
6
|
+
* therefore carried *per record*, not per run: a run resumed under a different
|
|
7
|
+
* harness, or migrated between them, stays one ordered log.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately a string rather than a closed union — a format is a value that
|
|
10
|
+
* ships with an adapter, so adding one must not require a release of this
|
|
11
|
+
* package. Known formats are declared as constants here.
|
|
12
|
+
*/
|
|
13
|
+
export type TranscriptFormat = string;
|
|
14
|
+
/** pi's session-file entries (`@earendil-works/pi-coding-agent`). */
|
|
15
|
+
export declare const PI_TRANSCRIPT_FORMAT = "pi";
|
|
16
|
+
/** The pi session format version this package projects without migration. */
|
|
17
|
+
export declare const SUPPORTED_PI_TRANSCRIPT_VERSION = 3;
|
|
18
|
+
/**
|
|
19
|
+
* Narrow a record's format before projecting it. A projector reads one format;
|
|
20
|
+
* handing it another must fail loudly rather than render an empty conversation
|
|
21
|
+
* out of entries it did not understand.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertTranscriptFormat(records: readonly {
|
|
24
|
+
transcriptFormat: TranscriptFormat;
|
|
25
|
+
}[], expected: TranscriptFormat): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which transcript a record's entry is encoded in.
|
|
3
|
+
*
|
|
4
|
+
* The log stores a harness's own entries verbatim, so a reader cannot know how
|
|
5
|
+
* to project a record without being told what it is looking at. The format is
|
|
6
|
+
* therefore carried *per record*, not per run: a run resumed under a different
|
|
7
|
+
* harness, or migrated between them, stays one ordered log.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately a string rather than a closed union — a format is a value that
|
|
10
|
+
* ships with an adapter, so adding one must not require a release of this
|
|
11
|
+
* package. Known formats are declared as constants here.
|
|
12
|
+
*/
|
|
13
|
+
/** pi's session-file entries (`@earendil-works/pi-coding-agent`). */
|
|
14
|
+
export const PI_TRANSCRIPT_FORMAT = 'pi';
|
|
15
|
+
/** The pi session format version this package projects without migration. */
|
|
16
|
+
export const SUPPORTED_PI_TRANSCRIPT_VERSION = 3;
|
|
17
|
+
/**
|
|
18
|
+
* Narrow a record's format before projecting it. A projector reads one format;
|
|
19
|
+
* handing it another must fail loudly rather than render an empty conversation
|
|
20
|
+
* out of entries it did not understand.
|
|
21
|
+
*/
|
|
22
|
+
export function assertTranscriptFormat(records, expected) {
|
|
23
|
+
const formats = new Set(records.map((record) => record.transcriptFormat));
|
|
24
|
+
formats.delete(expected);
|
|
25
|
+
if (formats.size === 0)
|
|
26
|
+
return;
|
|
27
|
+
throw new Error(`run log carries transcript format(s) ${Array.from(formats).sort().join(', ')}; ` +
|
|
28
|
+
`this projector reads "${expected}" entries only`);
|
|
29
|
+
}
|
package/file-store.d.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* dropped. This is the store a local run uses before (or without) a database,
|
|
11
11
|
* and the one tests use when they want the log to survive a process restart.
|
|
12
12
|
*/
|
|
13
|
-
import type { PiSessionEntry } from './pi-entry';
|
|
14
13
|
import { type SessionProjectionOptions } from './projectors/session';
|
|
15
14
|
import { type RunEventRecord } from './record';
|
|
16
15
|
import { type AppendOptions, type RunLogCursor, type RunLogPage, type RunLogStore } from './store';
|
|
16
|
+
import type { PiSessionEntry } from './transcripts/pi-entry';
|
|
17
17
|
export interface FileRunLogStoreOptions {
|
|
18
18
|
/** Absolute path of the log file. Parent directories are created. */
|
|
19
19
|
path: string;
|