@nt-ai-lab/deterministic-agent-workflow-pi 0.5.5 → 0.5.6
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-automation.d.ts +22 -0
- package/dist/features/pi-extension/entrypoint/pi-workflow-automation.js +160 -0
- package/dist/features/pi-extension/entrypoint/pi-workflow-extension-platform.d.ts +1 -1
- package/dist/features/pi-extension/entrypoint/pi-workflow-extension.d.ts +1 -1
- package/dist/features/pi-extension/entrypoint/pi-workflow-extension.js +35 -34
- package/dist/features/pi-extension/entrypoint/pi-workflow-session-ownership.d.ts +12 -0
- package/dist/features/pi-extension/entrypoint/pi-workflow-session-ownership.js +72 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -0
- package/dist/platform/domain/pi-main-session.d.ts +2 -0
- package/dist/platform/domain/pi-main-session.js +15 -0
- package/dist/platform/domain/pi-workflow-extension-types.d.ts +16 -2
- package/dist/platform/infra/external-clients/pi/pi-context-window.d.ts +3 -0
- package/dist/platform/infra/external-clients/pi/pi-context-window.js +17 -0
- package/dist/platform/infra/external-clients/pi/pi-extension-context-window.d.ts +3 -0
- package/dist/platform/infra/external-clients/pi/pi-extension-context-window.js +77 -0
- package/package.json +4 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { PiWorkflowIdleContext } from '../../../platform/domain/pi-workflow-extension-types.js';
|
|
3
|
+
/** @riviere-role cli-entrypoint */
|
|
4
|
+
export declare function registerPiWorkflowAutomation<TState extends {
|
|
5
|
+
readonly currentStateMachineState: string;
|
|
6
|
+
}>(pi: ExtensionAPI, options: {
|
|
7
|
+
readonly databasePath: string;
|
|
8
|
+
readonly automation?: {
|
|
9
|
+
readonly ownsState: (state: TState) => boolean;
|
|
10
|
+
readonly onIdle: (context: PiWorkflowIdleContext<TState>) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
readonly isReady: (ctx: ExtensionContext) => boolean;
|
|
13
|
+
readonly getState: (ctx: ExtensionContext) => TState;
|
|
14
|
+
readonly runOperation: (ctx: ExtensionContext, args: readonly string[]) => {
|
|
15
|
+
readonly exitCode: number;
|
|
16
|
+
readonly output: string;
|
|
17
|
+
};
|
|
18
|
+
readonly fail: (ctx: ExtensionContext, reason: string) => void;
|
|
19
|
+
}): {
|
|
20
|
+
ownsState: (ctx: ExtensionContext) => boolean;
|
|
21
|
+
afterOperation: (ctx: ExtensionContext) => void;
|
|
22
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ReviewCoordinator } from '@nt-ai-lab/deterministic-agent-workflow-cli';
|
|
2
|
+
import { WorkflowStateError } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
3
|
+
import { createStore } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
|
|
4
|
+
import { createPiExtensionContextWindow } from '../../../platform/infra/external-clients/pi/pi-extension-context-window.js';
|
|
5
|
+
/** @riviere-role cli-entrypoint */
|
|
6
|
+
export function registerPiWorkflowAutomation(pi, options) {
|
|
7
|
+
const automation = options.automation;
|
|
8
|
+
if (automation === undefined)
|
|
9
|
+
return {
|
|
10
|
+
ownsState: () => false,
|
|
11
|
+
afterOperation: () => undefined,
|
|
12
|
+
};
|
|
13
|
+
const active = new Map();
|
|
14
|
+
const cancellations = new Map();
|
|
15
|
+
const refreshContext = createPiExtensionContextWindow(pi);
|
|
16
|
+
const fail = (ctx, error) => {
|
|
17
|
+
options.fail(ctx, `Workflow automation failed: ${String(error)}`);
|
|
18
|
+
};
|
|
19
|
+
const isReady = (ctx) => {
|
|
20
|
+
try {
|
|
21
|
+
return options.isReady(ctx);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
fail(ctx, error);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const ownsState = (ctx) => {
|
|
29
|
+
if (!isReady(ctx))
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return active.has(ctx.sessionManager.getSessionId()) || automation.ownsState(options.getState(ctx));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
fail(ctx, error);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const runIdle = async (ctx) => {
|
|
40
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
41
|
+
if (!isReady(ctx) || !ctx.isIdle() || active.has(sessionId) || !ownsState(ctx))
|
|
42
|
+
return;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
active.set(sessionId, controller);
|
|
45
|
+
const stops = new Set();
|
|
46
|
+
cancellations.set(sessionId, stops);
|
|
47
|
+
const requireActive = () => {
|
|
48
|
+
if (controller.signal.aborted || active.get(sessionId) !== controller ||
|
|
49
|
+
ctx.sessionManager.getSessionId() !== sessionId || !isReady(ctx)) {
|
|
50
|
+
throw new WorkflowStateError('The workflow automation context is no longer active.');
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const resume = {};
|
|
54
|
+
try {
|
|
55
|
+
await automation.onIdle({
|
|
56
|
+
sessionId,
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
workingDirectory: ctx.cwd,
|
|
59
|
+
getState: () => {
|
|
60
|
+
requireActive();
|
|
61
|
+
return options.getState(ctx);
|
|
62
|
+
},
|
|
63
|
+
runOperation: (operation, ...args) => {
|
|
64
|
+
requireActive();
|
|
65
|
+
const result = options.runOperation(ctx, [operation, ...args]);
|
|
66
|
+
if (result.exitCode !== 0)
|
|
67
|
+
throw new WorkflowStateError(result.output);
|
|
68
|
+
return result.output;
|
|
69
|
+
},
|
|
70
|
+
runReviews: async (request, client) => {
|
|
71
|
+
requireActive();
|
|
72
|
+
const store = createStore(options.databasePath);
|
|
73
|
+
const coordinator = new ReviewCoordinator({
|
|
74
|
+
store,
|
|
75
|
+
client,
|
|
76
|
+
now: () => new Date().toISOString()
|
|
77
|
+
});
|
|
78
|
+
const cancel = async () => {
|
|
79
|
+
const result = await coordinator.cancel(request.bundleId, 'Pi session shutdown');
|
|
80
|
+
if (result.type === 'failed')
|
|
81
|
+
throw new WorkflowStateError(result.reason);
|
|
82
|
+
};
|
|
83
|
+
stops.add(cancel);
|
|
84
|
+
try {
|
|
85
|
+
return await coordinator.run({
|
|
86
|
+
...request,
|
|
87
|
+
sessionId,
|
|
88
|
+
workingDirectory: ctx.cwd
|
|
89
|
+
}, options.getState(ctx).currentStateMachineState);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
stops.delete(cancel);
|
|
93
|
+
store.db.close();
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
resumeWithFreshContext: (instructions) => {
|
|
97
|
+
requireActive();
|
|
98
|
+
if (resume.instructions !== undefined)
|
|
99
|
+
throw new WorkflowStateError('Workflow automation already requested a fresh context.');
|
|
100
|
+
if (automation.ownsState(options.getState(ctx))) {
|
|
101
|
+
throw new WorkflowStateError('Cannot resume the conversational agent in a workflow-owned state.');
|
|
102
|
+
}
|
|
103
|
+
refreshContext(ctx, instructions);
|
|
104
|
+
resume.instructions = instructions;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
fail(ctx, error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
active.delete(sessionId);
|
|
114
|
+
cancellations.delete(sessionId);
|
|
115
|
+
}
|
|
116
|
+
if (!controller.signal.aborted && resume.instructions !== undefined) {
|
|
117
|
+
pi.sendUserMessage(resume.instructions);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
pi.on('session_shutdown', async (_event, ctx) => {
|
|
121
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
122
|
+
active.get(sessionId)?.abort();
|
|
123
|
+
const results = await Promise.allSettled([...cancellations.get(sessionId) ?? []].map((cancel) => cancel()));
|
|
124
|
+
for (const result of results) {
|
|
125
|
+
if (result.status === 'rejected')
|
|
126
|
+
fail(ctx, result.reason);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
pi.on('agent_settled', (_event, ctx) => runIdle(ctx));
|
|
130
|
+
pi.on('session_start', (_event, ctx) => runIdle(ctx));
|
|
131
|
+
pi.on('tool_call', (_event, ctx) => {
|
|
132
|
+
if (ownsState(ctx))
|
|
133
|
+
return {
|
|
134
|
+
block: true,
|
|
135
|
+
reason: 'The workflow owns this state; conversational tools are disabled.'
|
|
136
|
+
};
|
|
137
|
+
return undefined;
|
|
138
|
+
});
|
|
139
|
+
pi.on('input', (_event, ctx) => {
|
|
140
|
+
if (ownsState(ctx))
|
|
141
|
+
return { action: 'handled' };
|
|
142
|
+
return undefined;
|
|
143
|
+
});
|
|
144
|
+
pi.on('session_before_switch', (_event, ctx) => {
|
|
145
|
+
if (active.has(ctx.sessionManager.getSessionId()))
|
|
146
|
+
return { cancel: true };
|
|
147
|
+
return undefined;
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
ownsState,
|
|
151
|
+
afterOperation: (ctx) => {
|
|
152
|
+
if (!ownsState(ctx))
|
|
153
|
+
return;
|
|
154
|
+
if (ctx.isIdle())
|
|
155
|
+
void runIdle(ctx);
|
|
156
|
+
else
|
|
157
|
+
ctx.abort();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { RunnerResult } from '@nt-ai-lab/deterministic-agent-workflow-cli';
|
|
3
|
-
import type { PiSessionIdResult } from '../../../platform/domain/pi-workflow-extension-types';
|
|
3
|
+
import type { PiSessionIdResult } from '../../../platform/domain/pi-workflow-extension-types.js';
|
|
4
4
|
/** @riviere-role cli-entrypoint */
|
|
5
5
|
export declare function translationNote(toolName: string): string;
|
|
6
6
|
/** @riviere-role cli-entrypoint */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseWorkflowState, RehydratableWorkflow, TransitionContext } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
2
|
-
import type { PiWorkflowExtension, PiWorkflowExtensionConfig } from '../../../platform/domain/pi-workflow-extension-types';
|
|
2
|
+
import type { PiWorkflowExtension, PiWorkflowExtensionConfig } from '../../../platform/domain/pi-workflow-extension-types.js';
|
|
3
3
|
export declare const PI_IDLE_RECOVERY_MESSAGE: string;
|
|
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 */
|
|
@@ -4,9 +4,11 @@ import { createPreToolUseHandler, createWorkflowRunner, formatStopPreventionMess
|
|
|
4
4
|
import { createStore } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
|
|
5
5
|
import { Type } from 'typebox';
|
|
6
6
|
import { parsePiCommandArguments } from '../../../platform/domain/pi-command-arguments.js';
|
|
7
|
-
import { hasPiWorkflowMarker, PI_WORKFLOW_MARKER_CUSTOM_TYPE,
|
|
7
|
+
import { hasPiWorkflowMarker, PI_WORKFLOW_MARKER_CUSTOM_TYPE, } from '../../../platform/infra/external-clients/pi/pi-session-file.js';
|
|
8
8
|
import { getLatestPiAssistantSettlement, PiTranscriptReader, } from '../../../platform/infra/external-clients/pi/pi-transcript-reader.js';
|
|
9
9
|
import { notifyRouteResult, readSessionId, readWorkflowInstruction, requireSessionFile, resolveDatabasePath, translationNote, } from './pi-workflow-extension-platform.js';
|
|
10
|
+
import { createPiWorkflowSessionOwnership } from './pi-workflow-session-ownership.js';
|
|
11
|
+
import { registerPiWorkflowAutomation } from './pi-workflow-automation.js';
|
|
10
12
|
const PI_QUESTION_TOOL = 'question';
|
|
11
13
|
const DEFAULT_COMMAND_NAME = 'workflow';
|
|
12
14
|
const DEFAULT_TOOL_NAME = 'workflow';
|
|
@@ -23,6 +25,7 @@ export function createPiWorkflowExtension(config) {
|
|
|
23
25
|
const databasePath = resolveDatabasePath(config.databasePath);
|
|
24
26
|
const commandName = config.commandName ?? DEFAULT_COMMAND_NAME;
|
|
25
27
|
const toolName = config.toolName ?? DEFAULT_TOOL_NAME;
|
|
28
|
+
const ownership = createPiWorkflowSessionOwnership(databasePath);
|
|
26
29
|
const initializationBySession = new Map();
|
|
27
30
|
const sessionStartsById = new Map();
|
|
28
31
|
const recoveredAssistantBySession = new Map();
|
|
@@ -54,7 +57,7 @@ export function createPiWorkflowExtension(config) {
|
|
|
54
57
|
appendToFile: () => undefined,
|
|
55
58
|
now,
|
|
56
59
|
transcriptReader: new PiTranscriptReader(() => ctx.sessionManager.getBranch()),
|
|
57
|
-
sessionContext: { getMainSessionId: () => sessionId },
|
|
60
|
+
sessionContext: { getMainSessionId: () => ownership.delegatedParent(sessionId, store) ?? sessionId, },
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
function useEngine(ctx, operation) {
|
|
@@ -63,6 +66,7 @@ export function createPiWorkflowExtension(config) {
|
|
|
63
66
|
const engineDeps = buildEngineDeps(ctx, store);
|
|
64
67
|
const now = engineDeps.now;
|
|
65
68
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
69
|
+
ownership.requireAccess(store, sessionId);
|
|
66
70
|
const platform = {
|
|
67
71
|
getPluginRoot: () => config.pluginRoot,
|
|
68
72
|
now,
|
|
@@ -77,15 +81,6 @@ export function createPiWorkflowExtension(config) {
|
|
|
77
81
|
store.db.close();
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
|
-
function hasPersistedWorkflowState(sessionId) {
|
|
81
|
-
const store = createStore(databasePath);
|
|
82
|
-
try {
|
|
83
|
-
return store.hasSessionStarted(sessionId);
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
store.db.close();
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
84
|
function markInitializationFailed(ctx, sessionId, detail) {
|
|
90
85
|
return failSession(ctx, sessionId, `Pi workflow initialization failed: ${detail}`);
|
|
91
86
|
}
|
|
@@ -114,23 +109,9 @@ export function createPiWorkflowExtension(config) {
|
|
|
114
109
|
}
|
|
115
110
|
function isInactive(ctx) {
|
|
116
111
|
const session = readSessionId(ctx);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const parentSessionFile = ctx.sessionManager.getHeader()?.parentSession;
|
|
121
|
-
if (parentSessionFile === undefined) {
|
|
122
|
-
return event.reason === 'fork' ? 'Forked Pi session has no verifiable parent session file.' : undefined;
|
|
123
|
-
}
|
|
124
|
-
const parent = readPiSessionMetadata(parentSessionFile);
|
|
125
|
-
const store = createStore(databasePath);
|
|
126
|
-
try {
|
|
127
|
-
return store.hasSessionStarted(parent.id) || parent.hasWorkflowMarker
|
|
128
|
-
? `Cannot fork Pi session ${parent.id}: its workflow is active.`
|
|
129
|
-
: undefined;
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
store.db.close();
|
|
133
|
-
}
|
|
112
|
+
if (!session.ok)
|
|
113
|
+
return false;
|
|
114
|
+
return initializationBySession.get(session.sessionId)?.type === 'inactive';
|
|
134
115
|
}
|
|
135
116
|
function initializeSession(event, ctx, pi, sessionId) {
|
|
136
117
|
try {
|
|
@@ -140,22 +121,29 @@ export function createPiWorkflowExtension(config) {
|
|
|
140
121
|
return 'Pi session header does not match the active session UUID.';
|
|
141
122
|
const activeBranchHasWorkflowMarker = hasPiWorkflowMarker(ctx.sessionManager.getBranch());
|
|
142
123
|
const sessionHasWorkflowMarker = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
|
|
124
|
+
const inheritedWorkflowState = ownership.usesInheritedWorkflow(sessionId);
|
|
125
|
+
const delegatedParent = ownership.delegatedParent(sessionId);
|
|
143
126
|
const result = useEngine(ctx, (engine) => {
|
|
144
127
|
const sqliteHasWorkflowState = engine.hasSessionStarted(sessionId);
|
|
145
128
|
if (!sqliteHasWorkflowState) {
|
|
146
|
-
const parentFailure = parentSafetyFailure(event, ctx);
|
|
129
|
+
const parentFailure = ownership.parentSafetyFailure(event, ctx);
|
|
147
130
|
if (parentFailure !== undefined)
|
|
148
131
|
return {
|
|
149
132
|
type: 'error',
|
|
150
133
|
output: parentFailure,
|
|
151
134
|
};
|
|
152
135
|
}
|
|
153
|
-
if (
|
|
136
|
+
if (delegatedParent !== undefined && !sqliteHasWorkflowState)
|
|
137
|
+
return {
|
|
138
|
+
type: 'error',
|
|
139
|
+
output: `Pi parent session ${delegatedParent} has no persisted workflow.`,
|
|
140
|
+
};
|
|
141
|
+
if (!inheritedWorkflowState && sessionHasWorkflowMarker && !activeBranchHasWorkflowMarker)
|
|
154
142
|
return {
|
|
155
143
|
type: 'error',
|
|
156
144
|
output: `The active Pi branch does not contain this session's ${PI_WORKFLOW_MARKER_CUSTOM_TYPE} marker.`,
|
|
157
145
|
};
|
|
158
|
-
if (activeBranchHasWorkflowMarker !== sqliteHasWorkflowState)
|
|
146
|
+
if (!inheritedWorkflowState && activeBranchHasWorkflowMarker !== sqliteHasWorkflowState)
|
|
159
147
|
return {
|
|
160
148
|
type: 'error',
|
|
161
149
|
output: `Pi transcript and SQLite workflow state disagree for session ${sessionId}.`,
|
|
@@ -244,7 +232,7 @@ export function createPiWorkflowExtension(config) {
|
|
|
244
232
|
}
|
|
245
233
|
sessionStartsById.set(session.sessionId, event);
|
|
246
234
|
try {
|
|
247
|
-
const hasPersistedWorkflow =
|
|
235
|
+
const hasPersistedWorkflow = ownership.hasPersistedWorkflow(session.sessionId);
|
|
248
236
|
const hasTranscriptWorkflow = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
|
|
249
237
|
if (!hasPersistedWorkflow && !hasTranscriptWorkflow) {
|
|
250
238
|
initializationBySession.set(session.sessionId, { type: 'inactive' });
|
|
@@ -304,7 +292,7 @@ export function createPiWorkflowExtension(config) {
|
|
|
304
292
|
return { action: 'handled' };
|
|
305
293
|
});
|
|
306
294
|
pi.on('agent_settled', (_event, ctx) => {
|
|
307
|
-
if (isInactive(ctx) || readinessFailure(ctx) !== undefined)
|
|
295
|
+
if (isInactive(ctx) || readinessFailure(ctx) !== undefined || automation.ownsState(ctx))
|
|
308
296
|
return;
|
|
309
297
|
const session = readSessionId(ctx);
|
|
310
298
|
if (!session.ok)
|
|
@@ -353,6 +341,8 @@ export function createPiWorkflowExtension(config) {
|
|
|
353
341
|
executionMode: 'sequential',
|
|
354
342
|
async execute(_toolCallId, parameters, _signal, _onUpdate, ctx) {
|
|
355
343
|
const result = runRoute(ctx, [parameters.operation, ...(parameters.args ?? [])], pi);
|
|
344
|
+
if (result.exitCode === 0)
|
|
345
|
+
automation.afterOperation(ctx);
|
|
356
346
|
return {
|
|
357
347
|
content: [{
|
|
358
348
|
type: 'text',
|
|
@@ -368,12 +358,23 @@ export function createPiWorkflowExtension(config) {
|
|
|
368
358
|
handler: async (rawArguments, ctx) => {
|
|
369
359
|
try {
|
|
370
360
|
const result = runRoute(ctx, parsePiCommandArguments(rawArguments), pi);
|
|
371
|
-
|
|
361
|
+
if (result.exitCode === 0)
|
|
362
|
+
automation.afterOperation(ctx);
|
|
363
|
+
if (result.exitCode !== 0 || (!automation.ownsState(ctx) && readinessFailure(ctx) === undefined))
|
|
364
|
+
notifyRouteResult(ctx, pi, result);
|
|
372
365
|
}
|
|
373
366
|
catch (error) {
|
|
374
367
|
ctx.ui.notify(String(error), 'error');
|
|
375
368
|
}
|
|
376
369
|
},
|
|
377
370
|
});
|
|
371
|
+
const automation = registerPiWorkflowAutomation(pi, {
|
|
372
|
+
databasePath,
|
|
373
|
+
...(config.automation === undefined ? {} : { automation: config.automation }),
|
|
374
|
+
isReady: (ctx) => readinessFailure(ctx) === undefined && !ownership.usesInheritedWorkflow(ctx.sessionManager.getSessionId()),
|
|
375
|
+
getState: (ctx) => useEngine(ctx, (engine) => engine.getWorkflowState(ctx.sessionManager.getSessionId())),
|
|
376
|
+
runOperation: (ctx, args) => runRoute(ctx, args, pi),
|
|
377
|
+
fail: (ctx, reason) => markSafetyUnavailable(ctx, ctx.sessionManager.getSessionId(), reason),
|
|
378
|
+
});
|
|
378
379
|
};
|
|
379
380
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExtensionContext, SessionStartEvent } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { type SqliteEventStore } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
|
|
3
|
+
interface PiWorkflowSessionOwnership {
|
|
4
|
+
delegatedParent(sessionId: string, store?: SqliteEventStore): string | undefined;
|
|
5
|
+
hasPersistedWorkflow(sessionId: string): boolean;
|
|
6
|
+
usesInheritedWorkflow(sessionId: string): boolean;
|
|
7
|
+
parentSafetyFailure(event: SessionStartEvent, ctx: ExtensionContext): string | undefined;
|
|
8
|
+
requireAccess(store: SqliteEventStore, sessionId: string): void;
|
|
9
|
+
}
|
|
10
|
+
/** @riviere-role cli-entrypoint */
|
|
11
|
+
export declare function createPiWorkflowSessionOwnership(databasePath: string): PiWorkflowSessionOwnership;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createStore, } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
|
|
2
|
+
import { resolvePiMainSessionId } from '../../../platform/domain/pi-main-session.js';
|
|
3
|
+
import { readPiSessionMetadata } from '../../../platform/infra/external-clients/pi/pi-session-file.js';
|
|
4
|
+
/** @riviere-role cli-entrypoint */
|
|
5
|
+
export function createPiWorkflowSessionOwnership(databasePath) {
|
|
6
|
+
const delegatedParent = (sessionId, existingStore) => {
|
|
7
|
+
const store = existingStore ?? createStore(databasePath);
|
|
8
|
+
try {
|
|
9
|
+
if (store.hasSessionStarted(sessionId))
|
|
10
|
+
return undefined;
|
|
11
|
+
const mainSessionId = resolvePiMainSessionId(sessionId);
|
|
12
|
+
return mainSessionId === sessionId ? undefined : mainSessionId;
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
if (existingStore === undefined)
|
|
16
|
+
store.db.close();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const requireParent = (store, parentSessionId) => {
|
|
20
|
+
if (!store.hasSessionStarted(parentSessionId)) {
|
|
21
|
+
throw new TypeError(`Pi parent session ${parentSessionId} has no persisted workflow.`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
delegatedParent,
|
|
26
|
+
hasPersistedWorkflow(sessionId) {
|
|
27
|
+
const store = createStore(databasePath);
|
|
28
|
+
try {
|
|
29
|
+
const parentSessionId = delegatedParent(sessionId, store);
|
|
30
|
+
if (parentSessionId !== undefined) {
|
|
31
|
+
requireParent(store, parentSessionId);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return store.hasSessionStarted(sessionId);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
store.db.close();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
usesInheritedWorkflow(sessionId) {
|
|
41
|
+
const store = createStore(databasePath);
|
|
42
|
+
try {
|
|
43
|
+
return delegatedParent(sessionId, store) !== undefined;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
store.db.close();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
parentSafetyFailure(event, ctx) {
|
|
50
|
+
const parentSessionFile = ctx.sessionManager.getHeader()?.parentSession;
|
|
51
|
+
if (parentSessionFile === undefined) {
|
|
52
|
+
return event.reason === 'fork' ? 'Forked Pi session has no verifiable parent session file.' : undefined;
|
|
53
|
+
}
|
|
54
|
+
const parent = readPiSessionMetadata(parentSessionFile);
|
|
55
|
+
const store = createStore(databasePath);
|
|
56
|
+
try {
|
|
57
|
+
return store.hasSessionStarted(parent.id) || parent.hasWorkflowMarker
|
|
58
|
+
? `Cannot fork Pi session ${parent.id}: its workflow is active.`
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
store.db.close();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
requireAccess(store, sessionId) {
|
|
66
|
+
const parentSessionId = delegatedParent(sessionId, store);
|
|
67
|
+
if (parentSessionId !== undefined) {
|
|
68
|
+
requireParent(store, parentSessionId);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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 {
|
|
1
|
+
export { createPiWorkflowExtension, PI_IDLE_RECOVERY_MESSAGE, PI_SESSION_BRANCH_BLOCK_MESSAGE, } from './features/pi-extension/entrypoint/pi-workflow-extension.js';
|
|
2
|
+
export type { PiWorkflowExtension, PiWorkflowExtensionConfig, PiWorkflowIdleContext, } from './platform/domain/pi-workflow-extension-types.js';
|
|
3
|
+
export { resolvePiMainSessionId } from './platform/domain/pi-main-session.js';
|
|
4
|
+
export { refreshPiContextWindow } from './platform/infra/external-clients/pi/pi-context-window.js';
|
|
5
|
+
export { PiTranscriptReader } from './platform/infra/external-clients/pi/pi-transcript-reader.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { createPiWorkflowExtension, PI_IDLE_RECOVERY_MESSAGE, PI_SESSION_BRANCH_BLOCK_MESSAGE, } from './features/pi-extension/entrypoint/pi-workflow-extension.js';
|
|
2
|
+
export { resolvePiMainSessionId } from './platform/domain/pi-main-session.js';
|
|
3
|
+
export { refreshPiContextWindow } from './platform/infra/external-clients/pi/pi-context-window.js';
|
|
2
4
|
export { PiTranscriptReader } from './platform/infra/external-clients/pi/pi-transcript-reader.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const PI_SUBAGENT_PARENT_SESSION = 'PI_SUBAGENT_PARENT_SESSION';
|
|
2
|
+
/** @riviere-role domain-service */
|
|
3
|
+
export function resolvePiMainSessionId(currentSessionId, environment = process.env) {
|
|
4
|
+
const current = currentSessionId.trim();
|
|
5
|
+
if (current.length === 0)
|
|
6
|
+
throw new TypeError('Pi returned an empty session UUID.');
|
|
7
|
+
const rawParent = environment[PI_SUBAGENT_PARENT_SESSION];
|
|
8
|
+
if (rawParent === undefined)
|
|
9
|
+
return current;
|
|
10
|
+
const parent = rawParent.trim();
|
|
11
|
+
if (parent.length === 0) {
|
|
12
|
+
throw new TypeError(`${PI_SUBAGENT_PARENT_SESSION} must contain a non-empty session UUID.`);
|
|
13
|
+
}
|
|
14
|
+
return parent;
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import type { BaseWorkflowState, RehydratableWorkflow, TransitionContext, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
3
|
-
import type { PlatformContext, PreToolUseHandlerConfig, RouteMap } from '@nt-ai-lab/deterministic-agent-workflow-cli';
|
|
2
|
+
import type { BaseWorkflowState, RehydratableWorkflow, ReviewBundleRequest, TransitionContext, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
|
|
3
|
+
import type { PlatformContext, PreToolUseHandlerConfig, RouteMap, ReviewAgentClient, ReviewCoordinatorResult } 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 */
|
|
@@ -23,6 +23,16 @@ export type PiSessionIdResult = {
|
|
|
23
23
|
readonly reason: string;
|
|
24
24
|
};
|
|
25
25
|
/** @riviere-role value-object */
|
|
26
|
+
export interface PiWorkflowIdleContext<TState> {
|
|
27
|
+
readonly sessionId: string;
|
|
28
|
+
readonly signal: AbortSignal;
|
|
29
|
+
readonly workingDirectory: string;
|
|
30
|
+
getState(): TState;
|
|
31
|
+
runOperation(operation: string, ...args: readonly string[]): string;
|
|
32
|
+
runReviews(request: Omit<ReviewBundleRequest, 'sessionId' | 'workingDirectory'>, client: ReviewAgentClient): Promise<ReviewCoordinatorResult>;
|
|
33
|
+
resumeWithFreshContext(stateInstructions: string): void;
|
|
34
|
+
}
|
|
35
|
+
/** @riviere-role value-object */
|
|
26
36
|
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
37
|
readonly workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation, TTransitionContext>;
|
|
28
38
|
readonly routes: RouteMap<TWorkflow, TState>;
|
|
@@ -33,4 +43,8 @@ export type PiWorkflowExtensionConfig<TWorkflow extends RehydratableWorkflow<TSt
|
|
|
33
43
|
readonly commandName?: string;
|
|
34
44
|
readonly toolName?: string;
|
|
35
45
|
readonly stopPreventionMessage?: string;
|
|
46
|
+
readonly automation?: {
|
|
47
|
+
readonly ownsState: (state: TState) => boolean;
|
|
48
|
+
readonly onIdle: (context: PiWorkflowIdleContext<TState>) => Promise<void>;
|
|
49
|
+
};
|
|
36
50
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { estimateTokens, } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
/** @riviere-role external-client-service */
|
|
3
|
+
export function refreshPiContextWindow(session, stateInstructions) {
|
|
4
|
+
if (stateInstructions.trim().length === 0) {
|
|
5
|
+
throw new TypeError('Pi context state instructions must not be empty.');
|
|
6
|
+
}
|
|
7
|
+
if (!session.isIdle) {
|
|
8
|
+
throw new TypeError('Cannot refresh Pi context until the session is idle.');
|
|
9
|
+
}
|
|
10
|
+
if (!session.sessionManager.isPersisted()) {
|
|
11
|
+
throw new TypeError('Cannot refresh Pi context without a persisted session.');
|
|
12
|
+
}
|
|
13
|
+
const tokensBefore = session.messages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
14
|
+
const boundary = session.sessionManager.appendCustomEntry('pi-context-window-boundary');
|
|
15
|
+
session.sessionManager.appendCompaction(stateInstructions, boundary, tokensBefore);
|
|
16
|
+
session.agent.state.messages = session.sessionManager.buildSessionContext().messages;
|
|
17
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { buildSessionContext, } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
const BOUNDARY_TYPE = 'pi-context-window-state';
|
|
4
|
+
function readBoundary(ctx) {
|
|
5
|
+
const branch = ctx.sessionManager.getBranch();
|
|
6
|
+
const boundary = branch.findLast((entry) => entry.type === 'custom' && entry.customType === BOUNDARY_TYPE);
|
|
7
|
+
if (boundary === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (boundary.type !== 'custom' || typeof boundary.data !== 'string' || boundary.data.trim().length === 0) {
|
|
10
|
+
throw new TypeError('Pi context boundary has invalid state instructions.');
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
boundary,
|
|
14
|
+
instructions: boundary.data,
|
|
15
|
+
entries: branch.slice(branch.indexOf(boundary))
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** @riviere-role external-client-service */
|
|
19
|
+
export function createPiExtensionContextWindow(pi) {
|
|
20
|
+
pi.on('context', (_event, ctx) => {
|
|
21
|
+
try {
|
|
22
|
+
const window = readBoundary(ctx);
|
|
23
|
+
if (window === undefined)
|
|
24
|
+
return;
|
|
25
|
+
return {
|
|
26
|
+
messages: [{
|
|
27
|
+
role: 'user',
|
|
28
|
+
content: window.instructions,
|
|
29
|
+
timestamp: Date.parse(window.boundary.timestamp),
|
|
30
|
+
}, ...buildSessionContext(window.entries).messages],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
ctx.abort();
|
|
35
|
+
ctx.ui.notify(`Pi context safety is unavailable: ${String(error)}`, 'error');
|
|
36
|
+
ctx.shutdown();
|
|
37
|
+
return { messages: [] };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
pi.on('session_before_compact', (event, ctx) => {
|
|
41
|
+
try {
|
|
42
|
+
const window = readBoundary(ctx);
|
|
43
|
+
if (window === undefined)
|
|
44
|
+
return;
|
|
45
|
+
return {
|
|
46
|
+
compaction: {
|
|
47
|
+
summary: window.instructions,
|
|
48
|
+
firstKeptEntryId: window.boundary.id,
|
|
49
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
ctx.abort();
|
|
55
|
+
ctx.ui.notify(`Pi context safety is unavailable: ${String(error)}`, 'error');
|
|
56
|
+
ctx.shutdown();
|
|
57
|
+
return { cancel: true };
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return (ctx, instructions) => {
|
|
61
|
+
if (instructions.trim().length === 0) {
|
|
62
|
+
throw new TypeError('Pi context state instructions must not be empty.');
|
|
63
|
+
}
|
|
64
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
65
|
+
throw new TypeError('Cannot refresh Pi context until the session is idle without pending messages.');
|
|
66
|
+
}
|
|
67
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
68
|
+
if (sessionFile === undefined || !existsSync(sessionFile)) {
|
|
69
|
+
throw new TypeError('Cannot refresh Pi context without a persisted session.');
|
|
70
|
+
}
|
|
71
|
+
pi.appendEntry(BOUNDARY_TYPE, instructions);
|
|
72
|
+
const window = readBoundary(ctx);
|
|
73
|
+
if (window?.instructions !== instructions || window.boundary.id !== ctx.sessionManager.getLeafId()) {
|
|
74
|
+
throw new TypeError('Pi did not persist the requested context boundary.');
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nt-ai-lab/deterministic-agent-workflow-pi",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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-engine": "0.4.
|
|
28
|
-
"@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.
|
|
26
|
+
"@nt-ai-lab/deterministic-agent-workflow-cli": "0.4.9",
|
|
27
|
+
"@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.9",
|
|
28
|
+
"@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.9"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|