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.
- package/README.md +1 -1
- package/dist/application/context-engine.js +4 -1
- package/dist/application/context-preparation.d.ts +60 -0
- package/dist/application/context-preparation.js +139 -0
- package/dist/application/session-service.d.ts +1 -8
- package/dist/application/session-service.js +362 -844
- package/dist/application/turn-accounting.d.ts +74 -0
- package/dist/application/turn-accounting.js +444 -0
- package/dist/application/turn-lifecycle.d.ts +21 -9
- package/dist/application/turn-lifecycle.js +141 -27
- package/dist/application/turn-persistence.d.ts +62 -0
- package/dist/application/turn-persistence.js +114 -0
- package/dist/cli/protocol.d.ts +16 -4
- package/dist/cli/protocol.js +136 -100
- package/dist/cli-runtime.js +10 -2
- package/package.json +1 -1
|
@@ -1,36 +1,150 @@
|
|
|
1
1
|
import { AgentRunCancelledError, ModelProviderError, } from '../core/runtime.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
package/dist/cli/protocol.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|