@agentic-kit/run-log 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 +169 -0
- package/esm/file-store.d.ts +33 -0
- package/esm/file-store.js +85 -0
- package/esm/follow.d.ts +36 -0
- package/esm/follow.js +65 -0
- package/esm/index.d.ts +16 -0
- package/esm/index.js +16 -0
- package/esm/pi-entry.d.ts +157 -0
- package/esm/pi-entry.js +61 -0
- package/esm/projectors/parts.d.ts +75 -0
- package/esm/projectors/parts.js +135 -0
- package/esm/projectors/session.d.ts +34 -0
- package/esm/projectors/session.js +64 -0
- package/esm/projectors/tool-state.d.ts +77 -0
- package/esm/projectors/tool-state.js +127 -0
- package/esm/projectors/usage.d.ts +35 -0
- package/esm/projectors/usage.js +76 -0
- package/esm/record.d.ts +48 -0
- package/esm/record.js +83 -0
- package/esm/store.d.ts +46 -0
- package/esm/store.js +51 -0
- package/file-store.d.ts +33 -0
- package/file-store.js +90 -0
- package/follow.d.ts +36 -0
- package/follow.js +69 -0
- package/index.d.ts +16 -0
- package/index.js +49 -0
- package/package.json +39 -0
- package/pi-entry.d.ts +157 -0
- package/pi-entry.js +73 -0
- package/projectors/parts.d.ts +75 -0
- package/projectors/parts.js +138 -0
- package/projectors/session.d.ts +34 -0
- package/projectors/session.js +68 -0
- package/projectors/tool-state.d.ts +77 -0
- package/projectors/tool-state.js +133 -0
- package/projectors/usage.d.ts +35 -0
- package/projectors/usage.js +81 -0
- package/record.d.ts +48 -0
- package/record.js +90 -0
- package/store.d.ts +46 -0
- package/store.js +56 -0
package/pi-entry.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural subset of pi's session entries this package reads.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately structural, not imported from pi: a run log stores pi entries
|
|
5
|
+
* *verbatim*, so the types here describe what the projectors read rather than
|
|
6
|
+
* re-declaring pi's format. Every interface keeps an index signature so an
|
|
7
|
+
* entry produced by a newer pi still parses, and unknown `type` values are
|
|
8
|
+
* carried through untouched instead of being dropped.
|
|
9
|
+
*
|
|
10
|
+
* Reference: `@earendil-works/pi-coding-agent` `docs/session-format.md`
|
|
11
|
+
* (session file version 3).
|
|
12
|
+
*/
|
|
13
|
+
export interface PiUsageCost {
|
|
14
|
+
input?: number;
|
|
15
|
+
output?: number;
|
|
16
|
+
cacheRead?: number;
|
|
17
|
+
cacheWrite?: number;
|
|
18
|
+
total?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface PiUsage {
|
|
21
|
+
input?: number;
|
|
22
|
+
output?: number;
|
|
23
|
+
cacheRead?: number;
|
|
24
|
+
cacheWrite?: number;
|
|
25
|
+
totalTokens?: number;
|
|
26
|
+
cost?: PiUsageCost;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface PiTextContent {
|
|
30
|
+
type: 'text';
|
|
31
|
+
text: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PiThinkingContent {
|
|
34
|
+
type: 'thinking';
|
|
35
|
+
thinking: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PiImageContent {
|
|
38
|
+
type: 'image';
|
|
39
|
+
data: string;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PiToolCallContent {
|
|
43
|
+
type: 'toolCall';
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
arguments?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export type PiContent = PiTextContent | PiThinkingContent | PiImageContent | PiToolCallContent;
|
|
49
|
+
export interface PiUserMessage {
|
|
50
|
+
role: 'user';
|
|
51
|
+
content: string | PiContent[];
|
|
52
|
+
timestamp?: number;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
export interface PiAssistantMessage {
|
|
56
|
+
role: 'assistant';
|
|
57
|
+
content: PiContent[];
|
|
58
|
+
api?: string;
|
|
59
|
+
provider?: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
usage?: PiUsage;
|
|
62
|
+
stopReason?: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted';
|
|
63
|
+
errorMessage?: string;
|
|
64
|
+
timestamp?: number;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
export interface PiToolResultMessage {
|
|
68
|
+
role: 'toolResult';
|
|
69
|
+
toolCallId: string;
|
|
70
|
+
toolName: string;
|
|
71
|
+
content: PiContent[];
|
|
72
|
+
details?: unknown;
|
|
73
|
+
usage?: PiUsage;
|
|
74
|
+
isError?: boolean;
|
|
75
|
+
timestamp?: number;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
export interface PiBashExecutionMessage {
|
|
79
|
+
role: 'bashExecution';
|
|
80
|
+
command: string;
|
|
81
|
+
output: string;
|
|
82
|
+
exitCode?: number;
|
|
83
|
+
cancelled?: boolean;
|
|
84
|
+
truncated?: boolean;
|
|
85
|
+
timestamp?: number;
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
export interface PiCustomMessage {
|
|
89
|
+
role: 'custom';
|
|
90
|
+
customType: string;
|
|
91
|
+
content: string | PiContent[];
|
|
92
|
+
display?: boolean;
|
|
93
|
+
details?: unknown;
|
|
94
|
+
timestamp?: number;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
export interface PiSummaryMessage {
|
|
98
|
+
role: 'branchSummary' | 'compactionSummary';
|
|
99
|
+
summary: string;
|
|
100
|
+
timestamp?: number;
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
export type PiMessage = PiUserMessage | PiAssistantMessage | PiToolResultMessage | PiBashExecutionMessage | PiCustomMessage | PiSummaryMessage;
|
|
104
|
+
export interface PiSessionHeader {
|
|
105
|
+
type: 'session';
|
|
106
|
+
version: number;
|
|
107
|
+
id: string;
|
|
108
|
+
timestamp: string;
|
|
109
|
+
cwd?: string;
|
|
110
|
+
parentSession?: string;
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
113
|
+
export interface PiEntryBase {
|
|
114
|
+
id: string;
|
|
115
|
+
parentId: string | null;
|
|
116
|
+
timestamp: string;
|
|
117
|
+
}
|
|
118
|
+
export interface PiMessageEntry extends PiEntryBase {
|
|
119
|
+
type: 'message';
|
|
120
|
+
message: PiMessage;
|
|
121
|
+
[key: string]: unknown;
|
|
122
|
+
}
|
|
123
|
+
export interface PiCompactionEntry extends PiEntryBase {
|
|
124
|
+
type: 'compaction';
|
|
125
|
+
summary: string;
|
|
126
|
+
tokensBefore?: number;
|
|
127
|
+
usage?: PiUsage;
|
|
128
|
+
[key: string]: unknown;
|
|
129
|
+
}
|
|
130
|
+
export interface PiBranchSummaryEntry extends PiEntryBase {
|
|
131
|
+
type: 'branch_summary';
|
|
132
|
+
summary: string;
|
|
133
|
+
fromId: string;
|
|
134
|
+
usage?: PiUsage;
|
|
135
|
+
[key: string]: unknown;
|
|
136
|
+
}
|
|
137
|
+
export interface PiOtherEntry extends PiEntryBase {
|
|
138
|
+
type: string;
|
|
139
|
+
[key: string]: unknown;
|
|
140
|
+
}
|
|
141
|
+
export type PiSessionEntry = PiSessionHeader | PiMessageEntry | PiCompactionEntry | PiBranchSummaryEntry | PiOtherEntry;
|
|
142
|
+
export declare const isPiSessionHeader: (entry: PiSessionEntry) => entry is PiSessionHeader;
|
|
143
|
+
export declare const isPiMessageEntry: (entry: PiSessionEntry) => entry is PiMessageEntry;
|
|
144
|
+
export declare const isPiCompactionEntry: (entry: PiSessionEntry) => entry is PiCompactionEntry;
|
|
145
|
+
export declare const isPiBranchSummaryEntry: (entry: PiSessionEntry) => entry is PiBranchSummaryEntry;
|
|
146
|
+
export declare const isAssistantMessage: (message: PiMessage) => message is PiAssistantMessage;
|
|
147
|
+
export declare const isToolResultMessage: (message: PiMessage) => message is PiToolResultMessage;
|
|
148
|
+
/**
|
|
149
|
+
* Narrow an untrusted value (a database JSONB column, an HTTP body) to a pi
|
|
150
|
+
* entry. Throws rather than returning null: a log row that cannot be read is a
|
|
151
|
+
* corrupted log, never an empty one.
|
|
152
|
+
*/
|
|
153
|
+
export declare function assertPiSessionEntry(value: unknown): PiSessionEntry;
|
|
154
|
+
/** The text of a message's content, whether it is a string or a block array. */
|
|
155
|
+
export declare function contentText(content: string | PiContent[] | undefined): string;
|
|
156
|
+
/** Tool calls requested by an assistant message, in order. */
|
|
157
|
+
export declare function toolCalls(message: PiAssistantMessage): PiToolCallContent[];
|
package/pi-entry.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The structural subset of pi's session entries this package reads.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately structural, not imported from pi: a run log stores pi entries
|
|
6
|
+
* *verbatim*, so the types here describe what the projectors read rather than
|
|
7
|
+
* re-declaring pi's format. Every interface keeps an index signature so an
|
|
8
|
+
* entry produced by a newer pi still parses, and unknown `type` values are
|
|
9
|
+
* carried through untouched instead of being dropped.
|
|
10
|
+
*
|
|
11
|
+
* Reference: `@earendil-works/pi-coding-agent` `docs/session-format.md`
|
|
12
|
+
* (session file version 3).
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.isToolResultMessage = exports.isAssistantMessage = exports.isPiBranchSummaryEntry = exports.isPiCompactionEntry = exports.isPiMessageEntry = exports.isPiSessionHeader = void 0;
|
|
16
|
+
exports.assertPiSessionEntry = assertPiSessionEntry;
|
|
17
|
+
exports.contentText = contentText;
|
|
18
|
+
exports.toolCalls = toolCalls;
|
|
19
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
const isPiSessionHeader = (entry) => entry.type === 'session';
|
|
21
|
+
exports.isPiSessionHeader = isPiSessionHeader;
|
|
22
|
+
const isPiMessageEntry = (entry) => entry.type === 'message' && isRecord(entry.message);
|
|
23
|
+
exports.isPiMessageEntry = isPiMessageEntry;
|
|
24
|
+
const isPiCompactionEntry = (entry) => entry.type === 'compaction';
|
|
25
|
+
exports.isPiCompactionEntry = isPiCompactionEntry;
|
|
26
|
+
const isPiBranchSummaryEntry = (entry) => entry.type === 'branch_summary';
|
|
27
|
+
exports.isPiBranchSummaryEntry = isPiBranchSummaryEntry;
|
|
28
|
+
const isAssistantMessage = (message) => message.role === 'assistant';
|
|
29
|
+
exports.isAssistantMessage = isAssistantMessage;
|
|
30
|
+
const isToolResultMessage = (message) => message.role === 'toolResult';
|
|
31
|
+
exports.isToolResultMessage = isToolResultMessage;
|
|
32
|
+
/**
|
|
33
|
+
* Narrow an untrusted value (a database JSONB column, an HTTP body) to a pi
|
|
34
|
+
* entry. Throws rather than returning null: a log row that cannot be read is a
|
|
35
|
+
* corrupted log, never an empty one.
|
|
36
|
+
*/
|
|
37
|
+
function assertPiSessionEntry(value) {
|
|
38
|
+
if (!isRecord(value)) {
|
|
39
|
+
throw new TypeError(`pi session entry must be an object, received ${typeof value}`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof value.type !== 'string' || value.type.length === 0) {
|
|
42
|
+
throw new TypeError('pi session entry must carry a non-empty string `type`');
|
|
43
|
+
}
|
|
44
|
+
if (value.type !== 'session') {
|
|
45
|
+
if (typeof value.id !== 'string' || value.id.length === 0) {
|
|
46
|
+
throw new TypeError(`pi ${value.type} entry must carry a non-empty string \`id\``);
|
|
47
|
+
}
|
|
48
|
+
if (typeof value.timestamp !== 'string') {
|
|
49
|
+
throw new TypeError(`pi ${value.type} entry must carry an ISO string \`timestamp\``);
|
|
50
|
+
}
|
|
51
|
+
if (!('parentId' in value)) {
|
|
52
|
+
throw new TypeError(`pi ${value.type} entry must carry \`parentId\` (null for the first entry)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
/** The text of a message's content, whether it is a string or a block array. */
|
|
58
|
+
function contentText(content) {
|
|
59
|
+
if (typeof content === 'string')
|
|
60
|
+
return content;
|
|
61
|
+
if (!Array.isArray(content))
|
|
62
|
+
return '';
|
|
63
|
+
return content
|
|
64
|
+
.filter((block) => isRecord(block) && block.type === 'text')
|
|
65
|
+
.map((block) => block.text)
|
|
66
|
+
.join('');
|
|
67
|
+
}
|
|
68
|
+
/** Tool calls requested by an assistant message, in order. */
|
|
69
|
+
function toolCalls(message) {
|
|
70
|
+
if (!Array.isArray(message.content))
|
|
71
|
+
return [];
|
|
72
|
+
return message.content.filter((block) => isRecord(block) && block.type === 'toolCall');
|
|
73
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderable projection: run log records → an ordered list of parts a UI draws.
|
|
3
|
+
*
|
|
4
|
+
* This is the projection that replaces per-host transcript encodings. A tool
|
|
5
|
+
* call and its later result collapse into one part, so a renderer never has to
|
|
6
|
+
* correlate two messages itself, and an unknown entry type becomes an
|
|
7
|
+
* `unknown` part rather than disappearing — a log written by a newer pi still
|
|
8
|
+
* renders, minus the detail this version understands.
|
|
9
|
+
*/
|
|
10
|
+
import { type PiSessionEntry } from '../pi-entry';
|
|
11
|
+
import type { RunEventRecord } from '../record';
|
|
12
|
+
export type ToolStatus = 'requested' | 'completed' | 'failed';
|
|
13
|
+
export interface PartBase {
|
|
14
|
+
/** Sequence of the record that introduced the part — a stable React key. */
|
|
15
|
+
seq: number;
|
|
16
|
+
entryId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TextPart extends PartBase {
|
|
19
|
+
kind: 'text';
|
|
20
|
+
role: 'user' | 'assistant';
|
|
21
|
+
text: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
provider?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ThinkingPart extends PartBase {
|
|
26
|
+
kind: 'thinking';
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ToolPart extends PartBase {
|
|
30
|
+
kind: 'tool';
|
|
31
|
+
toolCallId: string;
|
|
32
|
+
name: string;
|
|
33
|
+
arguments: Record<string, unknown>;
|
|
34
|
+
status: ToolStatus;
|
|
35
|
+
/** Text of the tool result, once one has been logged. */
|
|
36
|
+
output?: string;
|
|
37
|
+
details?: unknown;
|
|
38
|
+
/** Sequence of the record that settled the call. */
|
|
39
|
+
settledSeq?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface BashPart extends PartBase {
|
|
42
|
+
kind: 'bash';
|
|
43
|
+
command: string;
|
|
44
|
+
output: string;
|
|
45
|
+
exitCode?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface CustomPart extends PartBase {
|
|
48
|
+
kind: 'custom';
|
|
49
|
+
customType: string;
|
|
50
|
+
text: string;
|
|
51
|
+
display: boolean;
|
|
52
|
+
details?: unknown;
|
|
53
|
+
}
|
|
54
|
+
export interface SummaryPart extends PartBase {
|
|
55
|
+
kind: 'summary';
|
|
56
|
+
reason: 'compaction' | 'branch';
|
|
57
|
+
summary: string;
|
|
58
|
+
}
|
|
59
|
+
export interface UnknownPart extends PartBase {
|
|
60
|
+
kind: 'unknown';
|
|
61
|
+
entryType: string;
|
|
62
|
+
entry: PiSessionEntry;
|
|
63
|
+
}
|
|
64
|
+
export type ConversationPart = TextPart | ThinkingPart | ToolPart | BashPart | CustomPart | SummaryPart | UnknownPart;
|
|
65
|
+
export interface Conversation {
|
|
66
|
+
parts: ConversationPart[];
|
|
67
|
+
/** From the session header, when the log carries one. */
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
cwd?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Project records into a conversation. Pure and total: the same records always
|
|
73
|
+
* produce the same parts, whichever host wrote them.
|
|
74
|
+
*/
|
|
75
|
+
export declare function projectParts(records: readonly RunEventRecord[]): Conversation;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Renderable projection: run log records → an ordered list of parts a UI draws.
|
|
4
|
+
*
|
|
5
|
+
* This is the projection that replaces per-host transcript encodings. A tool
|
|
6
|
+
* call and its later result collapse into one part, so a renderer never has to
|
|
7
|
+
* correlate two messages itself, and an unknown entry type becomes an
|
|
8
|
+
* `unknown` part rather than disappearing — a log written by a newer pi still
|
|
9
|
+
* renders, minus the detail this version understands.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.projectParts = projectParts;
|
|
13
|
+
const pi_entry_1 = require("../pi-entry");
|
|
14
|
+
/**
|
|
15
|
+
* Project records into a conversation. Pure and total: the same records always
|
|
16
|
+
* produce the same parts, whichever host wrote them.
|
|
17
|
+
*/
|
|
18
|
+
function projectParts(records) {
|
|
19
|
+
const parts = [];
|
|
20
|
+
const toolsByCallId = new Map();
|
|
21
|
+
let sessionId;
|
|
22
|
+
let cwd;
|
|
23
|
+
for (const record of records) {
|
|
24
|
+
const { entry, seq } = record;
|
|
25
|
+
if ((0, pi_entry_1.isPiSessionHeader)(entry)) {
|
|
26
|
+
sessionId = entry.id;
|
|
27
|
+
if (typeof entry.cwd === 'string')
|
|
28
|
+
cwd = entry.cwd;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if ((0, pi_entry_1.isPiCompactionEntry)(entry)) {
|
|
32
|
+
parts.push({ kind: 'summary', reason: 'compaction', summary: entry.summary, seq, entryId: entry.id });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if ((0, pi_entry_1.isPiBranchSummaryEntry)(entry)) {
|
|
36
|
+
parts.push({ kind: 'summary', reason: 'branch', summary: entry.summary, seq, entryId: entry.id });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!(0, pi_entry_1.isPiMessageEntry)(entry)) {
|
|
40
|
+
parts.push({ kind: 'unknown', entryType: entry.type, entry, seq, entryId: entry.id });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const message = entry.message;
|
|
44
|
+
const base = { seq, entryId: entry.id };
|
|
45
|
+
if (message.role === 'user') {
|
|
46
|
+
parts.push({ kind: 'text', role: 'user', text: (0, pi_entry_1.contentText)(message.content), ...base });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if ((0, pi_entry_1.isAssistantMessage)(message)) {
|
|
50
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
51
|
+
if (block.type === 'text' && block.text.length > 0) {
|
|
52
|
+
parts.push({
|
|
53
|
+
kind: 'text',
|
|
54
|
+
role: 'assistant',
|
|
55
|
+
text: block.text,
|
|
56
|
+
...(message.model ? { model: message.model } : {}),
|
|
57
|
+
...(message.provider ? { provider: message.provider } : {}),
|
|
58
|
+
...base
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (block.type === 'thinking') {
|
|
62
|
+
parts.push({ kind: 'thinking', text: block.thinking, ...base });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const call of (0, pi_entry_1.toolCalls)(message)) {
|
|
66
|
+
const part = {
|
|
67
|
+
kind: 'tool',
|
|
68
|
+
toolCallId: call.id,
|
|
69
|
+
name: call.name,
|
|
70
|
+
arguments: call.arguments ?? {},
|
|
71
|
+
status: 'requested',
|
|
72
|
+
...base
|
|
73
|
+
};
|
|
74
|
+
toolsByCallId.set(call.id, part);
|
|
75
|
+
parts.push(part);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if ((0, pi_entry_1.isToolResultMessage)(message)) {
|
|
80
|
+
const existing = toolsByCallId.get(message.toolCallId);
|
|
81
|
+
const output = (0, pi_entry_1.contentText)(message.content);
|
|
82
|
+
const status = message.isError ? 'failed' : 'completed';
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.status = status;
|
|
85
|
+
existing.output = output;
|
|
86
|
+
existing.settledSeq = seq;
|
|
87
|
+
if (message.details !== undefined)
|
|
88
|
+
existing.details = message.details;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// A result whose call is not in this window (paged read, forked branch).
|
|
92
|
+
parts.push({
|
|
93
|
+
kind: 'tool',
|
|
94
|
+
toolCallId: message.toolCallId,
|
|
95
|
+
name: message.toolName,
|
|
96
|
+
arguments: {},
|
|
97
|
+
status,
|
|
98
|
+
output,
|
|
99
|
+
settledSeq: seq,
|
|
100
|
+
...base
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (message.role === 'bashExecution') {
|
|
106
|
+
parts.push({
|
|
107
|
+
kind: 'bash',
|
|
108
|
+
command: message.command,
|
|
109
|
+
output: message.output,
|
|
110
|
+
...(typeof message.exitCode === 'number' ? { exitCode: message.exitCode } : {}),
|
|
111
|
+
...base
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (message.role === 'custom') {
|
|
116
|
+
parts.push({
|
|
117
|
+
kind: 'custom',
|
|
118
|
+
customType: message.customType,
|
|
119
|
+
text: (0, pi_entry_1.contentText)(message.content),
|
|
120
|
+
display: message.display !== false,
|
|
121
|
+
...(message.details !== undefined ? { details: message.details } : {}),
|
|
122
|
+
...base
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
parts.push({
|
|
127
|
+
kind: 'summary',
|
|
128
|
+
reason: message.role === 'branchSummary' ? 'branch' : 'compaction',
|
|
129
|
+
summary: message.summary ?? '',
|
|
130
|
+
...base
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
parts,
|
|
135
|
+
...(sessionId ? { sessionId } : {}),
|
|
136
|
+
...(cwd ? { cwd } : {})
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session projection: run log records → a pi session file.
|
|
3
|
+
*
|
|
4
|
+
* This is what makes a run resumable anywhere. The log is the source of truth;
|
|
5
|
+
* a `.jsonl` session is a derived artifact, so a run that started in the cloud
|
|
6
|
+
* can be continued locally (and vice versa) by projecting the log back into the
|
|
7
|
+
* only format pi's `SessionManager` reads.
|
|
8
|
+
*
|
|
9
|
+
* Returns a string rather than writing a file: the projection is pure, and the
|
|
10
|
+
* node-side write lives in `@agentic-kit/run-log/file-store`.
|
|
11
|
+
*/
|
|
12
|
+
import { type PiSessionEntry } from '../pi-entry';
|
|
13
|
+
import { type RunEventRecord } from '../record';
|
|
14
|
+
export interface SessionProjectionOptions {
|
|
15
|
+
/** Used when the log carries no session header (a run logged headerless). */
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SessionProjection {
|
|
21
|
+
/** The full session file contents, newline-terminated. */
|
|
22
|
+
jsonl: string;
|
|
23
|
+
/** Entries in file order, header first. */
|
|
24
|
+
entries: PiSessionEntry[];
|
|
25
|
+
piSessionVersion: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Project records into a pi session file. Throws when the records cannot form a
|
|
29
|
+
* loadable session — an unresumable session must fail at projection time, not
|
|
30
|
+
* when pi later reads a truncated tree.
|
|
31
|
+
*/
|
|
32
|
+
export declare function projectSession(records: readonly RunEventRecord[], options?: SessionProjectionOptions): SessionProjection;
|
|
33
|
+
/** Parse a pi session file into entries — the inverse, for importing a session. */
|
|
34
|
+
export declare function parseSessionJsonl(jsonl: string): PiSessionEntry[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session projection: run log records → a pi session file.
|
|
4
|
+
*
|
|
5
|
+
* This is what makes a run resumable anywhere. The log is the source of truth;
|
|
6
|
+
* a `.jsonl` session is a derived artifact, so a run that started in the cloud
|
|
7
|
+
* can be continued locally (and vice versa) by projecting the log back into the
|
|
8
|
+
* only format pi's `SessionManager` reads.
|
|
9
|
+
*
|
|
10
|
+
* Returns a string rather than writing a file: the projection is pure, and the
|
|
11
|
+
* node-side write lives in `@agentic-kit/run-log/file-store`.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.projectSession = projectSession;
|
|
15
|
+
exports.parseSessionJsonl = parseSessionJsonl;
|
|
16
|
+
const pi_entry_1 = require("../pi-entry");
|
|
17
|
+
const record_1 = require("../record");
|
|
18
|
+
/**
|
|
19
|
+
* Project records into a pi session file. Throws when the records cannot form a
|
|
20
|
+
* loadable session — an unresumable session must fail at projection time, not
|
|
21
|
+
* when pi later reads a truncated tree.
|
|
22
|
+
*/
|
|
23
|
+
function projectSession(records, options = {}) {
|
|
24
|
+
(0, record_1.assertOrdered)(records);
|
|
25
|
+
const versions = new Set(records.map((record) => record.piSessionVersion));
|
|
26
|
+
if (versions.size > 1) {
|
|
27
|
+
throw new Error(`run log mixes pi session versions (${Array.from(versions).sort().join(', ')}); migrate the older entries before projecting a session`);
|
|
28
|
+
}
|
|
29
|
+
const piSessionVersion = records[0]?.piSessionVersion ?? record_1.SUPPORTED_PI_SESSION_VERSION;
|
|
30
|
+
const entries = records.map((record) => record.entry);
|
|
31
|
+
const headerIndex = entries.findIndex(pi_entry_1.isPiSessionHeader);
|
|
32
|
+
if (headerIndex > 0) {
|
|
33
|
+
throw new Error(`run log carries a session header at position ${String(headerIndex)}; a pi session file requires it first`);
|
|
34
|
+
}
|
|
35
|
+
const body = headerIndex === 0 ? entries.slice(1) : entries;
|
|
36
|
+
const header = headerIndex === 0
|
|
37
|
+
? entries[0]
|
|
38
|
+
: {
|
|
39
|
+
type: 'session',
|
|
40
|
+
version: piSessionVersion,
|
|
41
|
+
id: options.sessionId ?? records[0]?.runId ?? 'run-log',
|
|
42
|
+
timestamp: options.timestamp ?? records[0]?.recordedAt ?? new Date().toISOString(),
|
|
43
|
+
...(options.cwd ? { cwd: options.cwd } : {})
|
|
44
|
+
};
|
|
45
|
+
const ordered = [header, ...body];
|
|
46
|
+
return {
|
|
47
|
+
entries: ordered,
|
|
48
|
+
piSessionVersion,
|
|
49
|
+
jsonl: ordered.map((entry) => JSON.stringify(entry)).join('\n') + '\n'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Parse a pi session file into entries — the inverse, for importing a session. */
|
|
53
|
+
function parseSessionJsonl(jsonl) {
|
|
54
|
+
const entries = [];
|
|
55
|
+
const lines = jsonl.split('\n');
|
|
56
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
57
|
+
const line = lines[i].trim();
|
|
58
|
+
if (line.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
entries.push(JSON.parse(line));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`pi session line ${String(i + 1)} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool and approval projection: run log records → the state a UI needs to know
|
|
3
|
+
* what is running and what is waiting on a human.
|
|
4
|
+
*
|
|
5
|
+
* Approvals ride in the log as pi `custom` messages rather than a side channel,
|
|
6
|
+
* because "the run is blocked on you" is part of the run's history: a surface
|
|
7
|
+
* that reconnects hours later must be able to see a pending request without
|
|
8
|
+
* having been present when it was raised, and both placements then behave the
|
|
9
|
+
* same — the cloud already treats the conversation as the approval UI.
|
|
10
|
+
*/
|
|
11
|
+
import type { RunEventRecord } from '../record';
|
|
12
|
+
/** `customType` of an approval request written by the gate extension. */
|
|
13
|
+
export declare const APPROVAL_REQUEST_TYPE = "constructive.approval.request";
|
|
14
|
+
/** `customType` of the human's answer to a request. */
|
|
15
|
+
export declare const APPROVAL_RESOLUTION_TYPE = "constructive.approval.resolution";
|
|
16
|
+
export type ToolCallStatus = 'requested' | 'awaiting-approval' | 'rejected' | 'running' | 'completed' | 'failed';
|
|
17
|
+
export interface ToolCallState {
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
arguments: Record<string, unknown>;
|
|
21
|
+
status: ToolCallStatus;
|
|
22
|
+
requestedSeq: number;
|
|
23
|
+
settledSeq?: number;
|
|
24
|
+
output?: string;
|
|
25
|
+
approval?: ApprovalState;
|
|
26
|
+
}
|
|
27
|
+
export interface ApprovalState {
|
|
28
|
+
toolCallId: string;
|
|
29
|
+
requestedSeq: number;
|
|
30
|
+
prompt: string;
|
|
31
|
+
resolvedSeq?: number;
|
|
32
|
+
decision?: 'approved' | 'rejected';
|
|
33
|
+
reason?: string;
|
|
34
|
+
actorId?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ToolStateProjection {
|
|
37
|
+
tools: Record<string, ToolCallState>;
|
|
38
|
+
/** In log order — the oldest unanswered request first. */
|
|
39
|
+
pendingApprovals: ApprovalState[];
|
|
40
|
+
}
|
|
41
|
+
export declare function projectToolState(records: readonly RunEventRecord[]): ToolStateProjection;
|
|
42
|
+
/** The parts of an approval request an extension needs to write one. */
|
|
43
|
+
export interface ApprovalRequestInput {
|
|
44
|
+
toolCallId: string;
|
|
45
|
+
prompt: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ApprovalResolutionInput {
|
|
48
|
+
toolCallId: string;
|
|
49
|
+
decision: 'approved' | 'rejected';
|
|
50
|
+
reason?: string;
|
|
51
|
+
actorId?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Build the pi `custom` message an approval request is carried in. */
|
|
54
|
+
export declare const approvalRequestMessage: (input: ApprovalRequestInput) => {
|
|
55
|
+
role: "custom";
|
|
56
|
+
customType: string;
|
|
57
|
+
content: string;
|
|
58
|
+
display: boolean;
|
|
59
|
+
details: {
|
|
60
|
+
toolCallId: string;
|
|
61
|
+
};
|
|
62
|
+
timestamp: number;
|
|
63
|
+
};
|
|
64
|
+
/** Build the pi `custom` message a human's answer is carried in. */
|
|
65
|
+
export declare const approvalResolutionMessage: (input: ApprovalResolutionInput) => {
|
|
66
|
+
role: "custom";
|
|
67
|
+
customType: string;
|
|
68
|
+
content: string;
|
|
69
|
+
display: boolean;
|
|
70
|
+
details: {
|
|
71
|
+
actorId?: string;
|
|
72
|
+
reason?: string;
|
|
73
|
+
toolCallId: string;
|
|
74
|
+
decision: "rejected" | "approved";
|
|
75
|
+
};
|
|
76
|
+
timestamp: number;
|
|
77
|
+
};
|