@nt-ai-lab/deterministic-agent-workflow-pi 0.4.3 → 0.5.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,15 @@
1
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ import type { RunnerResult } from '@nt-ai-lab/deterministic-agent-workflow-cli';
3
+ import type { PiSessionIdResult } from '../../../platform/domain/pi-workflow-extension-types';
4
+ /** @riviere-role cli-entrypoint */
5
+ export declare function translationNote(toolName: string): string;
6
+ /** @riviere-role cli-entrypoint */
7
+ export declare function resolveDatabasePath(configured: string | undefined): string;
8
+ /** @riviere-role cli-entrypoint */
9
+ export declare function readSessionId(ctx: ExtensionContext): PiSessionIdResult;
10
+ /** @riviere-role cli-entrypoint */
11
+ export declare function requireSessionFile(ctx: ExtensionContext): string;
12
+ /** @riviere-role cli-entrypoint */
13
+ export declare function notifyRouteResult(ctx: ExtensionContext, pi: ExtensionAPI, result: RunnerResult): void;
14
+ /** @riviere-role cli-entrypoint */
15
+ export declare function readWorkflowInstruction(path: string, note: string): string;
@@ -0,0 +1,67 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /** @riviere-role cli-entrypoint */
5
+ export function translationNote(toolName) {
6
+ return [
7
+ `> **Pi**: When instructions say to run a workflow command, call the \`${toolName}\` tool instead:`,
8
+ '> `operation: "<op>", args: ["<arg>", ...]`.',
9
+ '',
10
+ '---',
11
+ '',
12
+ '',
13
+ ].join('\n');
14
+ }
15
+ /** @riviere-role cli-entrypoint */
16
+ export function resolveDatabasePath(configured) {
17
+ if (configured !== undefined && configured !== '')
18
+ return configured;
19
+ const fromEnvironment = process.env['WORKFLOW_EVENTS_DB'];
20
+ if (fromEnvironment !== undefined && fromEnvironment !== '')
21
+ return fromEnvironment;
22
+ return join(homedir(), 'ai-workflow-database', '.workflow-events.db');
23
+ }
24
+ /** @riviere-role cli-entrypoint */
25
+ export function readSessionId(ctx) {
26
+ try {
27
+ const sessionId = ctx.sessionManager.getSessionId();
28
+ if (sessionId.trim().length === 0)
29
+ return {
30
+ ok: false,
31
+ reason: 'Pi returned an empty session UUID.'
32
+ };
33
+ return {
34
+ ok: true,
35
+ sessionId
36
+ };
37
+ }
38
+ catch (error) {
39
+ return {
40
+ ok: false,
41
+ reason: `Pi session UUID is unavailable: ${String(error)}`
42
+ };
43
+ }
44
+ }
45
+ /** @riviere-role cli-entrypoint */
46
+ export function requireSessionFile(ctx) {
47
+ const sessionFile = ctx.sessionManager.getSessionFile();
48
+ if (sessionFile === undefined)
49
+ throw new TypeError('Ephemeral Pi sessions are unsupported because no persistent transcript file is available.');
50
+ return sessionFile;
51
+ }
52
+ /** @riviere-role cli-entrypoint */
53
+ export function notifyRouteResult(ctx, pi, result) {
54
+ if (result.exitCode !== 0) {
55
+ ctx.ui.notify(result.output, 'error');
56
+ return;
57
+ }
58
+ if (result.output === '') {
59
+ ctx.ui.notify('Workflow operation completed.', 'info');
60
+ return;
61
+ }
62
+ pi.sendUserMessage(result.output);
63
+ }
64
+ /** @riviere-role cli-entrypoint */
65
+ export function readWorkflowInstruction(path, note) {
66
+ return `${note}${readFileSync(path, 'utf8')}`;
67
+ }
@@ -1,6 +1,6 @@
1
- import type { BaseWorkflowState, RehydratableWorkflow } from '@nt-ai-lab/deterministic-agent-workflow-engine';
1
+ import type { BaseWorkflowState, RehydratableWorkflow, TransitionContext } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
2
  import type { PiWorkflowExtension, PiWorkflowExtensionConfig } from '../../../platform/domain/pi-workflow-extension-types';
3
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
4
  export declare const PI_SESSION_BRANCH_BLOCK_MESSAGE = "Pi session tree navigation and forks are disabled while a workflow is active.";
5
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;
6
+ export declare function createPiWorkflowExtension<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string, TTransitionContext extends TransitionContext<TState, TStateName> = TransitionContext<TState, TStateName>>(config: PiWorkflowExtensionConfig<TWorkflow, TState, TDeps, TStateName, TOperation, TTransitionContext>): PiWorkflowExtension;
@@ -1,5 +1,3 @@
1
- import { readFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
1
  import { join } from 'node:path';
4
2
  import { WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
5
3
  import { createPreToolUseHandler, createWorkflowRunner, getRepositoryName, } from '@nt-ai-lab/deterministic-agent-workflow-cli';
@@ -8,77 +6,25 @@ import { Type } from 'typebox';
8
6
  import { parsePiCommandArguments } from '../../../platform/domain/pi-command-arguments.js';
9
7
  import { hasPiWorkflowMarker, PI_WORKFLOW_MARKER_CUSTOM_TYPE, readPiSessionMetadata, } from '../../../platform/infra/external-clients/pi/pi-session-file.js';
10
8
  import { getLatestPiAssistantSettlement, PiTranscriptReader, } from '../../../platform/infra/external-clients/pi/pi-transcript-reader.js';
9
+ import { notifyRouteResult, readSessionId, readWorkflowInstruction, requireSessionFile, resolveDatabasePath, translationNote, } from './pi-workflow-extension-platform.js';
11
10
  const PI_QUESTION_TOOL = 'question';
12
11
  const DEFAULT_COMMAND_NAME = 'workflow';
13
12
  const DEFAULT_TOOL_NAME = 'workflow';
14
13
  const INITIALIZATION_PENDING_REASON = 'Pi workflow initialization has not completed safely. Tool execution is blocked.';
14
+ const INACTIVE_WORKFLOW_REASON = 'Pi workflow is inactive. Run the workflow init command before using workflow operations.';
15
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
16
  export const PI_SESSION_BRANCH_BLOCK_MESSAGE = 'Pi session tree navigation and forks are disabled while a workflow is active.';
17
17
  const workflowToolParameters = Type.Object({
18
18
  operation: Type.String({ description: 'Workflow operation, for example init or transition' }),
19
19
  args: Type.Optional(Type.Array(Type.String({ description: 'One workflow operation argument' }))),
20
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
21
  /** @riviere-role cli-entrypoint */
77
22
  export function createPiWorkflowExtension(config) {
78
23
  const databasePath = resolveDatabasePath(config.databasePath);
79
24
  const commandName = config.commandName ?? DEFAULT_COMMAND_NAME;
80
25
  const toolName = config.toolName ?? DEFAULT_TOOL_NAME;
81
26
  const initializationBySession = new Map();
27
+ const sessionStartsById = new Map();
82
28
  const recoveredAssistantBySession = new Map();
83
29
  const preToolUse = createPreToolUseHandler({
84
30
  bashForbidden: config.bashForbidden,
@@ -94,23 +40,28 @@ export function createPiWorkflowExtension(config) {
94
40
  questionToolName: PI_QUESTION_TOOL,
95
41
  customGates: config.customGates,
96
42
  });
43
+ function buildEngineDeps(ctx, store) {
44
+ const sessionId = ctx.sessionManager.getSessionId();
45
+ const now = () => new Date().toISOString();
46
+ const note = translationNote(toolName);
47
+ return {
48
+ store,
49
+ getPluginRoot: () => config.pluginRoot,
50
+ getEnvFilePath: () => join(config.pluginRoot, '.pi', 'unused.env'),
51
+ getRepositoryName: () => getRepositoryName(ctx.cwd),
52
+ readFile: (path) => readWorkflowInstruction(path, note),
53
+ appendToFile: () => undefined,
54
+ now,
55
+ transcriptReader: new PiTranscriptReader(() => ctx.sessionManager.getBranch()),
56
+ sessionContext: { getMainSessionId: () => sessionId },
57
+ };
58
+ }
97
59
  function useEngine(ctx, operation) {
98
60
  const store = createStore(databasePath);
99
61
  try {
62
+ const engineDeps = buildEngineDeps(ctx, store);
63
+ const now = engineDeps.now;
100
64
  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
65
  const platform = {
115
66
  getPluginRoot: () => config.pluginRoot,
116
67
  now,
@@ -125,8 +76,22 @@ export function createPiWorkflowExtension(config) {
125
76
  store.db.close();
126
77
  }
127
78
  }
79
+ function hasPersistedWorkflowState(sessionId) {
80
+ const store = createStore(databasePath);
81
+ try {
82
+ return store.hasSessionStarted(sessionId);
83
+ }
84
+ finally {
85
+ store.db.close();
86
+ }
87
+ }
128
88
  function markInitializationFailed(ctx, sessionId, detail) {
129
- const reason = `Pi workflow initialization failed: ${detail}`;
89
+ return failSession(ctx, sessionId, `Pi workflow initialization failed: ${detail}`);
90
+ }
91
+ function markSafetyUnavailable(ctx, sessionId, detail) {
92
+ return failSession(ctx, sessionId, `Pi workflow safety is unavailable: ${detail}`);
93
+ }
94
+ function failSession(ctx, sessionId, reason) {
130
95
  initializationBySession.set(sessionId, {
131
96
  type: 'failed',
132
97
  reason,
@@ -142,8 +107,14 @@ export function createPiWorkflowExtension(config) {
142
107
  const status = initializationBySession.get(session.sessionId);
143
108
  if (status?.type === 'ready')
144
109
  return undefined;
110
+ if (status?.type === 'inactive')
111
+ return INACTIVE_WORKFLOW_REASON;
145
112
  return status?.type === 'failed' ? status.reason : INITIALIZATION_PENDING_REASON;
146
113
  }
114
+ function isInactive(ctx) {
115
+ const session = readSessionId(ctx);
116
+ return session.ok && initializationBySession.get(session.sessionId)?.type === 'inactive';
117
+ }
147
118
  function parentSafetyFailure(event, ctx) {
148
119
  const parentSessionFile = ctx.sessionManager.getHeader()?.parentSession;
149
120
  if (parentSessionFile === undefined) {
@@ -211,7 +182,29 @@ export function createPiWorkflowExtension(config) {
211
182
  return String(error);
212
183
  }
213
184
  }
214
- function runRoute(ctx, args) {
185
+ function runRoute(ctx, args, pi) {
186
+ if (isInactive(ctx) && args[0] === 'init') {
187
+ const session = readSessionId(ctx);
188
+ if (!session.ok)
189
+ return {
190
+ output: session.reason,
191
+ exitCode: 1,
192
+ };
193
+ const event = sessionStartsById.get(session.sessionId);
194
+ if (event === undefined)
195
+ return {
196
+ output: INITIALIZATION_PENDING_REASON,
197
+ exitCode: 1,
198
+ };
199
+ initializationBySession.set(session.sessionId, { type: 'initializing' });
200
+ const failure = initializeSession(event, ctx, pi, session.sessionId);
201
+ if (failure !== undefined)
202
+ return {
203
+ output: markInitializationFailed(ctx, session.sessionId, failure),
204
+ exitCode: 1,
205
+ };
206
+ initializationBySession.set(session.sessionId, { type: 'ready' });
207
+ }
215
208
  const notReady = readinessFailure(ctx);
216
209
  if (notReady !== undefined)
217
210
  return {
@@ -235,7 +228,7 @@ export function createPiWorkflowExtension(config) {
235
228
  }
236
229
  catch (error) {
237
230
  return {
238
- output: markInitializationFailed(ctx, session.sessionId, `Workflow operation could not establish safe state: ${String(error)}`),
231
+ output: markSafetyUnavailable(ctx, session.sessionId, `Workflow operation could not establish safe state: ${String(error)}`),
239
232
  exitCode: 1,
240
233
  };
241
234
  }
@@ -248,6 +241,19 @@ export function createPiWorkflowExtension(config) {
248
241
  ctx.shutdown();
249
242
  return;
250
243
  }
244
+ sessionStartsById.set(session.sessionId, event);
245
+ try {
246
+ const hasPersistedWorkflow = hasPersistedWorkflowState(session.sessionId);
247
+ const hasTranscriptWorkflow = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
248
+ if (!hasPersistedWorkflow && !hasTranscriptWorkflow) {
249
+ initializationBySession.set(session.sessionId, { type: 'inactive' });
250
+ return;
251
+ }
252
+ }
253
+ catch (error) {
254
+ markInitializationFailed(ctx, session.sessionId, String(error));
255
+ return;
256
+ }
251
257
  initializationBySession.set(session.sessionId, { type: 'initializing' });
252
258
  const failure = initializeSession(event, ctx, pi, session.sessionId);
253
259
  if (failure !== undefined) {
@@ -257,6 +263,8 @@ export function createPiWorkflowExtension(config) {
257
263
  initializationBySession.set(session.sessionId, { type: 'ready' });
258
264
  });
259
265
  pi.on('tool_call', (event, ctx) => {
266
+ if (isInactive(ctx))
267
+ return;
260
268
  const notReady = readinessFailure(ctx);
261
269
  if (notReady !== undefined)
262
270
  return {
@@ -281,11 +289,13 @@ export function createPiWorkflowExtension(config) {
281
289
  catch (error) {
282
290
  return {
283
291
  block: true,
284
- reason: markInitializationFailed(ctx, session.sessionId, `Tool safety could not be established: ${String(error)}`),
292
+ reason: markSafetyUnavailable(ctx, session.sessionId, `Tool safety could not be established: ${String(error)}`),
285
293
  };
286
294
  }
287
295
  });
288
296
  pi.on('input', (_event, ctx) => {
297
+ if (isInactive(ctx))
298
+ return;
289
299
  const notReady = readinessFailure(ctx);
290
300
  if (notReady === undefined)
291
301
  return;
@@ -293,7 +303,7 @@ export function createPiWorkflowExtension(config) {
293
303
  return { action: 'handled' };
294
304
  });
295
305
  pi.on('agent_settled', (_event, ctx) => {
296
- if (readinessFailure(ctx) !== undefined)
306
+ if (isInactive(ctx) || readinessFailure(ctx) !== undefined)
297
307
  return;
298
308
  const session = readSessionId(ctx);
299
309
  if (!session.ok)
@@ -311,7 +321,7 @@ export function createPiWorkflowExtension(config) {
311
321
  }
312
322
  }
313
323
  catch (error) {
314
- markInitializationFailed(ctx, session.sessionId, `Stopping safety could not be established: ${String(error)}`);
324
+ markSafetyUnavailable(ctx, session.sessionId, `Stopping safety could not be established: ${String(error)}`);
315
325
  }
316
326
  });
317
327
  const blockSessionBranching = (ctx) => {
@@ -321,6 +331,8 @@ export function createPiWorkflowExtension(config) {
321
331
  ctx.shutdown();
322
332
  return { cancel: true };
323
333
  }
334
+ if (isInactive(ctx))
335
+ return undefined;
324
336
  const notReady = readinessFailure(ctx);
325
337
  if (notReady !== undefined) {
326
338
  markInitializationFailed(ctx, session.sessionId, notReady);
@@ -339,7 +351,7 @@ export function createPiWorkflowExtension(config) {
339
351
  parameters: workflowToolParameters,
340
352
  executionMode: 'sequential',
341
353
  async execute(_toolCallId, parameters, _signal, _onUpdate, ctx) {
342
- const result = runRoute(ctx, [parameters.operation, ...(parameters.args ?? [])]);
354
+ const result = runRoute(ctx, [parameters.operation, ...(parameters.args ?? [])], pi);
343
355
  return {
344
356
  content: [{
345
357
  type: 'text',
@@ -354,7 +366,7 @@ export function createPiWorkflowExtension(config) {
354
366
  description: `Execute a deterministic workflow operation: /${commandName} <operation> [args]`,
355
367
  handler: async (rawArguments, ctx) => {
356
368
  try {
357
- const result = runRoute(ctx, parsePiCommandArguments(rawArguments));
369
+ const result = runRoute(ctx, parsePiCommandArguments(rawArguments), pi);
358
370
  notifyRouteResult(ctx, pi, result);
359
371
  }
360
372
  catch (error) {
@@ -1,10 +1,12 @@
1
1
  import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
2
- import type { BaseWorkflowState, RehydratableWorkflow, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { BaseWorkflowState, RehydratableWorkflow, TransitionContext, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
3
3
  import type { PlatformContext, PreToolUseHandlerConfig, RouteMap } from '@nt-ai-lab/deterministic-agent-workflow-cli';
4
4
  /** @riviere-role value-object */
5
5
  export type PiWorkflowExtension = ExtensionFactory;
6
6
  /** @riviere-role value-object */
7
7
  export type PiInitializationStatus = {
8
+ readonly type: 'inactive';
9
+ } | {
8
10
  readonly type: 'initializing';
9
11
  } | {
10
12
  readonly type: 'ready';
@@ -21,8 +23,8 @@ export type PiSessionIdResult = {
21
23
  readonly reason: string;
22
24
  };
23
25
  /** @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
+ export type PiWorkflowExtensionConfig<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string, TTransitionContext extends TransitionContext<TState, TStateName> = TransitionContext<TState, TStateName>> = Omit<PreToolUseHandlerConfig<TWorkflow, TState, TStateName>, 'questionToolName'> & {
27
+ readonly workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation, TTransitionContext>;
26
28
  readonly routes: RouteMap<TWorkflow, TState>;
27
29
  readonly buildWorkflowDeps: (platform: PlatformContext) => TDeps;
28
30
  readonly pluginRoot: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nt-ai-lab/deterministic-agent-workflow-pi",
3
- "version": "0.4.3",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,9 +23,9 @@
23
23
  },
24
24
  "dependencies": {
25
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"
26
+ "@nt-ai-lab/deterministic-agent-workflow-cli": "0.4.6",
27
+ "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.6",
28
+ "@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.6"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "^0.84.4",