@nt-ai-lab/deterministic-agent-workflow-pi 0.5.4 → 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.
@@ -4,9 +4,10 @@ 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, 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';
@@ -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' });
@@ -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,6 +26,7 @@ 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;
@@ -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.4",
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.7",
27
- "@nt-ai-lab/deterministic-agent-workflow-engine": "0.4.7",
28
- "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.4.7"
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",