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

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,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,
@@ -105,7 +51,7 @@ export function createPiWorkflowExtension(config) {
105
51
  getPluginRoot: () => config.pluginRoot,
106
52
  getEnvFilePath: () => join(config.pluginRoot, '.pi', 'unused.env'),
107
53
  getRepositoryName: () => getRepositoryName(ctx.cwd),
108
- readFile: (path) => `${note}${readFileSync(path, 'utf8')}`,
54
+ readFile: (path) => readWorkflowInstruction(path, note),
109
55
  appendToFile: () => undefined,
110
56
  now,
111
57
  transcriptReader: new PiTranscriptReader(() => ctx.sessionManager.getBranch()),
@@ -142,8 +88,14 @@ export function createPiWorkflowExtension(config) {
142
88
  const status = initializationBySession.get(session.sessionId);
143
89
  if (status?.type === 'ready')
144
90
  return undefined;
91
+ if (status?.type === 'inactive')
92
+ return INACTIVE_WORKFLOW_REASON;
145
93
  return status?.type === 'failed' ? status.reason : INITIALIZATION_PENDING_REASON;
146
94
  }
95
+ function isInactive(ctx) {
96
+ const session = readSessionId(ctx);
97
+ return session.ok && initializationBySession.get(session.sessionId)?.type === 'inactive';
98
+ }
147
99
  function parentSafetyFailure(event, ctx) {
148
100
  const parentSessionFile = ctx.sessionManager.getHeader()?.parentSession;
149
101
  if (parentSessionFile === undefined) {
@@ -211,7 +163,29 @@ export function createPiWorkflowExtension(config) {
211
163
  return String(error);
212
164
  }
213
165
  }
214
- function runRoute(ctx, args) {
166
+ function runRoute(ctx, args, pi) {
167
+ if (isInactive(ctx) && args[0] === 'init') {
168
+ const session = readSessionId(ctx);
169
+ if (!session.ok)
170
+ return {
171
+ output: session.reason,
172
+ exitCode: 1,
173
+ };
174
+ const event = sessionStartsById.get(session.sessionId);
175
+ if (event === undefined)
176
+ return {
177
+ output: INITIALIZATION_PENDING_REASON,
178
+ exitCode: 1,
179
+ };
180
+ initializationBySession.set(session.sessionId, { type: 'initializing' });
181
+ const failure = initializeSession(event, ctx, pi, session.sessionId);
182
+ if (failure !== undefined)
183
+ return {
184
+ output: markInitializationFailed(ctx, session.sessionId, failure),
185
+ exitCode: 1,
186
+ };
187
+ initializationBySession.set(session.sessionId, { type: 'ready' });
188
+ }
215
189
  const notReady = readinessFailure(ctx);
216
190
  if (notReady !== undefined)
217
191
  return {
@@ -248,6 +222,19 @@ export function createPiWorkflowExtension(config) {
248
222
  ctx.shutdown();
249
223
  return;
250
224
  }
225
+ sessionStartsById.set(session.sessionId, event);
226
+ try {
227
+ const hasPersistedWorkflow = useEngine(ctx, (engine) => engine.hasSessionStarted(session.sessionId));
228
+ const hasTranscriptWorkflow = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
229
+ if (!hasPersistedWorkflow && !hasTranscriptWorkflow) {
230
+ initializationBySession.set(session.sessionId, { type: 'inactive' });
231
+ return;
232
+ }
233
+ }
234
+ catch (error) {
235
+ markInitializationFailed(ctx, session.sessionId, String(error));
236
+ return;
237
+ }
251
238
  initializationBySession.set(session.sessionId, { type: 'initializing' });
252
239
  const failure = initializeSession(event, ctx, pi, session.sessionId);
253
240
  if (failure !== undefined) {
@@ -257,6 +244,8 @@ export function createPiWorkflowExtension(config) {
257
244
  initializationBySession.set(session.sessionId, { type: 'ready' });
258
245
  });
259
246
  pi.on('tool_call', (event, ctx) => {
247
+ if (isInactive(ctx))
248
+ return;
260
249
  const notReady = readinessFailure(ctx);
261
250
  if (notReady !== undefined)
262
251
  return {
@@ -286,6 +275,8 @@ export function createPiWorkflowExtension(config) {
286
275
  }
287
276
  });
288
277
  pi.on('input', (_event, ctx) => {
278
+ if (isInactive(ctx))
279
+ return;
289
280
  const notReady = readinessFailure(ctx);
290
281
  if (notReady === undefined)
291
282
  return;
@@ -293,7 +284,7 @@ export function createPiWorkflowExtension(config) {
293
284
  return { action: 'handled' };
294
285
  });
295
286
  pi.on('agent_settled', (_event, ctx) => {
296
- if (readinessFailure(ctx) !== undefined)
287
+ if (isInactive(ctx) || readinessFailure(ctx) !== undefined)
297
288
  return;
298
289
  const session = readSessionId(ctx);
299
290
  if (!session.ok)
@@ -321,6 +312,8 @@ export function createPiWorkflowExtension(config) {
321
312
  ctx.shutdown();
322
313
  return { cancel: true };
323
314
  }
315
+ if (isInactive(ctx))
316
+ return undefined;
324
317
  const notReady = readinessFailure(ctx);
325
318
  if (notReady !== undefined) {
326
319
  markInitializationFailed(ctx, session.sessionId, notReady);
@@ -339,7 +332,7 @@ export function createPiWorkflowExtension(config) {
339
332
  parameters: workflowToolParameters,
340
333
  executionMode: 'sequential',
341
334
  async execute(_toolCallId, parameters, _signal, _onUpdate, ctx) {
342
- const result = runRoute(ctx, [parameters.operation, ...(parameters.args ?? [])]);
335
+ const result = runRoute(ctx, [parameters.operation, ...(parameters.args ?? [])], pi);
343
336
  return {
344
337
  content: [{
345
338
  type: 'text',
@@ -354,7 +347,7 @@ export function createPiWorkflowExtension(config) {
354
347
  description: `Execute a deterministic workflow operation: /${commandName} <operation> [args]`,
355
348
  handler: async (rawArguments, ctx) => {
356
349
  try {
357
- const result = runRoute(ctx, parsePiCommandArguments(rawArguments));
350
+ const result = runRoute(ctx, parsePiCommandArguments(rawArguments), pi);
358
351
  notifyRouteResult(ctx, pi, result);
359
352
  }
360
353
  catch (error) {
@@ -5,6 +5,8 @@ import type { PlatformContext, PreToolUseHandlerConfig, RouteMap } from '@nt-ai-
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';
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.2",
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.5",
27
+ "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.5",
28
+ "@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.5"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "^0.84.4",