praxis-agent 0.55.0 → 0.55.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.
@@ -1,36 +1,150 @@
1
1
  import { AgentRunCancelledError, ModelProviderError, } from '../core/runtime.js';
2
- export class TurnTerminalController {
3
- sink;
4
- terminal = false;
5
- constructor(sink) {
6
- this.sink = sink;
7
- }
8
- emit = (event) => {
9
- if (event.type === 'state' &&
10
- (event.state === 'completed' ||
11
- event.state === 'cancelled' ||
12
- event.state === 'failed')) {
13
- return;
2
+ import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
3
+ /** Owns the lifecycle and active-turn coordination for one session service. */
4
+ export class TurnCoordinator {
5
+ options;
6
+ activeTurns = new Map();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ async run(request, work) {
11
+ const { sessionId } = request.activation;
12
+ const mailbox = request.submission.kind === 'shell'
13
+ ? undefined
14
+ : new ActiveTurnInputMailbox(this.options.createSteeringId);
15
+ const record = {
16
+ ...(mailbox ? { mailbox } : {}),
17
+ terminal: false,
18
+ };
19
+ let terminalState = 'failed';
20
+ let pendingFailure;
21
+ const scope = {
22
+ emit: (event) => {
23
+ if (event.type === 'state' &&
24
+ (event.state === 'completed' ||
25
+ event.state === 'failed' ||
26
+ event.state === 'cancelled')) {
27
+ return;
28
+ }
29
+ this.options.eventSink(event);
30
+ },
31
+ ...(mailbox ? { steering: mailbox } : {}),
32
+ };
33
+ try {
34
+ this.validateRequest(request);
35
+ if (this.activeTurns.has(sessionId)) {
36
+ throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
37
+ }
38
+ this.activeTurns.set(sessionId, record);
39
+ const result = await work(scope);
40
+ terminalState = 'completed';
41
+ this.transition(record, 'completed');
42
+ return result;
43
+ }
44
+ catch (error) {
45
+ if (!record.terminal) {
46
+ terminalState = this.terminalState(error, request.signal);
47
+ this.transition(record, terminalState);
48
+ }
49
+ throw error;
50
+ }
51
+ finally {
52
+ try {
53
+ if (mailbox) {
54
+ pendingFailure = this.rejectPending(mailbox.close(), terminalState === 'cancelled' ? 'cancelled' : 'failed');
55
+ }
56
+ }
57
+ finally {
58
+ if (this.activeTurns.get(sessionId) === record) {
59
+ this.activeTurns.delete(sessionId);
60
+ }
61
+ }
62
+ if (pendingFailure) {
63
+ // A rejected-input sink failure intentionally retains its prior precedence.
64
+ // eslint-disable-next-line no-unsafe-finally -- compatibility is covered by the sink-error regression
65
+ throw pendingFailure.error;
66
+ }
14
67
  }
15
- this.sink(event);
16
- };
17
- complete() {
18
- this.transition('completed');
19
68
  }
20
- fail(error, signal) {
21
- if (this.terminal)
22
- return;
23
- const cancelled = signal?.aborted === true ||
69
+ steer(sessionId, content) {
70
+ const active = this.activeTurns.get(sessionId);
71
+ if (!active)
72
+ return { kind: 'no-active-turn' };
73
+ if (!active.mailbox)
74
+ return { kind: 'not-steerable' };
75
+ const result = active.mailbox.enqueue(content);
76
+ if (result.kind === 'accepted' || result.kind === 'empty')
77
+ return result;
78
+ return { kind: 'turn-completing' };
79
+ }
80
+ withdrawSteering(sessionId, id) {
81
+ const active = this.activeTurns.get(sessionId);
82
+ if (!active)
83
+ return { kind: 'no-active-turn' };
84
+ if (!active.mailbox)
85
+ return { kind: 'not-steerable' };
86
+ const result = active.mailbox.withdraw(id);
87
+ return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
88
+ }
89
+ close() {
90
+ let firstFailure;
91
+ for (const active of this.activeTurns.values()) {
92
+ if (!active.mailbox)
93
+ continue;
94
+ const failure = this.rejectPending(active.mailbox.close(), 'closed');
95
+ firstFailure ??= failure;
96
+ }
97
+ if (firstFailure)
98
+ throw firstFailure.error;
99
+ }
100
+ validateRequest(request) {
101
+ const { activation, submission } = request;
102
+ const prompt = submission.kind === 'shell'
103
+ ? `! ${submission.command}`
104
+ : submission.kind === 'retry'
105
+ ? submission.prompt
106
+ : submission.text;
107
+ const images = submission.kind === 'prompt' ? (submission.images ?? []) : [];
108
+ const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
109
+ if (prompt.length === 0 && images.length === 0 && documents.length === 0) {
110
+ throw new Error('Prompt must not be empty');
111
+ }
112
+ if (activation.name !== undefined && activation.name.length === 0) {
113
+ throw new Error('Session name must not be empty');
114
+ }
115
+ if (submission.kind === 'shell' && submission.command.trim().length === 0) {
116
+ throw new Error('Shell command must not be empty');
117
+ }
118
+ }
119
+ terminalState(error, signal) {
120
+ return signal?.aborted === true ||
24
121
  error instanceof AgentRunCancelledError ||
25
- (error instanceof ModelProviderError && error.kind === 'cancelled');
26
- this.transition(cancelled ? 'cancelled' : 'failed');
122
+ (error instanceof ModelProviderError && error.kind === 'cancelled')
123
+ ? 'cancelled'
124
+ : 'failed';
125
+ }
126
+ transition(record, state) {
127
+ if (record.terminal)
128
+ return;
129
+ record.terminal = true;
130
+ this.options.eventSink({ type: 'state', state });
27
131
  }
28
- transition(state) {
29
- if (this.terminal) {
30
- throw new Error('Turn terminal transition already emitted');
132
+ rejectPending(items, reason) {
133
+ let firstFailure;
134
+ for (const item of items) {
135
+ try {
136
+ this.options.eventSink({
137
+ type: 'user-input-rejected',
138
+ id: item.id,
139
+ content: item.content,
140
+ reason,
141
+ });
142
+ }
143
+ catch (error) {
144
+ firstFailure ??= { error };
145
+ }
31
146
  }
32
- this.terminal = true;
33
- this.sink({ type: 'state', state });
147
+ return firstFailure;
34
148
  }
35
149
  }
36
150
  //# sourceMappingURL=turn-lifecycle.js.map
@@ -0,0 +1,62 @@
1
+ import type { ModelMessage } from '../core/runtime.js';
2
+ import type { TranscriptEvent } from '../core/transcript-event.js';
3
+ import type { NativeTranscriptEntry } from '../native/schema.js';
4
+ import type { NativeCompactionAppend, NativeInterruption, NativeMessageAppend, NativeSessionTranscriptLease } from './native-session-transcript.js';
5
+ import type { NativeTranscriptTail } from '../persistence/native-transcript-store.js';
6
+ export interface TurnPersistenceView {
7
+ readonly projectionEntries: readonly NativeTranscriptEntry[];
8
+ readonly projectionTail: NativeTranscriptTail;
9
+ readonly activeEvents: readonly TranscriptEvent[];
10
+ readonly activeMessages: readonly ModelMessage[];
11
+ readonly interruption: NativeInterruption;
12
+ }
13
+ export type TurnPersistenceCommand = {
14
+ readonly kind: 'projection';
15
+ readonly entries: readonly NativeTranscriptEntry[];
16
+ } | {
17
+ readonly kind: 'messages';
18
+ readonly input: NativeMessageAppend;
19
+ readonly projectionEntries?: readonly NativeTranscriptEntry[];
20
+ } | {
21
+ readonly kind: 'tool-execution-started';
22
+ readonly callId: string;
23
+ } | {
24
+ readonly kind: 'tool-completion';
25
+ readonly input: Parameters<NativeSessionTranscriptLease['appendToolCompletion']>[0];
26
+ } | {
27
+ readonly kind: 'compaction';
28
+ readonly input: NativeCompactionAppend;
29
+ };
30
+ export type TurnPersistenceReceipt = {
31
+ readonly kind: 'projection';
32
+ readonly lastProjectionId: string | null;
33
+ } | {
34
+ readonly kind: 'messages';
35
+ readonly eventId: string;
36
+ } | {
37
+ readonly kind: 'tool-execution-started';
38
+ readonly callId: string;
39
+ } | {
40
+ readonly kind: 'tool-completion';
41
+ readonly callId: string;
42
+ } | {
43
+ readonly kind: 'compaction';
44
+ readonly boundaryId: string;
45
+ readonly summaryId: string;
46
+ };
47
+ export declare class TurnPersistence {
48
+ private readonly input;
49
+ private projectionEntries;
50
+ private projectionTail;
51
+ private commitQueue;
52
+ constructor(input: {
53
+ readonly native: NativeSessionTranscriptLease;
54
+ readonly initialProjectionEntries?: readonly NativeTranscriptEntry[];
55
+ });
56
+ view(): TurnPersistenceView;
57
+ refresh(): TurnPersistenceView;
58
+ commit(command: TurnPersistenceCommand): Promise<TurnPersistenceReceipt>;
59
+ private commitCommand;
60
+ private stageProjection;
61
+ }
62
+ //# sourceMappingURL=turn-persistence.d.ts.map
@@ -0,0 +1,114 @@
1
+ import { projectNativeSessionEntries } from './native-session-projection.js';
2
+ const emptyProjectionTail = () => ({
3
+ byteLength: 0,
4
+ lastLineHash: null,
5
+ lastEventId: null,
6
+ newlineTerminated: true,
7
+ });
8
+ export class TurnPersistence {
9
+ input;
10
+ projectionEntries;
11
+ projectionTail = emptyProjectionTail();
12
+ commitQueue = Promise.resolve();
13
+ constructor(input) {
14
+ this.input = input;
15
+ this.projectionEntries = structuredClone(input.initialProjectionEntries
16
+ ? [...input.initialProjectionEntries]
17
+ : projectNativeSessionEntries(input.native.activeEvents()));
18
+ }
19
+ view() {
20
+ return {
21
+ projectionEntries: structuredClone(this.projectionEntries),
22
+ projectionTail: structuredClone(this.projectionTail),
23
+ activeEvents: structuredClone(this.input.native.activeEvents()),
24
+ activeMessages: structuredClone(this.input.native.activeMessages()),
25
+ interruption: structuredClone(this.input.native.interruption()),
26
+ };
27
+ }
28
+ refresh() {
29
+ this.projectionEntries = structuredClone(projectNativeSessionEntries(this.input.native.activeEvents()));
30
+ return this.view();
31
+ }
32
+ commit(command) {
33
+ let ownership;
34
+ try {
35
+ ownership = { ok: true, command: structuredClone(command) };
36
+ }
37
+ catch (error) {
38
+ ownership = { ok: false, error };
39
+ }
40
+ const operation = this.commitQueue.then(() => {
41
+ if (!ownership.ok)
42
+ throw ownership.error;
43
+ return this.commitCommand(ownership.command);
44
+ });
45
+ this.commitQueue = operation.then(() => undefined, () => undefined);
46
+ return operation;
47
+ }
48
+ async commitCommand(command) {
49
+ switch (command.kind) {
50
+ case 'projection': {
51
+ const staged = this.stageProjection(command.entries);
52
+ this.projectionEntries = staged.entries;
53
+ this.projectionTail = staged.tail;
54
+ const lastProjectionId = [...command.entries]
55
+ .reverse()
56
+ .find((entry) => typeof entry.uuid === 'string')?.uuid;
57
+ return {
58
+ kind: 'projection',
59
+ lastProjectionId: typeof lastProjectionId === 'string' ? lastProjectionId : null,
60
+ };
61
+ }
62
+ case 'messages': {
63
+ if (command.input.messages.length === 0)
64
+ throw new Error('native transcript cannot append empty messages');
65
+ const staged = command.projectionEntries === undefined
66
+ ? undefined
67
+ : this.stageProjection(command.projectionEntries);
68
+ const eventId = await this.input.native.appendMessages(command.input);
69
+ if (staged) {
70
+ this.projectionEntries = staged.entries;
71
+ this.projectionTail = staged.tail;
72
+ }
73
+ return { kind: 'messages', eventId };
74
+ }
75
+ case 'tool-execution-started':
76
+ await this.input.native.beginToolExecution(command.callId);
77
+ return { kind: 'tool-execution-started', callId: command.callId };
78
+ case 'tool-completion':
79
+ await this.input.native.appendToolCompletion(command.input);
80
+ return { kind: 'tool-completion', callId: command.input.callId };
81
+ case 'compaction': {
82
+ const result = await this.input.native.appendCompaction(command.input);
83
+ return { kind: 'compaction', ...result };
84
+ }
85
+ default:
86
+ throw new Error('Unknown turn persistence command');
87
+ }
88
+ }
89
+ stageProjection(entries) {
90
+ if (entries.length === 0)
91
+ throw new Error('Cannot append an empty projection');
92
+ const stagedEntries = [...this.projectionEntries, ...entries];
93
+ const lastUuidValue = [...entries]
94
+ .reverse()
95
+ .find((entry) => typeof entry.uuid === 'string')?.uuid;
96
+ const lastUuid = typeof lastUuidValue === 'string' ? lastUuidValue : undefined;
97
+ const byteLength = this.projectionTail.byteLength + entries.length;
98
+ return {
99
+ entries: stagedEntries,
100
+ tail: {
101
+ ...this.projectionTail,
102
+ byteLength,
103
+ lastLineHash: `projection:${byteLength}`,
104
+ lastEventId: typeof lastUuid === 'string'
105
+ ? lastUuid
106
+ : this.projectionTail.lastEventId,
107
+ ...(this.projectionTail.branchParentId === undefined
108
+ ? {}
109
+ : { branchParentId: null }),
110
+ },
111
+ };
112
+ }
113
+ }
114
+ //# sourceMappingURL=turn-persistence.js.map
@@ -243,11 +243,9 @@ export declare function parseCliInvocation(argv: readonly string[]): CliInvocati
243
243
  export declare function readStreamJsonMessages(input: AsyncIterable<string | Uint8Array>): AsyncGenerator<StreamJsonMessage>;
244
244
  export declare function readStreamUserMessages(input: AsyncIterable<string | Uint8Array>): AsyncGenerator<StreamUserMessage>;
245
245
  export declare class StreamJsonOutput {
246
- private readonly write;
246
+ private readonly emitRaw;
247
247
  private readonly info;
248
248
  private readonly sessionId;
249
- private readonly includePartialMessages;
250
- private readonly includeHookEvents;
251
249
  private turnId;
252
250
  private turnText;
253
251
  private turnThinking;
@@ -263,7 +261,17 @@ export declare class StreamJsonOutput {
263
261
  private modelTurns;
264
262
  private compacting;
265
263
  private sessionState;
266
- constructor(write: (value: unknown) => void, info: CliRuntimeInfo, sessionId: string, includePartialMessages: boolean, includeHookEvents?: boolean);
264
+ private readonly emitSessionStateEvents;
265
+ private partialEvents;
266
+ private pendingPartialStop;
267
+ constructor(emitRaw: (value: unknown) => void, info: CliRuntimeInfo, sessionId: string, options: {
268
+ includePartialMessages: boolean;
269
+ includeHookEvents?: boolean;
270
+ emitSessionStateEvents?: boolean;
271
+ });
272
+ private readonly includePartialMessages;
273
+ private readonly includeHookEvents;
274
+ private write;
267
275
  init(): void;
268
276
  replayUser(message: StreamUserMessage['message']): void;
269
277
  /**
@@ -282,11 +290,15 @@ export declare class StreamJsonOutput {
282
290
  error(message: string, startedAt: number): void;
283
291
  private onEvent;
284
292
  private writeSessionState;
293
+ private writeTerminalIdle;
285
294
  private ensureTurn;
286
295
  private discardTurn;
287
296
  private startTurn;
297
+ private writePartialMessageStart;
288
298
  private partialText;
299
+ private beginPartialBlock;
289
300
  private finishPartial;
301
+ private finishPartialTail;
290
302
  private flushAssistant;
291
303
  private finishTurn;
292
304
  }