@nt-ai-lab/deterministic-agent-workflow-pi 0.4.3

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.
@@ -0,0 +1,6 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { PiWorkflowExtension, PiWorkflowExtensionConfig } from '../../../platform/domain/pi-workflow-extension-types';
3
+ export declare const PI_IDLE_RECOVERY_MESSAGE = "You have stopped. You should never stop until the workflow is complete unless your current state permits stopping.";
4
+ export declare const PI_SESSION_BRANCH_BLOCK_MESSAGE = "Pi session tree navigation and forks are disabled while a workflow is active.";
5
+ /** @riviere-role cli-entrypoint */
6
+ export declare function createPiWorkflowExtension<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string>(config: PiWorkflowExtensionConfig<TWorkflow, TState, TDeps, TStateName, TOperation>): PiWorkflowExtension;
@@ -0,0 +1,366 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
5
+ import { createPreToolUseHandler, createWorkflowRunner, getRepositoryName, } from '@nt-ai-lab/deterministic-agent-workflow-cli';
6
+ import { createStore } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
7
+ import { Type } from 'typebox';
8
+ import { parsePiCommandArguments } from '../../../platform/domain/pi-command-arguments.js';
9
+ import { hasPiWorkflowMarker, PI_WORKFLOW_MARKER_CUSTOM_TYPE, readPiSessionMetadata, } from '../../../platform/infra/external-clients/pi/pi-session-file.js';
10
+ import { getLatestPiAssistantSettlement, PiTranscriptReader, } from '../../../platform/infra/external-clients/pi/pi-transcript-reader.js';
11
+ const PI_QUESTION_TOOL = 'question';
12
+ const DEFAULT_COMMAND_NAME = 'workflow';
13
+ const DEFAULT_TOOL_NAME = 'workflow';
14
+ const INITIALIZATION_PENDING_REASON = 'Pi workflow initialization has not completed safely. Tool execution is blocked.';
15
+ export const PI_IDLE_RECOVERY_MESSAGE = 'You have stopped. You should never stop until the workflow is complete unless your current state permits stopping.';
16
+ export const PI_SESSION_BRANCH_BLOCK_MESSAGE = 'Pi session tree navigation and forks are disabled while a workflow is active.';
17
+ const workflowToolParameters = Type.Object({
18
+ operation: Type.String({ description: 'Workflow operation, for example init or transition' }),
19
+ args: Type.Optional(Type.Array(Type.String({ description: 'One workflow operation argument' }))),
20
+ });
21
+ function translationNote(toolName) {
22
+ return [
23
+ `> **Pi**: When instructions say to run a workflow command, call the \`${toolName}\` tool instead:`,
24
+ '> `operation: "<op>", args: ["<arg>", ...]`.',
25
+ '',
26
+ '---',
27
+ '',
28
+ '',
29
+ ].join('\n');
30
+ }
31
+ function resolveDatabasePath(configured) {
32
+ if (configured !== undefined && configured !== '')
33
+ return configured;
34
+ const fromEnvironment = process.env['WORKFLOW_EVENTS_DB'];
35
+ if (fromEnvironment !== undefined && fromEnvironment !== '')
36
+ return fromEnvironment;
37
+ return join(homedir(), 'ai-workflow-database', '.workflow-events.db');
38
+ }
39
+ function readSessionId(ctx) {
40
+ try {
41
+ const sessionId = ctx.sessionManager.getSessionId();
42
+ if (sessionId.trim().length === 0)
43
+ return {
44
+ ok: false,
45
+ reason: 'Pi returned an empty session UUID.',
46
+ };
47
+ return {
48
+ ok: true,
49
+ sessionId,
50
+ };
51
+ }
52
+ catch (error) {
53
+ return {
54
+ ok: false,
55
+ reason: `Pi session UUID is unavailable: ${String(error)}`,
56
+ };
57
+ }
58
+ }
59
+ function requireSessionFile(ctx) {
60
+ const sessionFile = ctx.sessionManager.getSessionFile();
61
+ if (sessionFile === undefined)
62
+ throw new TypeError('Ephemeral Pi sessions are unsupported because no persistent transcript file is available.');
63
+ return sessionFile;
64
+ }
65
+ function notifyRouteResult(ctx, pi, result) {
66
+ if (result.exitCode !== 0) {
67
+ ctx.ui.notify(result.output, 'error');
68
+ return;
69
+ }
70
+ if (result.output === '') {
71
+ ctx.ui.notify('Workflow operation completed.', 'info');
72
+ return;
73
+ }
74
+ pi.sendUserMessage(result.output);
75
+ }
76
+ /** @riviere-role cli-entrypoint */
77
+ export function createPiWorkflowExtension(config) {
78
+ const databasePath = resolveDatabasePath(config.databasePath);
79
+ const commandName = config.commandName ?? DEFAULT_COMMAND_NAME;
80
+ const toolName = config.toolName ?? DEFAULT_TOOL_NAME;
81
+ const initializationBySession = new Map();
82
+ const recoveredAssistantBySession = new Map();
83
+ const preToolUse = createPreToolUseHandler({
84
+ bashForbidden: config.bashForbidden,
85
+ isWriteAllowed: config.isWriteAllowed,
86
+ questionToolName: PI_QUESTION_TOOL,
87
+ customGates: config.customGates,
88
+ });
89
+ const runner = createWorkflowRunner({
90
+ workflowDefinition: config.workflowDefinition,
91
+ routes: config.routes,
92
+ bashForbidden: config.bashForbidden,
93
+ isWriteAllowed: config.isWriteAllowed,
94
+ questionToolName: PI_QUESTION_TOOL,
95
+ customGates: config.customGates,
96
+ });
97
+ function useEngine(ctx, operation) {
98
+ const store = createStore(databasePath);
99
+ try {
100
+ const sessionId = ctx.sessionManager.getSessionId();
101
+ const now = () => new Date().toISOString();
102
+ const note = translationNote(toolName);
103
+ const engineDeps = {
104
+ store,
105
+ getPluginRoot: () => config.pluginRoot,
106
+ getEnvFilePath: () => join(config.pluginRoot, '.pi', 'unused.env'),
107
+ getRepositoryName: () => getRepositoryName(ctx.cwd),
108
+ readFile: (path) => `${note}${readFileSync(path, 'utf8')}`,
109
+ appendToFile: () => undefined,
110
+ now,
111
+ transcriptReader: new PiTranscriptReader(() => ctx.sessionManager.getBranch()),
112
+ sessionContext: { getMainSessionId: () => sessionId },
113
+ };
114
+ const platform = {
115
+ getPluginRoot: () => config.pluginRoot,
116
+ now,
117
+ getSessionId: () => sessionId,
118
+ store,
119
+ };
120
+ const workflowDeps = config.buildWorkflowDeps(platform);
121
+ const engine = new WorkflowEngine(config.workflowDefinition, engineDeps, workflowDeps);
122
+ return operation(engine, engineDeps, workflowDeps);
123
+ }
124
+ finally {
125
+ store.db.close();
126
+ }
127
+ }
128
+ function markInitializationFailed(ctx, sessionId, detail) {
129
+ const reason = `Pi workflow initialization failed: ${detail}`;
130
+ initializationBySession.set(sessionId, {
131
+ type: 'failed',
132
+ reason,
133
+ });
134
+ ctx.ui.notify(reason, 'error');
135
+ ctx.shutdown();
136
+ return reason;
137
+ }
138
+ function readinessFailure(ctx) {
139
+ const session = readSessionId(ctx);
140
+ if (!session.ok)
141
+ return session.reason;
142
+ const status = initializationBySession.get(session.sessionId);
143
+ if (status?.type === 'ready')
144
+ return undefined;
145
+ return status?.type === 'failed' ? status.reason : INITIALIZATION_PENDING_REASON;
146
+ }
147
+ function parentSafetyFailure(event, ctx) {
148
+ const parentSessionFile = ctx.sessionManager.getHeader()?.parentSession;
149
+ if (parentSessionFile === undefined) {
150
+ return event.reason === 'fork' ? 'Forked Pi session has no verifiable parent session file.' : undefined;
151
+ }
152
+ const parent = readPiSessionMetadata(parentSessionFile);
153
+ const store = createStore(databasePath);
154
+ try {
155
+ return store.hasSessionStarted(parent.id) || parent.hasWorkflowMarker
156
+ ? `Cannot fork Pi session ${parent.id}: its workflow is active.`
157
+ : undefined;
158
+ }
159
+ finally {
160
+ store.db.close();
161
+ }
162
+ }
163
+ function initializeSession(event, ctx, pi, sessionId) {
164
+ try {
165
+ const sessionFile = requireSessionFile(ctx);
166
+ const header = ctx.sessionManager.getHeader();
167
+ if (header?.id !== sessionId)
168
+ return 'Pi session header does not match the active session UUID.';
169
+ const activeBranchHasWorkflowMarker = hasPiWorkflowMarker(ctx.sessionManager.getBranch());
170
+ const sessionHasWorkflowMarker = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
171
+ const result = useEngine(ctx, (engine) => {
172
+ const sqliteHasWorkflowState = engine.hasSessionStarted(sessionId);
173
+ if (!sqliteHasWorkflowState) {
174
+ const parentFailure = parentSafetyFailure(event, ctx);
175
+ if (parentFailure !== undefined)
176
+ return {
177
+ type: 'error',
178
+ output: parentFailure,
179
+ };
180
+ }
181
+ if (sessionHasWorkflowMarker && !activeBranchHasWorkflowMarker)
182
+ return {
183
+ type: 'error',
184
+ output: `The active Pi branch does not contain this session's ${PI_WORKFLOW_MARKER_CUSTOM_TYPE} marker.`,
185
+ };
186
+ if (activeBranchHasWorkflowMarker !== sqliteHasWorkflowState)
187
+ return {
188
+ type: 'error',
189
+ output: `Pi transcript and SQLite workflow state disagree for session ${sessionId}.`,
190
+ };
191
+ const repository = getRepositoryName(ctx.cwd);
192
+ if (repository === undefined)
193
+ return {
194
+ type: 'error',
195
+ output: 'repository must be a non-empty string.',
196
+ };
197
+ return engine.startSession(sessionId, sessionFile, repository);
198
+ });
199
+ if (result.type !== 'success')
200
+ return result.output;
201
+ if (result.output !== '') {
202
+ pi.sendMessage({
203
+ customType: 'deterministic-agent-workflow',
204
+ content: result.output,
205
+ display: true,
206
+ }, { triggerTurn: false });
207
+ }
208
+ return undefined;
209
+ }
210
+ catch (error) {
211
+ return String(error);
212
+ }
213
+ }
214
+ function runRoute(ctx, args) {
215
+ const notReady = readinessFailure(ctx);
216
+ if (notReady !== undefined)
217
+ return {
218
+ output: notReady,
219
+ exitCode: 1,
220
+ };
221
+ const session = readSessionId(ctx);
222
+ if (!session.ok)
223
+ return {
224
+ output: session.reason,
225
+ exitCode: 1,
226
+ };
227
+ try {
228
+ return useEngine(ctx, (_engine, engineDeps, workflowDeps) => runner(args, engineDeps, workflowDeps, {
229
+ getSessionId: () => session.sessionId,
230
+ getSessionTranscriptPath: () => requireSessionFile(ctx),
231
+ getSessionRepository: () => getRepositoryName(ctx.cwd),
232
+ getRepositoryRoot: () => ctx.cwd,
233
+ getWorkflowEventsDbPath: () => databasePath,
234
+ }));
235
+ }
236
+ catch (error) {
237
+ return {
238
+ output: markInitializationFailed(ctx, session.sessionId, `Workflow operation could not establish safe state: ${String(error)}`),
239
+ exitCode: 1,
240
+ };
241
+ }
242
+ }
243
+ return (pi) => {
244
+ pi.on('session_start', (event, ctx) => {
245
+ const session = readSessionId(ctx);
246
+ if (!session.ok) {
247
+ ctx.ui.notify(`Pi workflow initialization failed: ${session.reason}`, 'error');
248
+ ctx.shutdown();
249
+ return;
250
+ }
251
+ initializationBySession.set(session.sessionId, { type: 'initializing' });
252
+ const failure = initializeSession(event, ctx, pi, session.sessionId);
253
+ if (failure !== undefined) {
254
+ markInitializationFailed(ctx, session.sessionId, failure);
255
+ return;
256
+ }
257
+ initializationBySession.set(session.sessionId, { type: 'ready' });
258
+ });
259
+ pi.on('tool_call', (event, ctx) => {
260
+ const notReady = readinessFailure(ctx);
261
+ if (notReady !== undefined)
262
+ return {
263
+ block: true,
264
+ reason: notReady,
265
+ };
266
+ const session = readSessionId(ctx);
267
+ if (!session.ok)
268
+ return {
269
+ block: true,
270
+ reason: session.reason,
271
+ };
272
+ try {
273
+ const result = useEngine(ctx, (engine) => preToolUse(engine, session.sessionId, event.toolName, { ...event.input }));
274
+ if (result.type === 'success')
275
+ return;
276
+ return {
277
+ block: true,
278
+ reason: result.output,
279
+ };
280
+ }
281
+ catch (error) {
282
+ return {
283
+ block: true,
284
+ reason: markInitializationFailed(ctx, session.sessionId, `Tool safety could not be established: ${String(error)}`),
285
+ };
286
+ }
287
+ });
288
+ pi.on('input', (_event, ctx) => {
289
+ const notReady = readinessFailure(ctx);
290
+ if (notReady === undefined)
291
+ return;
292
+ ctx.ui.notify(notReady, 'error');
293
+ return { action: 'handled' };
294
+ });
295
+ pi.on('agent_settled', (_event, ctx) => {
296
+ if (readinessFailure(ctx) !== undefined)
297
+ return;
298
+ const session = readSessionId(ctx);
299
+ if (!session.ok)
300
+ return;
301
+ const settlement = getLatestPiAssistantSettlement(ctx.sessionManager.getBranch());
302
+ if (settlement?.stopReason !== 'stop')
303
+ return;
304
+ if (recoveredAssistantBySession.get(session.sessionId) === settlement.id)
305
+ return;
306
+ try {
307
+ const result = useEngine(ctx, (engine) => engine.checkStopping(session.sessionId, 'stop'));
308
+ if (result.type === 'blocked' && ctx.isIdle() && !ctx.hasPendingMessages()) {
309
+ recoveredAssistantBySession.set(session.sessionId, settlement.id);
310
+ pi.sendUserMessage(PI_IDLE_RECOVERY_MESSAGE);
311
+ }
312
+ }
313
+ catch (error) {
314
+ markInitializationFailed(ctx, session.sessionId, `Stopping safety could not be established: ${String(error)}`);
315
+ }
316
+ });
317
+ const blockSessionBranching = (ctx) => {
318
+ const session = readSessionId(ctx);
319
+ if (!session.ok) {
320
+ ctx.ui.notify(`${INITIALIZATION_PENDING_REASON} ${session.reason}`, 'error');
321
+ ctx.shutdown();
322
+ return { cancel: true };
323
+ }
324
+ const notReady = readinessFailure(ctx);
325
+ if (notReady !== undefined) {
326
+ markInitializationFailed(ctx, session.sessionId, notReady);
327
+ return { cancel: true };
328
+ }
329
+ ctx.ui.notify(PI_SESSION_BRANCH_BLOCK_MESSAGE, 'warning');
330
+ return { cancel: true };
331
+ };
332
+ pi.on('session_before_tree', (_event, ctx) => blockSessionBranching(ctx));
333
+ pi.on('session_before_fork', (_event, ctx) => blockSessionBranching(ctx));
334
+ pi.registerTool({
335
+ name: toolName,
336
+ label: 'Workflow',
337
+ description: 'Execute a deterministic workflow operation such as init, transition, or record-*.',
338
+ promptSnippet: `Execute deterministic workflow operations with ${toolName}.`,
339
+ parameters: workflowToolParameters,
340
+ executionMode: 'sequential',
341
+ async execute(_toolCallId, parameters, _signal, _onUpdate, ctx) {
342
+ const result = runRoute(ctx, [parameters.operation, ...(parameters.args ?? [])]);
343
+ return {
344
+ content: [{
345
+ type: 'text',
346
+ text: result.output,
347
+ }],
348
+ details: { exitCode: result.exitCode },
349
+ isError: result.exitCode !== 0,
350
+ };
351
+ },
352
+ });
353
+ pi.registerCommand(commandName, {
354
+ description: `Execute a deterministic workflow operation: /${commandName} <operation> [args]`,
355
+ handler: async (rawArguments, ctx) => {
356
+ try {
357
+ const result = runRoute(ctx, parsePiCommandArguments(rawArguments));
358
+ notifyRouteResult(ctx, pi, result);
359
+ }
360
+ catch (error) {
361
+ ctx.ui.notify(String(error), 'error');
362
+ }
363
+ },
364
+ });
365
+ };
366
+ }
@@ -0,0 +1,3 @@
1
+ export { createPiWorkflowExtension, PI_IDLE_RECOVERY_MESSAGE, PI_SESSION_BRANCH_BLOCK_MESSAGE, } from './features/pi-extension/entrypoint/pi-workflow-extension';
2
+ export type { PiWorkflowExtension, PiWorkflowExtensionConfig, } from './platform/domain/pi-workflow-extension-types';
3
+ export { PiTranscriptReader } from './platform/infra/external-clients/pi/pi-transcript-reader';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createPiWorkflowExtension, PI_IDLE_RECOVERY_MESSAGE, PI_SESSION_BRANCH_BLOCK_MESSAGE, } from './features/pi-extension/entrypoint/pi-workflow-extension.js';
2
+ export { PiTranscriptReader } from './platform/infra/external-clients/pi/pi-transcript-reader.js';
@@ -0,0 +1,4 @@
1
+ /** @riviere-role domain-error */
2
+ export declare class PiCommandArgumentError extends TypeError {
3
+ constructor(message: string);
4
+ }
@@ -0,0 +1,7 @@
1
+ /** @riviere-role domain-error */
2
+ export class PiCommandArgumentError extends TypeError {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'PiCommandArgumentError';
6
+ }
7
+ }
@@ -0,0 +1,2 @@
1
+ /** @riviere-role domain-service */
2
+ export declare function parsePiCommandArguments(input: string): readonly string[];
@@ -0,0 +1,61 @@
1
+ import { PiCommandArgumentError } from './pi-command-argument-error.js';
2
+ function nextState(state, character) {
3
+ if (state.escaping)
4
+ return {
5
+ ...state,
6
+ current: `${state.current}${character}`,
7
+ escaping: false,
8
+ };
9
+ if (character === '\\')
10
+ return {
11
+ ...state,
12
+ escaping: true,
13
+ tokenStarted: true,
14
+ };
15
+ if (state.quote !== undefined)
16
+ return character === state.quote
17
+ ? {
18
+ ...state,
19
+ quote: undefined,
20
+ }
21
+ : {
22
+ ...state,
23
+ current: `${state.current}${character}`,
24
+ };
25
+ if (character === '"' || character === "'")
26
+ return {
27
+ ...state,
28
+ quote: character,
29
+ tokenStarted: true,
30
+ };
31
+ if (/\s/.test(character))
32
+ return state.tokenStarted
33
+ ? {
34
+ tokens: [...state.tokens, state.current],
35
+ current: '',
36
+ quote: undefined,
37
+ escaping: false,
38
+ tokenStarted: false,
39
+ }
40
+ : state;
41
+ return {
42
+ ...state,
43
+ current: `${state.current}${character}`,
44
+ tokenStarted: true,
45
+ };
46
+ }
47
+ /** @riviere-role domain-service */
48
+ export function parsePiCommandArguments(input) {
49
+ const parsed = [...input].reduce(nextState, {
50
+ tokens: [],
51
+ current: '',
52
+ quote: undefined,
53
+ escaping: false,
54
+ tokenStarted: false,
55
+ });
56
+ if (parsed.quote !== undefined)
57
+ throw new PiCommandArgumentError(`Unmatched ${parsed.quote} quote in workflow command arguments.`);
58
+ if (parsed.escaping)
59
+ throw new PiCommandArgumentError('Workflow command arguments end with an unmatched escape character.');
60
+ return parsed.tokenStarted ? [...parsed.tokens, parsed.current] : parsed.tokens;
61
+ }
@@ -0,0 +1,32 @@
1
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
2
+ import type { BaseWorkflowState, RehydratableWorkflow, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
3
+ import type { PlatformContext, PreToolUseHandlerConfig, RouteMap } from '@nt-ai-lab/deterministic-agent-workflow-cli';
4
+ /** @riviere-role value-object */
5
+ export type PiWorkflowExtension = ExtensionFactory;
6
+ /** @riviere-role value-object */
7
+ export type PiInitializationStatus = {
8
+ readonly type: 'initializing';
9
+ } | {
10
+ readonly type: 'ready';
11
+ } | {
12
+ readonly type: 'failed';
13
+ readonly reason: string;
14
+ };
15
+ /** @riviere-role value-object */
16
+ export type PiSessionIdResult = {
17
+ readonly ok: true;
18
+ readonly sessionId: string;
19
+ } | {
20
+ readonly ok: false;
21
+ readonly reason: string;
22
+ };
23
+ /** @riviere-role value-object */
24
+ export type PiWorkflowExtensionConfig<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string> = Omit<PreToolUseHandlerConfig<TWorkflow, TState, TStateName>, 'questionToolName'> & {
25
+ readonly workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation>;
26
+ readonly routes: RouteMap<TWorkflow, TState>;
27
+ readonly buildWorkflowDeps: (platform: PlatformContext) => TDeps;
28
+ readonly pluginRoot: string;
29
+ readonly databasePath?: string;
30
+ readonly commandName?: string;
31
+ readonly toolName?: string;
32
+ };
@@ -0,0 +1,4 @@
1
+ /** @riviere-role external-client-error */
2
+ export declare class PiSessionFileError extends TypeError {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
@@ -0,0 +1,7 @@
1
+ /** @riviere-role external-client-error */
2
+ export class PiSessionFileError extends TypeError {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = 'PiSessionFileError';
6
+ }
7
+ }
@@ -0,0 +1,13 @@
1
+ import type { SessionEntry } from '@earendil-works/pi-coding-agent';
2
+ export declare const PI_WORKFLOW_MARKER_CUSTOM_TYPE = "deterministic-agent-workflow";
3
+ /** @riviere-role external-client-model */
4
+ export type PiSessionMetadata = {
5
+ readonly id: string;
6
+ readonly hasWorkflowMarker: boolean;
7
+ };
8
+ /** @riviere-role external-client-service */
9
+ export declare function hasPiWorkflowMarker(entries: readonly SessionEntry[]): boolean;
10
+ /** @riviere-role external-client-service */
11
+ export declare function readPiSessionMetadata(filePath: string): PiSessionMetadata;
12
+ /** @riviere-role external-client-service */
13
+ export declare function readPiSessionId(filePath: string): string;
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { PiSessionFileError } from './pi-session-file-error.js';
3
+ const MAX_ENTRY_BYTES = 1024 * 1024;
4
+ export const PI_WORKFLOW_MARKER_CUSTOM_TYPE = 'deterministic-agent-workflow';
5
+ function parseEntry(entryText, filePath, lineNumber) {
6
+ if (Buffer.byteLength(entryText) > MAX_ENTRY_BYTES) {
7
+ throw new PiSessionFileError(`Pi session entry at line ${lineNumber} exceeds ${MAX_ENTRY_BYTES} bytes: ${filePath}`);
8
+ }
9
+ if (entryText.trim().length === 0) {
10
+ const detail = lineNumber === 1 ? 'an empty header' : `an empty entry at line ${lineNumber}`;
11
+ throw new PiSessionFileError(`Pi session file has ${detail}: ${filePath}`);
12
+ }
13
+ try {
14
+ const entry = JSON.parse(entryText);
15
+ if (typeof entry !== 'object' || entry === null || !('type' in entry) || typeof entry.type !== 'string') {
16
+ throw new PiSessionFileError(`Pi session file has an invalid entry at line ${lineNumber}: ${filePath}`);
17
+ }
18
+ return entry;
19
+ }
20
+ catch (error) {
21
+ if (error instanceof PiSessionFileError)
22
+ throw error;
23
+ throw new PiSessionFileError(`Cannot parse Pi session entry at line ${lineNumber}: ${filePath}`, { cause: error });
24
+ }
25
+ }
26
+ function readEntries(filePath) {
27
+ const content = readFileSync(filePath, 'utf8');
28
+ const serializedEntries = content.endsWith('\n') ? content.slice(0, -1).split('\n') : content.split('\n');
29
+ return serializedEntries.map((entry, index) => parseEntry(entry, filePath, index + 1));
30
+ }
31
+ function requireSessionId(header, filePath) {
32
+ if (header.type !== 'session')
33
+ throw new PiSessionFileError(`Pi session file does not begin with a session header: ${filePath}`);
34
+ if (typeof header.id !== 'string' || header.id.trim().length === 0) {
35
+ throw new PiSessionFileError(`Pi session header has no valid session UUID: ${filePath}`);
36
+ }
37
+ return header.id;
38
+ }
39
+ function isWorkflowMarker(entry) {
40
+ return entry.type === 'custom_message' && entry.customType === PI_WORKFLOW_MARKER_CUSTOM_TYPE;
41
+ }
42
+ /** @riviere-role external-client-service */
43
+ export function hasPiWorkflowMarker(entries) {
44
+ return entries.some((entry) => entry.type === 'custom_message' && entry.customType === PI_WORKFLOW_MARKER_CUSTOM_TYPE);
45
+ }
46
+ /** @riviere-role external-client-service */
47
+ export function readPiSessionMetadata(filePath) {
48
+ try {
49
+ const [header, ...entries] = readEntries(filePath);
50
+ return {
51
+ id: requireSessionId(header, filePath),
52
+ hasWorkflowMarker: entries.some(isWorkflowMarker),
53
+ };
54
+ }
55
+ catch (error) {
56
+ if (error instanceof PiSessionFileError)
57
+ throw error;
58
+ throw new PiSessionFileError(`Cannot read Pi session file: ${filePath}`, { cause: error });
59
+ }
60
+ }
61
+ /** @riviere-role external-client-service */
62
+ export function readPiSessionId(filePath) {
63
+ return readPiSessionMetadata(filePath).id;
64
+ }
@@ -0,0 +1,15 @@
1
+ import type { SessionEntry } from '@earendil-works/pi-coding-agent';
2
+ import type { TranscriptMessage, TranscriptReader } from '@nt-ai-lab/deterministic-agent-workflow-engine';
3
+ /** @riviere-role external-client-model */
4
+ export type PiAssistantSettlement = {
5
+ readonly id: string;
6
+ readonly stopReason: string;
7
+ };
8
+ /** @riviere-role external-client-service */
9
+ export declare function getLatestPiAssistantSettlement(entries: readonly SessionEntry[]): PiAssistantSettlement | undefined;
10
+ /** @riviere-role external-client-model */
11
+ export declare class PiTranscriptReader implements TranscriptReader {
12
+ private readonly getBranch;
13
+ constructor(getBranch: () => readonly SessionEntry[]);
14
+ readMessages(): readonly TranscriptMessage[];
15
+ }
@@ -0,0 +1,32 @@
1
+ /** @riviere-role external-client-service */
2
+ export function getLatestPiAssistantSettlement(entries) {
3
+ const entry = entries.findLast((candidate) => candidate.type === 'message' && candidate.message.role === 'assistant');
4
+ if (entry?.type !== 'message' || entry.message.role !== 'assistant')
5
+ return undefined;
6
+ return {
7
+ id: entry.id,
8
+ stopReason: entry.message.stopReason,
9
+ };
10
+ }
11
+ /** @riviere-role external-client-model */
12
+ export class PiTranscriptReader {
13
+ getBranch;
14
+ constructor(getBranch) {
15
+ this.getBranch = getBranch;
16
+ }
17
+ readMessages() {
18
+ return this.getBranch().flatMap((entry) => {
19
+ if (entry.type !== 'message' || entry.message.role !== 'assistant')
20
+ return [];
21
+ const text = entry.message.content
22
+ .filter((content) => content.type === 'text')
23
+ .map((content) => content.text)
24
+ .join('\n')
25
+ .trim();
26
+ return [{
27
+ id: entry.id,
28
+ textContent: text.length === 0 ? undefined : text,
29
+ }];
30
+ });
31
+ }
32
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@nt-ai-lab/deterministic-agent-workflow-pi",
3
+ "version": "0.4.3",
4
+ "private": false,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/NTCoding/deterministic-agent-workflows.git",
12
+ "directory": "packages/deterministic-agent-workflows-pi"
13
+ },
14
+ "exports": {
15
+ ".": "./dist/index.js"
16
+ },
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "peerDependencies": {
22
+ "@earendil-works/pi-coding-agent": "^0.84.4"
23
+ },
24
+ "dependencies": {
25
+ "typebox": "^1.3.7",
26
+ "@nt-ai-lab/deterministic-agent-workflow-cli": "0.4.4",
27
+ "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.4",
28
+ "@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.4"
29
+ },
30
+ "devDependencies": {
31
+ "@earendil-works/pi-coding-agent": "^0.84.4",
32
+ "vitest": "^2.1.9",
33
+ "zod": "^3.25.76"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }