@nt-ai-lab/deterministic-agent-workflow-pi 0.5.3 → 0.5.5-issue-526.ce3247e.0

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.
@@ -1,6 +1,6 @@
1
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
- 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.";
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 */
6
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,18 +1,19 @@
1
1
  import { join } from 'node:path';
2
2
  import { WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
3
- import { createPreToolUseHandler, createWorkflowRunner, getRepositoryName, } from '@nt-ai-lab/deterministic-agent-workflow-cli';
3
+ import { createPreToolUseHandler, createWorkflowRunner, formatStopPreventionMessage, getRepositoryName, } from '@nt-ai-lab/deterministic-agent-workflow-cli';
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, readPiSessionMetadata, } from '../../../platform/infra/external-clients/pi/pi-session-file.js';
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';
10
11
  const PI_QUESTION_TOOL = 'question';
11
12
  const DEFAULT_COMMAND_NAME = 'workflow';
12
13
  const DEFAULT_TOOL_NAME = 'workflow';
13
14
  const INITIALIZATION_PENDING_REASON = 'Pi workflow initialization has not completed safely. Tool execution is blocked.';
14
15
  const INACTIVE_WORKFLOW_REASON = 'Pi workflow is inactive. Run the workflow init command before using workflow operations.';
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_IDLE_RECOVERY_MESSAGE = formatStopPreventionMessage();
16
17
  export const PI_SESSION_BRANCH_BLOCK_MESSAGE = 'Pi session tree navigation and forks are disabled while a workflow is active.';
