@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.
- package/dist/features/pi-extension/entrypoint/pi-workflow-extension-platform.d.ts +15 -0
- package/dist/features/pi-extension/entrypoint/pi-workflow-extension-platform.js +67 -0
- package/dist/features/pi-extension/entrypoint/pi-workflow-extension.js +55 -62
- package/dist/platform/domain/pi-workflow-extension-types.d.ts +2 -0
- package/package.json +4 -4
|
@@ -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) =>
|
|
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.
|
|
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.
|
|
27
|
-
"@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.
|
|
28
|
-
"@nt-ai-lab/deterministic-agent-workflow-engine": "0.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",
|