17
18
  const workflowToolParameters = Type.Object({
18
19
  operation: Type.String({ description: 'Workflow operation, for example init or transition' }),
@@ -23,6 +24,7 @@ export function createPiWorkflowExtension(config) {
23
24
  const databasePath = resolveDatabasePath(config.databasePath);
24
25
  const commandName = config.commandName ?? DEFAULT_COMMAND_NAME;
25
26
  const toolName = config.toolName ?? DEFAULT_TOOL_NAME;
27
+ const ownership = createPiWorkflowSessionOwnership(databasePath);
26
28
  const initializationBySession = new Map();
27
29
  const sessionStartsById = new Map();
28
30
  const recoveredAssistantBySession = new Map();
@@ -35,6 +37,7 @@ export function createPiWorkflowExtension(config) {
35
37
  const runner = createWorkflowRunner({
36
38
  workflowDefinition: config.workflowDefinition,
37
39
  routes: config.routes,
40
+ unknownCommandMessage: config.unknownCommandMessage,
38
41
  bashForbidden: config.bashForbidden,
39
42
  isWriteAllowed: config.isWriteAllowed,
40
43
  questionToolName: PI_QUESTION_TOOL,
@@ -53,7 +56,7 @@ export function createPiWorkflowExtension(config) {
53
56
  appendToFile: () => undefined,
54
57
  now,
55
58
  transcriptReader: new PiTranscriptReader(() => ctx.sessionManager.getBranch()),
56
- sessionContext: { getMainSessionId: () => sessionId },
59
+ sessionContext: { getMainSessionId: () => ownership.delegatedParent(sessionId, store) ?? sessionId, },
57
60
  };
58
61
  }
59
62
  function useEngine(ctx, operation) {
@@ -62,6 +65,7 @@ export function createPiWorkflowExtension(config) {
62
65
  const engineDeps = buildEngineDeps(ctx, store);
63
66
  const now = engineDeps.now;
64
67
  const sessionId = ctx.sessionManager.getSessionId();
68
+ ownership.requireAccess(store, sessionId);
65
69
  const platform = {
66
70
  getPluginRoot: () => config.pluginRoot,
67
71
  now,
@@ -76,15 +80,6 @@ export function createPiWorkflowExtension(config) {
76
80
  store.db.close();
77
81
  }
78
82
  }
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
- }
88
83
  function markInitializationFailed(ctx, sessionId, detail) {
89
84
  return failSession(ctx, sessionId, `Pi workflow initialization failed: ${detail}`);
90
85
  }
@@ -113,23 +108,9 @@ export function createPiWorkflowExtension(config) {
113
108
  }
114
109
  function isInactive(ctx) {
115
110
  const session = readSessionId(ctx);
116
- return session.ok && initializationBySession.get(session.sessionId)?.type === 'inactive';
117
- }
118
- function parentSafetyFailure(event, ctx) {
119
- const parentSessionFile = ctx.sessionManager.getHeader()?.parentSession;
120
- if (parentSessionFile === undefined) {
121
- return event.reason === 'fork' ? 'Forked Pi session has no verifiable parent session file.' : undefined;
122
- }
123
- const parent = readPiSessionMetadata(parentSessionFile);
124
- const store = createStore(databasePath);
125
- try {
126
- return store.hasSessionStarted(parent.id) || parent.hasWorkflowMarker
127
- ? `Cannot fork Pi session ${parent.id}: its workflow is active.`
128
- : undefined;
129
- }
130
- finally {
131
- store.db.close();
132
- }
111
+ if (!session.ok)
112
+ return false;
113
+ return initializationBySession.get(session.sessionId)?.type === 'inactive';
133
114
  }
134
115
  function initializeSession(event, ctx, pi, sessionId) {
135
116
  try {
@@ -139,22 +120,29 @@ export function createPiWorkflowExtension(config) {
139
120
  return 'Pi session header does not match the active session UUID.';
140
121
  const activeBranchHasWorkflowMarker = hasPiWorkflowMarker(ctx.sessionManager.getBranch());
141
122
  const sessionHasWorkflowMarker = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
123
+ const inheritedWorkflowState = ownership.usesInheritedWorkflow(sessionId);
124
+ const delegatedParent = ownership.delegatedParent(sessionId);
142
125
  const result = useEngine(ctx, (engine) => {
143
126
  const sqliteHasWorkflowState = engine.hasSessionStarted(sessionId);
144
127
  if (!sqliteHasWorkflowState) {
145
- const parentFailure = parentSafetyFailure(event, ctx);
128
+ const parentFailure = ownership.parentSafetyFailure(event, ctx);
146
129
  if (parentFailure !== undefined)
147
130
  return {
148
131
  type: 'error',
149
132
  output: parentFailure,
150
133
  };
151
134
  }
152
- if (sessionHasWorkflowMarker && !activeBranchHasWorkflowMarker)
135
+ if (delegatedParent !== undefined && !sqliteHasWorkflowState)
136
+ return {
137
+ type: 'error',
138
+ output: `Pi parent session ${delegatedParent} has no persisted workflow.`,
139
+ };
140
+ if (!inheritedWorkflowState && sessionHasWorkflowMarker && !activeBranchHasWorkflowMarker)
153
141
  return {
154
142
  type: 'error',
155
143
  output: `The active Pi branch does not contain this session's ${PI_WORKFLOW_MARKER_CUSTOM_TYPE} marker.`,
156
144
  };
157
- if (activeBranchHasWorkflowMarker !== sqliteHasWorkflowState)
145
+ if (!inheritedWorkflowState && activeBranchHasWorkflowMarker !== sqliteHasWorkflowState)
158
146
  return {
159
147
  type: 'error',
160
148
  output: `Pi transcript and SQLite workflow state disagree for session ${sessionId}.`,
@@ -243,7 +231,7 @@ export function createPiWorkflowExtension(config) {
243
231
  }
244
232
  sessionStartsById.set(session.sessionId, event);
245
233
  try {
246
- const hasPersistedWorkflow = hasPersistedWorkflowState(session.sessionId);
234
+ const hasPersistedWorkflow = ownership.hasPersistedWorkflow(session.sessionId);
247
235
  const hasTranscriptWorkflow = hasPiWorkflowMarker(ctx.sessionManager.getEntries());
248
236
  if (!hasPersistedWorkflow && !hasTranscriptWorkflow) {
249
237
  initializationBySession.set(session.sessionId, { type: 'inactive' });
@@ -317,7 +305,7 @@ export function createPiWorkflowExtension(config) {
317
305
  const result = useEngine(ctx, (engine) => engine.checkStopping(session.sessionId, 'stop'));
318
306
  if (result.type === 'blocked' && ctx.isIdle() && !ctx.hasPendingMessages()) {
319
307
  recoveredAssistantBySession.set(session.sessionId, settlement.id);
320
- pi.sendUserMessage(PI_IDLE_RECOVERY_MESSAGE);
308
+ pi.sendUserMessage(formatStopPreventionMessage(result.output, config.stopPreventionMessage));
321
309
  }
322
310
  }
323
311
  catch (error) {
@@ -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
1
  export { createPiWorkflowExtension, PI_IDLE_RECOVERY_MESSAGE, PI_SESSION_BRANCH_BLOCK_MESSAGE, } from './features/pi-extension/entrypoint/pi-workflow-extension';
2
2
  export type { PiWorkflowExtension, PiWorkflowExtensionConfig, } from './platform/domain/pi-workflow-extension-types';
3
+ export { resolvePiMainSessionId } from './platform/domain/pi-main-session';
4
+ export { refreshPiContextWindow } from './platform/infra/external-clients/pi/pi-context-window';
3
5
  export { PiTranscriptReader } from './platform/infra/external-clients/pi/pi-transcript-reader';
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,2 @@
1
+ /** @riviere-role domain-service */
2
+ export declare function resolvePiMainSessionId(currentSessionId: string, environment?: Readonly<Record<string, string | undefined>>): string;
@@ -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
+ }
@@ -26,9 +26,11 @@ export type PiSessionIdResult = {
26
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
27
  readonly workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation, TTransitionContext>;
28
28
  readonly routes: RouteMap<TWorkflow, TState>;
29
+ readonly unknownCommandMessage: string;
29
30
  readonly buildWorkflowDeps: (platform: PlatformContext) => TDeps;
30
31
  readonly pluginRoot: string;
31
32
  readonly databasePath?: string;
32
33
  readonly commandName?: string;
33
34
  readonly toolName?: string;
35
+ readonly stopPreventionMessage?: string;
34
36
  };
@@ -0,0 +1,3 @@
1
+ import { type AgentSession } from '@earendil-works/pi-coding-agent';
2
+ /** @riviere-role external-client-service */
3
+ export declare function refreshPiContextWindow(session: AgentSession, stateInstructions: string): void;
@@ -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
+ }
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",
3
+ "version": "0.5.5-issue-526.ce3247e.0",
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.6",
27
- "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.6",
28
- "@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.6"
26
+ "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.8-issue-526.ce3247e.0",
27
+ "@nt-ai-lab/deterministic-agent-workflow-cli": "0.4.8-issue-526.ce3247e.0",
28
+ "@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.8-issue-526.ce3247e.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "^0.84.4",