praxis-agent 0.54.0 → 0.55.1
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 +11 -6
- 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/native-sidechain-transcript.d.ts +2 -0
- package/dist/application/native-sidechain-transcript.js +3 -0
- package/dist/application/session-service.d.ts +1 -1
- package/dist/application/session-service.js +99 -206
- package/dist/application/subagent-service.d.ts +2 -0
- package/dist/application/subagent-service.js +28 -12
- package/dist/application/team-agent-runtime.d.ts +1 -0
- package/dist/application/team-agent-runtime.js +3 -0
- package/dist/application/turn-lifecycle.d.ts +21 -9
- package/dist/application/turn-lifecycle.js +141 -27
- package/dist/cli-runtime.js +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -253,11 +253,16 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
253
253
|
recovery for malformed streamed tool arguments without tool execution or
|
|
254
254
|
lost resumability, one default-on bounded Anthropic non-streaming replay for
|
|
255
255
|
eligible stream/idle failures without exposing failed-attempt output, and
|
|
256
|
-
token-only/no-API-dollar accounting for subscription runs.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
256
|
+
token-only/no-API-dollar accounting for subscription runs. Each main user
|
|
257
|
+
Turn and independent auxiliary Agent, Workflow, Team, recovery, or memory
|
|
258
|
+
Turn receives its own provider client. Session-memory requests reuse a
|
|
259
|
+
completion-scoped client but restart routing from primary for each request;
|
|
260
|
+
auto-mode critic and eval-judge requests remain independently constructed
|
|
261
|
+
one-shot clients. Failed attempts stay buffered, and the first successful
|
|
262
|
+
route, whether primary or fallback, stays sticky only through that logical
|
|
263
|
+
Turn's tool continuations; incompatible routes fail closed, and the next
|
|
264
|
+
independent Turn starts from primary. Recovery may persist only an optional
|
|
265
|
+
selected model, never provider route or wire state.
|
|
261
266
|
- **Transactional self-update** — `praxis update` verifies the package before
|
|
262
267
|
installing it, rejects concurrent updates, and can roll back after an
|
|
263
268
|
interruption or crash.
|
|
@@ -342,7 +347,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
|
|
|
342
347
|
`npm run test:coverage` measures all production code under `src/**` with V8 and
|
|
343
348
|
enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
|
|
344
349
|
and rejects any production runtime module with zero covered statements (while allowing
|
|
345
|
-
type-only modules). `npm run test:fixtures` executes the
|
|
350
|
+
type-only modules). `npm run test:fixtures` executes the 71-behavior native contract; 63 behaviors
|
|
346
351
|
are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
|
|
347
352
|
performs the structural check and is part of `npm run check`.
|
|
348
353
|
`npm run test:core-completion` is retained as a compatibility alias for
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentRunCancelledError, } from '../core/runtime.js';
|
|
2
2
|
import { contextRecoveryMadeProgress, isPromptTooLongError, } from '../core/context-budget.js';
|
|
3
|
+
import { StaleContextGenerationError } from './context-preparation.js';
|
|
3
4
|
function assertSignal(signal) {
|
|
4
5
|
if (signal?.aborted)
|
|
5
6
|
throw new AgentRunCancelledError();
|
|
@@ -95,7 +96,9 @@ export class ContextEngine {
|
|
|
95
96
|
return { kind: 'retry', envelope: proposal.envelope };
|
|
96
97
|
}
|
|
97
98
|
catch (cause) {
|
|
98
|
-
if (signal?.aborted ||
|
|
99
|
+
if (signal?.aborted ||
|
|
100
|
+
cause instanceof AgentRunCancelledError ||
|
|
101
|
+
cause instanceof StaleContextGenerationError)
|
|
99
102
|
throw cause;
|
|
100
103
|
return { kind: 'exhausted', error };
|
|
101
104
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ContextAssembler, type ContextAssemblyOptions } from '../core/context.js';
|
|
2
|
+
import type { ModelMessage, ModelToolDefinition } from '../core/runtime.js';
|
|
3
|
+
import type { ContextEnvelope } from './context-engine.js';
|
|
4
|
+
export interface ContextPreparationProjection {
|
|
5
|
+
readonly generation: number;
|
|
6
|
+
readonly envelope: ContextEnvelope;
|
|
7
|
+
readonly stableSystemMessageCount: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ContextPreparationSources {
|
|
10
|
+
readonly history: () => readonly ModelMessage[];
|
|
11
|
+
readonly memory: () => readonly ModelMessage[];
|
|
12
|
+
readonly activeTools: () => readonly ModelToolDefinition[];
|
|
13
|
+
}
|
|
14
|
+
export interface ContextPreparationProjectOptions {
|
|
15
|
+
readonly includeHistory?: boolean;
|
|
16
|
+
readonly includeMemory?: boolean;
|
|
17
|
+
readonly pendingMessages?: readonly ModelMessage[];
|
|
18
|
+
}
|
|
19
|
+
export interface ContextHistoryReplacement {
|
|
20
|
+
readonly generation: number;
|
|
21
|
+
readonly envelope: ContextEnvelope;
|
|
22
|
+
readonly stableSystemMessageCount: number;
|
|
23
|
+
commit<T>(replace: () => Promise<T>): Promise<{
|
|
24
|
+
generation: number;
|
|
25
|
+
value: T;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export declare class StaleContextGenerationError extends Error {
|
|
29
|
+
readonly expectedGeneration: number;
|
|
30
|
+
readonly actualGeneration: number;
|
|
31
|
+
constructor(expectedGeneration: number, actualGeneration: number);
|
|
32
|
+
}
|
|
33
|
+
export interface ContextPreparationOptions {
|
|
34
|
+
readonly assembler?: ContextAssembler;
|
|
35
|
+
readonly sources: ContextPreparationSources;
|
|
36
|
+
readonly agentMentions?: () => {
|
|
37
|
+
readonly prompt: string;
|
|
38
|
+
readonly messages: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
readonly initialGeneration?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Owns provider-visible context projection and guarded history replacement. */
|
|
43
|
+
export declare class ContextPreparation {
|
|
44
|
+
private readonly assembler;
|
|
45
|
+
private readonly sources;
|
|
46
|
+
private readonly agentMentions;
|
|
47
|
+
private generation;
|
|
48
|
+
private prepared;
|
|
49
|
+
private replacementQueue;
|
|
50
|
+
constructor(options: ContextPreparationOptions);
|
|
51
|
+
refresh(options?: ContextAssemblyOptions): Promise<void>;
|
|
52
|
+
project(options?: ContextPreparationProjectOptions): ContextPreparationProjection;
|
|
53
|
+
proposeHistoryReplacement(input: {
|
|
54
|
+
readonly historyMessages: readonly ModelMessage[];
|
|
55
|
+
readonly pendingMessages?: readonly ModelMessage[];
|
|
56
|
+
}): ContextHistoryReplacement;
|
|
57
|
+
private projectWithMessages;
|
|
58
|
+
private decorate;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=context-preparation.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
|
|
2
|
+
import { assembleContextSnapshot } from '../core/prompt-composer.js';
|
|
3
|
+
export class StaleContextGenerationError extends Error {
|
|
4
|
+
expectedGeneration;
|
|
5
|
+
actualGeneration;
|
|
6
|
+
constructor(expectedGeneration, actualGeneration) {
|
|
7
|
+
super(`Stale context generation: expected ${expectedGeneration}, actual ${actualGeneration}; prepare a new history replacement`);
|
|
8
|
+
this.name = 'StaleContextGenerationError';
|
|
9
|
+
this.expectedGeneration = expectedGeneration;
|
|
10
|
+
this.actualGeneration = actualGeneration;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function validateGeneration(generation) {
|
|
14
|
+
if (!Number.isSafeInteger(generation) || generation < 1) {
|
|
15
|
+
throw new TypeError('Context generation must be a positive safe integer');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function cloneTools(tools) {
|
|
19
|
+
return tools.map((tool) => ({ ...tool }));
|
|
20
|
+
}
|
|
21
|
+
/** Owns provider-visible context projection and guarded history replacement. */
|
|
22
|
+
export class ContextPreparation {
|
|
23
|
+
assembler;
|
|
24
|
+
sources;
|
|
25
|
+
agentMentions;
|
|
26
|
+
generation;
|
|
27
|
+
prepared;
|
|
28
|
+
replacementQueue = Promise.resolve();
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.assembler = options.assembler;
|
|
31
|
+
this.sources = options.sources;
|
|
32
|
+
this.agentMentions = options.agentMentions;
|
|
33
|
+
this.generation = options.initialGeneration ?? 1;
|
|
34
|
+
validateGeneration(this.generation);
|
|
35
|
+
}
|
|
36
|
+
async refresh(options = {}) {
|
|
37
|
+
const snapshot = await assembleContextSnapshot(this.assembler, options);
|
|
38
|
+
const projection = projectContextSnapshot(snapshot);
|
|
39
|
+
const stableCount = projection.stableSystemSectionCount;
|
|
40
|
+
const prepared = {
|
|
41
|
+
stableSystemMessages: projection.systemMessages.slice(0, stableCount),
|
|
42
|
+
volatileSystemMessages: projection.systemMessages.slice(stableCount),
|
|
43
|
+
...(projection.firstUserMessageContext === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { firstUserMessageContext: projection.firstUserMessageContext }),
|
|
46
|
+
stableSystemMessageCount: stableCount,
|
|
47
|
+
};
|
|
48
|
+
this.prepared = prepared;
|
|
49
|
+
}
|
|
50
|
+
project(options = {}) {
|
|
51
|
+
return this.projectWithMessages((options.includeHistory ?? true) ? this.sources.history() : [], (options.includeMemory ?? true) ? this.sources.memory() : [], options.pendingMessages ?? []);
|
|
52
|
+
}
|
|
53
|
+
proposeHistoryReplacement(input) {
|
|
54
|
+
const baseGeneration = this.generation;
|
|
55
|
+
if (baseGeneration === Number.MAX_SAFE_INTEGER) {
|
|
56
|
+
throw new RangeError('Context generation cannot exceed Number.MAX_SAFE_INTEGER');
|
|
57
|
+
}
|
|
58
|
+
const generation = baseGeneration + 1;
|
|
59
|
+
const projection = this.projectWithMessages(input.historyMessages, [], input.pendingMessages ?? []);
|
|
60
|
+
let committed = false;
|
|
61
|
+
return {
|
|
62
|
+
generation,
|
|
63
|
+
envelope: projection.envelope,
|
|
64
|
+
stableSystemMessageCount: projection.stableSystemMessageCount,
|
|
65
|
+
commit: async (replace) => {
|
|
66
|
+
const operation = this.replacementQueue.then(async () => {
|
|
67
|
+
if (this.generation !== baseGeneration) {
|
|
68
|
+
throw new StaleContextGenerationError(baseGeneration, this.generation);
|
|
69
|
+
}
|
|
70
|
+
if (committed) {
|
|
71
|
+
throw new StaleContextGenerationError(baseGeneration, this.generation);
|
|
72
|
+
}
|
|
73
|
+
const value = await replace();
|
|
74
|
+
this.generation = generation;
|
|
75
|
+
committed = true;
|
|
76
|
+
return { generation, value };
|
|
77
|
+
});
|
|
78
|
+
this.replacementQueue = operation.then(() => undefined, () => undefined);
|
|
79
|
+
return operation;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
projectWithMessages(historyMessages, memoryMessages, pendingMessages) {
|
|
84
|
+
const prepared = this.prepared;
|
|
85
|
+
if (!prepared) {
|
|
86
|
+
throw new Error('ContextPreparation must be refreshed before projecting');
|
|
87
|
+
}
|
|
88
|
+
const history = [...historyMessages];
|
|
89
|
+
const memory = [...memoryMessages];
|
|
90
|
+
const pending = [...pendingMessages];
|
|
91
|
+
const decoratedHistory = this.decorate([...history, ...memory, ...pending]);
|
|
92
|
+
const messages = [
|
|
93
|
+
...prepared.stableSystemMessages,
|
|
94
|
+
...prepared.volatileSystemMessages,
|
|
95
|
+
...decoratedHistory,
|
|
96
|
+
];
|
|
97
|
+
return {
|
|
98
|
+
generation: this.generation,
|
|
99
|
+
envelope: {
|
|
100
|
+
messages,
|
|
101
|
+
tools: cloneTools(this.sources.activeTools()),
|
|
102
|
+
},
|
|
103
|
+
stableSystemMessageCount: prepared.stableSystemMessageCount,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
decorate(messages) {
|
|
107
|
+
const prepared = this.prepared;
|
|
108
|
+
if (!prepared) {
|
|
109
|
+
throw new Error('ContextPreparation must be refreshed before projecting');
|
|
110
|
+
}
|
|
111
|
+
const withFirstUserContext = injectFirstUserMessageContext(messages, prepared.firstUserMessageContext);
|
|
112
|
+
const mentionInput = this.agentMentions?.();
|
|
113
|
+
if (!mentionInput || mentionInput.messages.length === 0)
|
|
114
|
+
return withFirstUserContext;
|
|
115
|
+
let insertionIndex = withFirstUserContext.length;
|
|
116
|
+
let foundPrompt = false;
|
|
117
|
+
for (let index = withFirstUserContext.length - 1; index >= 0; index -= 1) {
|
|
118
|
+
const message = withFirstUserContext[index];
|
|
119
|
+
if (message?.role === 'user' &&
|
|
120
|
+
typeof message.content === 'string' &&
|
|
121
|
+
message.content.endsWith(mentionInput.prompt)) {
|
|
122
|
+
insertionIndex = index;
|
|
123
|
+
foundPrompt = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!foundPrompt)
|
|
128
|
+
return withFirstUserContext;
|
|
129
|
+
return [
|
|
130
|
+
...withFirstUserContext.slice(0, insertionIndex),
|
|
131
|
+
...mentionInput.messages.map((content) => ({
|
|
132
|
+
role: 'user',
|
|
133
|
+
content,
|
|
134
|
+
})),
|
|
135
|
+
...withFirstUserContext.slice(insertionIndex),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=context-preparation.js.map
|
|
@@ -7,6 +7,8 @@ export interface NativeSidechainMetadata {
|
|
|
7
7
|
readonly spawnDepth: number;
|
|
8
8
|
readonly cwd: string;
|
|
9
9
|
readonly promptId: string;
|
|
10
|
+
/** Provider-neutral selected model identifier for recovery. */
|
|
11
|
+
readonly model?: string;
|
|
10
12
|
readonly name?: string;
|
|
11
13
|
readonly permissionMode?: NativeSidechainPermissionMode;
|
|
12
14
|
readonly isolation?: 'worktree';
|
|
@@ -19,6 +19,7 @@ const requiredKeys = [
|
|
|
19
19
|
'promptId',
|
|
20
20
|
];
|
|
21
21
|
const optionalKeys = [
|
|
22
|
+
'model',
|
|
22
23
|
'name',
|
|
23
24
|
'permissionMode',
|
|
24
25
|
'isolation',
|
|
@@ -74,6 +75,8 @@ function validateMetadata(value) {
|
|
|
74
75
|
throw new Error('native sidechain metadata cwd is invalid');
|
|
75
76
|
if (!nonBlank(record.promptId))
|
|
76
77
|
throw new Error('native sidechain metadata promptId is invalid');
|
|
78
|
+
if (record.model !== undefined && !nonBlank(record.model))
|
|
79
|
+
throw new Error('native sidechain metadata model is invalid');
|
|
77
80
|
if (record.name !== undefined && !nonBlank(record.name))
|
|
78
81
|
throw new Error('native sidechain metadata name is invalid');
|
|
79
82
|
if (record.permissionMode !== undefined &&
|
|
@@ -236,7 +236,7 @@ export declare class ClaudeSessionService {
|
|
|
236
236
|
private readonly hookLifecycle;
|
|
237
237
|
private readonly leadOperations;
|
|
238
238
|
private readonly fileChangeWatcher;
|
|
239
|
-
private readonly
|
|
239
|
+
private readonly turnCoordinator;
|
|
240
240
|
private runtimeCwd;
|
|
241
241
|
constructor(options: ClaudeSessionServiceOptions);
|
|
242
242
|
nextScheduledPrompt(signal?: AbortSignal): Promise<ScheduledPrompt | null>;
|
|
@@ -21,9 +21,9 @@ import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
|
21
21
|
import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
|
|
22
22
|
import { usageCostUsd } from '../core/usage.js';
|
|
23
23
|
import { isSessionId } from '../core/session.js';
|
|
24
|
-
import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
|
|
25
24
|
import { ContextBudget, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
26
25
|
import { ContextEngine } from './context-engine.js';
|
|
26
|
+
import { ContextPreparation } from './context-preparation.js';
|
|
27
27
|
import { TurnMemoryCoordinator } from './turn-memory-coordinator.js';
|
|
28
28
|
import { injectFirstUserMessageContext, projectContextSnapshot, } from '../core/context.js';
|
|
29
29
|
import { assembleContextSnapshot } from '../core/prompt-composer.js';
|
|
@@ -38,7 +38,7 @@ import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.
|
|
|
38
38
|
import { ModelCompactor } from './model-compactor.js';
|
|
39
39
|
import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
|
|
40
40
|
import { ScheduledPromptManager, } from './scheduled-prompt-manager.js';
|
|
41
|
-
import {
|
|
41
|
+
import { TurnCoordinator } from './turn-lifecycle.js';
|
|
42
42
|
import { ClaudeScheduledToolRegistry } from '../tools/claude-scheduled-tools.js';
|
|
43
43
|
import { ClaudeTaskToolRegistry } from '../tools/claude-task-tools.js';
|
|
44
44
|
import { ClaudeWorkflowToolRegistry } from '../tools/claude-workflow-tools.js';
|
|
@@ -684,12 +684,16 @@ export class ClaudeSessionService {
|
|
|
684
684
|
hookLifecycle;
|
|
685
685
|
leadOperations;
|
|
686
686
|
fileChangeWatcher;
|
|
687
|
-
|
|
687
|
+
turnCoordinator;
|
|
688
688
|
runtimeCwd;
|
|
689
689
|
constructor(options) {
|
|
690
690
|
const dataPlane = options.dataPlane ?? 'native';
|
|
691
691
|
assertNativeDataPlane(dataPlane);
|
|
692
692
|
this.options = { ...options, dataPlane };
|
|
693
|
+
this.turnCoordinator = new TurnCoordinator({
|
|
694
|
+
eventSink: options.eventSink ?? (() => undefined),
|
|
695
|
+
createSteeringId: randomUUID,
|
|
696
|
+
});
|
|
693
697
|
this.assertNativeTranscriptOptions();
|
|
694
698
|
this.leadOperations = options.teamLeadOperations ?? null;
|
|
695
699
|
this.hookLifecycle = new HookLifecycle(options.hooks, options.eventSink);
|
|
@@ -899,18 +903,7 @@ export class ClaudeSessionService {
|
|
|
899
903
|
}
|
|
900
904
|
async close() {
|
|
901
905
|
this.closing = true;
|
|
902
|
-
|
|
903
|
-
if (!mailbox)
|
|
904
|
-
continue;
|
|
905
|
-
for (const item of mailbox.close()) {
|
|
906
|
-
this.options.eventSink?.({
|
|
907
|
-
type: 'user-input-rejected',
|
|
908
|
-
id: item.id,
|
|
909
|
-
content: item.content,
|
|
910
|
-
reason: 'closed',
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
}
|
|
906
|
+
this.turnCoordinator.close();
|
|
914
907
|
await this.fileChangeWatcher?.close(5_000);
|
|
915
908
|
await this.hookLifecycle.close();
|
|
916
909
|
await this.drainDetachedHookRuns(5_000);
|
|
@@ -1117,6 +1110,9 @@ export class ClaudeSessionService {
|
|
|
1117
1110
|
...(this.options.providerForModel
|
|
1118
1111
|
? { providerForModel: this.options.providerForModel }
|
|
1119
1112
|
: {}),
|
|
1113
|
+
...(this.options.providerForTurn
|
|
1114
|
+
? { providerForTurn: this.options.providerForTurn }
|
|
1115
|
+
: {}),
|
|
1120
1116
|
baseTools: wrappedBase,
|
|
1121
1117
|
...(this.options.deferMcpTools === undefined
|
|
1122
1118
|
? {}
|
|
@@ -1327,28 +1323,10 @@ export class ClaudeSessionService {
|
|
|
1327
1323
|
});
|
|
1328
1324
|
}
|
|
1329
1325
|
steer(sessionId, content) {
|
|
1330
|
-
|
|
1331
|
-
if (!active)
|
|
1332
|
-
return { kind: 'no-active-turn' };
|
|
1333
|
-
const mailbox = active.mailbox;
|
|
1334
|
-
if (!mailbox)
|
|
1335
|
-
return { kind: 'not-steerable' };
|
|
1336
|
-
const result = mailbox.enqueue(content);
|
|
1337
|
-
if (result.kind === 'accepted')
|
|
1338
|
-
return result;
|
|
1339
|
-
if (result.kind === 'empty')
|
|
1340
|
-
return result;
|
|
1341
|
-
return { kind: 'turn-completing' };
|
|
1326
|
+
return this.turnCoordinator.steer(sessionId, content);
|
|
1342
1327
|
}
|
|
1343
1328
|
withdrawSteering(sessionId, id) {
|
|
1344
|
-
|
|
1345
|
-
if (!active)
|
|
1346
|
-
return { kind: 'no-active-turn' };
|
|
1347
|
-
const mailbox = active.mailbox;
|
|
1348
|
-
if (!mailbox)
|
|
1349
|
-
return { kind: 'not-steerable' };
|
|
1350
|
-
const result = mailbox.withdraw(id);
|
|
1351
|
-
return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
|
|
1329
|
+
return this.turnCoordinator.withdrawSteering(sessionId, id);
|
|
1352
1330
|
}
|
|
1353
1331
|
async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
|
|
1354
1332
|
this.worktreeManager?.bindSession(sessionId);
|
|
@@ -2532,29 +2510,8 @@ export class ClaudeSessionService {
|
|
|
2532
2510
|
const documents = submission.kind === 'prompt' ? (submission.documents ?? []) : [];
|
|
2533
2511
|
const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
|
|
2534
2512
|
const skipUserPrompt = submission.kind === 'retry';
|
|
2535
|
-
|
|
2536
|
-
let activeTurnInput;
|
|
2537
|
-
let activeTurnRecord;
|
|
2538
|
-
try {
|
|
2513
|
+
return this.turnCoordinator.run(request, async ({ emit, steering }) => {
|
|
2539
2514
|
this.assertTurnWritable();
|
|
2540
|
-
if (prompt.length === 0 && images.length === 0 && documents.length === 0)
|
|
2541
|
-
throw new Error('Prompt must not be empty');
|
|
2542
|
-
if (name !== undefined && name.length === 0) {
|
|
2543
|
-
throw new Error('Session name must not be empty');
|
|
2544
|
-
}
|
|
2545
|
-
if (shellCommand !== undefined && shellCommand.trim().length === 0) {
|
|
2546
|
-
throw new Error('Shell command must not be empty');
|
|
2547
|
-
}
|
|
2548
|
-
if (this.activeTurnInputs.has(sessionId)) {
|
|
2549
|
-
throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
|
|
2550
|
-
}
|
|
2551
|
-
activeTurnRecord =
|
|
2552
|
-
shellCommand === undefined
|
|
2553
|
-
? {
|
|
2554
|
-
mailbox: (activeTurnInput = new ActiveTurnInputMailbox(randomUUID)),
|
|
2555
|
-
}
|
|
2556
|
-
: {};
|
|
2557
|
-
this.activeTurnInputs.set(sessionId, activeTurnRecord);
|
|
2558
2515
|
await this.activateSessionCostTracker(sessionId);
|
|
2559
2516
|
await this.ensureFileResources(sessionId, signal);
|
|
2560
2517
|
this.worktreeManager?.bindSession(sessionId);
|
|
@@ -2858,6 +2815,9 @@ export class ClaudeSessionService {
|
|
|
2858
2815
|
...(this.options.providerForModel
|
|
2859
2816
|
? { providerForModel: this.options.providerForModel }
|
|
2860
2817
|
: {}),
|
|
2818
|
+
...(this.options.providerForTurn
|
|
2819
|
+
? { providerForTurn: this.options.providerForTurn }
|
|
2820
|
+
: {}),
|
|
2861
2821
|
baseTools,
|
|
2862
2822
|
...(this.options.deferMcpTools === undefined
|
|
2863
2823
|
? {}
|
|
@@ -3065,7 +3025,7 @@ export class ClaudeSessionService {
|
|
|
3065
3025
|
deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
|
|
3066
3026
|
})
|
|
3067
3027
|
: null;
|
|
3068
|
-
const runtime = new AgentRuntime(provider,
|
|
3028
|
+
const runtime = new AgentRuntime(provider, emit, {
|
|
3069
3029
|
emitInitialContextState: false,
|
|
3070
3030
|
...(this.options.emitToolUseSummaries
|
|
3071
3031
|
? {
|
|
@@ -3407,19 +3367,30 @@ export class ClaudeSessionService {
|
|
|
3407
3367
|
let agentSystem = null;
|
|
3408
3368
|
let planModeMessage;
|
|
3409
3369
|
let sessionMemoryMessage = null;
|
|
3410
|
-
let
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3370
|
+
let agentMentionMessages = [];
|
|
3371
|
+
const contextPreparation = new ContextPreparation({
|
|
3372
|
+
...(this.options.contextAssembler
|
|
3373
|
+
? { assembler: this.options.contextAssembler }
|
|
3374
|
+
: {}),
|
|
3375
|
+
sources: {
|
|
3376
|
+
history: activeTurnMessages,
|
|
3377
|
+
memory: () => projectMemoryRecallMessages,
|
|
3378
|
+
activeTools: () => provider.capabilities.tools
|
|
3379
|
+
? (activeTurnTools?.definitions() ?? [])
|
|
3380
|
+
: [],
|
|
3381
|
+
},
|
|
3382
|
+
agentMentions: () => ({
|
|
3383
|
+
prompt: effectivePrompt,
|
|
3384
|
+
messages: agentMentionMessages,
|
|
3385
|
+
}),
|
|
3386
|
+
});
|
|
3416
3387
|
refreshRuntimeContext = async () => {
|
|
3417
3388
|
agentSystem = await this.mainAgentSystemPrompt(agent);
|
|
3418
3389
|
planModeMessage =
|
|
3419
3390
|
this.options.interactiveTools?.contextMessage(sessionId);
|
|
3420
3391
|
// DEBUG_PLAN_CONTEXT
|
|
3421
3392
|
sessionMemoryMessage = this.sessionMemoryMessage(await turnMemory.sessionSummary());
|
|
3422
|
-
|
|
3393
|
+
await contextPreparation.refresh({
|
|
3423
3394
|
cwd: this.activeCwd(),
|
|
3424
3395
|
lifecycleId: sessionId,
|
|
3425
3396
|
...(agentSystem
|
|
@@ -3436,10 +3407,6 @@ export class ClaudeSessionService {
|
|
|
3436
3407
|
: {}),
|
|
3437
3408
|
},
|
|
3438
3409
|
});
|
|
3439
|
-
contextProjection = projectContextSnapshot(assembledContext);
|
|
3440
|
-
stableSystemMessageCount =
|
|
3441
|
-
contextProjection.stableSystemSectionCount;
|
|
3442
|
-
contextMessages = [...contextProjection.systemMessages];
|
|
3443
3410
|
};
|
|
3444
3411
|
await refreshRuntimeContext();
|
|
3445
3412
|
const expansion = shouldSkipUserPrompt()
|
|
@@ -3469,9 +3436,6 @@ export class ClaudeSessionService {
|
|
|
3469
3436
|
let compactionDurationMs;
|
|
3470
3437
|
let compactionDurationWithoutRetriesMs;
|
|
3471
3438
|
let compactionModelUsage;
|
|
3472
|
-
const currentDefinitions = () => provider.capabilities.tools
|
|
3473
|
-
? (activeTurnTools?.definitions() ?? [])
|
|
3474
|
-
: [];
|
|
3475
3439
|
const budget = this.contextBudget(provider);
|
|
3476
3440
|
const contextEngine = new ContextEngine({
|
|
3477
3441
|
...(budget ? { budget } : {}),
|
|
@@ -3503,88 +3467,50 @@ export class ClaudeSessionService {
|
|
|
3503
3467
|
? { documents }
|
|
3504
3468
|
: {}),
|
|
3505
3469
|
}));
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
if (agentMentionMessages.length === 0)
|
|
3511
|
-
return [...messages];
|
|
3512
|
-
let insertionIndex = messages.length;
|
|
3513
|
-
let foundPrompt = false;
|
|
3514
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3515
|
-
const message = messages[index];
|
|
3516
|
-
if (message?.role === 'user' &&
|
|
3517
|
-
typeof message.content === 'string' &&
|
|
3518
|
-
message.content.endsWith(effectivePrompt)) {
|
|
3519
|
-
insertionIndex = index;
|
|
3520
|
-
foundPrompt = true;
|
|
3521
|
-
break;
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
if (!foundPrompt)
|
|
3525
|
-
return [...messages];
|
|
3526
|
-
return [
|
|
3527
|
-
...messages.slice(0, insertionIndex),
|
|
3528
|
-
...agentMentionMessages.map((content) => ({
|
|
3529
|
-
role: 'user',
|
|
3530
|
-
content,
|
|
3531
|
-
})),
|
|
3532
|
-
...messages.slice(insertionIndex),
|
|
3533
|
-
];
|
|
3534
|
-
};
|
|
3535
|
-
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, contextProjection.firstUserMessageContext);
|
|
3536
|
-
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
3470
|
+
agentMentionMessages =
|
|
3471
|
+
shellCommand === undefined && !shouldSkipUserPrompt()
|
|
3472
|
+
? (this.options.extensions?.agentMentionMessages(effectivePrompt) ?? [])
|
|
3473
|
+
: [];
|
|
3537
3474
|
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
3538
3475
|
const contextTransitionPort = (pendingMessages = [], preservedUserMessages = []) => ({
|
|
3539
3476
|
current: () => {
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
return {
|
|
3545
|
-
messages: [
|
|
3546
|
-
...contextMessages,
|
|
3547
|
-
...injectTurnContext([
|
|
3548
|
-
...historyMessages,
|
|
3549
|
-
...pendingMessages,
|
|
3550
|
-
]),
|
|
3551
|
-
],
|
|
3552
|
-
tools: currentDefinitions(),
|
|
3553
|
-
};
|
|
3477
|
+
const projection = contextPreparation.project({
|
|
3478
|
+
pendingMessages,
|
|
3479
|
+
});
|
|
3480
|
+
return projection.envelope;
|
|
3554
3481
|
},
|
|
3555
|
-
irreducible: () => ({
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
]),
|
|
3482
|
+
irreducible: () => contextPreparation.project({
|
|
3483
|
+
includeHistory: false,
|
|
3484
|
+
includeMemory: false,
|
|
3485
|
+
pendingMessages: [
|
|
3486
|
+
...pendingMessages,
|
|
3487
|
+
...preservedUserMessages.map((content) => ({
|
|
3488
|
+
role: 'user',
|
|
3489
|
+
content,
|
|
3490
|
+
})),
|
|
3565
3491
|
],
|
|
3566
|
-
|
|
3567
|
-
}),
|
|
3492
|
+
}).envelope,
|
|
3568
3493
|
propose: async () => {
|
|
3569
3494
|
const activeNativeLease = nativeLease;
|
|
3570
3495
|
if (!budget)
|
|
3571
3496
|
throw new Error('Context budget is unavailable');
|
|
3572
|
-
const definitions =
|
|
3497
|
+
const definitions = contextPreparation.project().envelope.tools;
|
|
3573
3498
|
const historyMessages = activeTurnMessages();
|
|
3574
3499
|
if (historyMessages.length === 0)
|
|
3575
3500
|
throw new Error('Cannot compact an empty native transcript');
|
|
3576
3501
|
if (unresolvedActiveToolCallIds(historyMessages).length > 0)
|
|
3577
3502
|
throw new Error('Cannot compact a native transcript with unresolved tool calls');
|
|
3578
|
-
const irreducibleMessages =
|
|
3579
|
-
|
|
3580
|
-
|
|
3503
|
+
const irreducibleMessages = contextPreparation.project({
|
|
3504
|
+
includeHistory: false,
|
|
3505
|
+
includeMemory: false,
|
|
3506
|
+
pendingMessages: [
|
|
3581
3507
|
...pendingMessages,
|
|
3582
3508
|
...preservedUserMessages.map((content) => ({
|
|
3583
3509
|
role: 'user',
|
|
3584
3510
|
content,
|
|
3585
3511
|
})),
|
|
3586
|
-
]
|
|
3587
|
-
|
|
3512
|
+
],
|
|
3513
|
+
}).envelope.messages;
|
|
3588
3514
|
let compactableMessages = historyMessages;
|
|
3589
3515
|
let preservedMessages = [];
|
|
3590
3516
|
let compactionLogicalParentId;
|
|
@@ -3751,14 +3677,15 @@ export class ClaudeSessionService {
|
|
|
3751
3677
|
};
|
|
3752
3678
|
let replayMessages = preservedMessages;
|
|
3753
3679
|
try {
|
|
3754
|
-
const replayReport = budget.evaluate(
|
|
3755
|
-
|
|
3756
|
-
|
|
3680
|
+
const replayReport = budget.evaluate(contextPreparation.project({
|
|
3681
|
+
includeHistory: false,
|
|
3682
|
+
includeMemory: false,
|
|
3683
|
+
pendingMessages: [
|
|
3757
3684
|
summaryMessage,
|
|
3758
3685
|
...replayMessages,
|
|
3759
3686
|
...pendingMessages,
|
|
3760
|
-
]
|
|
3761
|
-
|
|
3687
|
+
],
|
|
3688
|
+
}).envelope.messages, definitions);
|
|
3762
3689
|
if (replayReport.shouldCompact)
|
|
3763
3690
|
throw new Error('replay overflow');
|
|
3764
3691
|
}
|
|
@@ -3769,23 +3696,16 @@ export class ClaudeSessionService {
|
|
|
3769
3696
|
// contains its contents and user prompts remain available.
|
|
3770
3697
|
replayMessages = replayMessages.filter((message) => message.role === 'user' && !Array.isArray(message.content));
|
|
3771
3698
|
}
|
|
3772
|
-
const
|
|
3773
|
-
...
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
...replayMessages,
|
|
3777
|
-
...pendingMessages,
|
|
3778
|
-
]),
|
|
3779
|
-
];
|
|
3699
|
+
const replacement = contextPreparation.proposeHistoryReplacement({
|
|
3700
|
+
historyMessages: [summaryMessage, ...replayMessages],
|
|
3701
|
+
pendingMessages,
|
|
3702
|
+
});
|
|
3780
3703
|
return {
|
|
3781
|
-
envelope:
|
|
3782
|
-
messages: proposedMessages,
|
|
3783
|
-
tools: definitions,
|
|
3784
|
-
},
|
|
3704
|
+
envelope: replacement.envelope,
|
|
3785
3705
|
commit: async () => {
|
|
3786
3706
|
if (signal?.aborted)
|
|
3787
3707
|
throw new AgentRunCancelledError();
|
|
3788
|
-
const
|
|
3708
|
+
const committed = await replacement.commit(() => nativeLease.appendCompaction({
|
|
3789
3709
|
summary: compacted.summary,
|
|
3790
3710
|
trigger: 'auto',
|
|
3791
3711
|
preTokens,
|
|
@@ -3802,7 +3722,8 @@ export class ClaudeSessionService {
|
|
|
3802
3722
|
preservePrefix: false,
|
|
3803
3723
|
}
|
|
3804
3724
|
: {}),
|
|
3805
|
-
});
|
|
3725
|
+
}));
|
|
3726
|
+
const ids = committed.value;
|
|
3806
3727
|
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
|
|
3807
3728
|
if (this.options.hooks) {
|
|
3808
3729
|
const outcome = await this.hookLifecycle.refresh(sessionId, hookSession, signal);
|
|
@@ -4089,25 +4010,24 @@ export class ClaudeSessionService {
|
|
|
4089
4010
|
};
|
|
4090
4011
|
}
|
|
4091
4012
|
if (shellCommand === undefined && budget) {
|
|
4092
|
-
const definitions = currentDefinitions();
|
|
4093
4013
|
await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4014
|
+
const projection = contextPreparation.project({
|
|
4015
|
+
includeMemory: false,
|
|
4016
|
+
});
|
|
4017
|
+
budget.assertFits(budget.evaluate(projection.envelope.messages, projection.envelope.tools));
|
|
4098
4018
|
}
|
|
4099
4019
|
let stopHookActive = false;
|
|
4020
|
+
const initialProjection = contextPreparation.project({
|
|
4021
|
+
includeMemory: false,
|
|
4022
|
+
});
|
|
4100
4023
|
const runtimeRequest = {
|
|
4101
4024
|
sessionId,
|
|
4102
|
-
messages:
|
|
4103
|
-
|
|
4104
|
-
...injectTurnContext(activeTurnMessages()),
|
|
4105
|
-
],
|
|
4106
|
-
stableSystemMessageCount,
|
|
4025
|
+
messages: initialProjection.envelope.messages,
|
|
4026
|
+
stableSystemMessageCount: initialProjection.stableSystemMessageCount,
|
|
4107
4027
|
cwd: this.activeCwd(),
|
|
4108
4028
|
toolResultDirectory,
|
|
4109
4029
|
observer,
|
|
4110
|
-
...(
|
|
4030
|
+
...(steering ? { steering } : {}),
|
|
4111
4031
|
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
4112
4032
|
...(this.options.maxModelTurns !== undefined
|
|
4113
4033
|
? { maxModelTurns: this.options.maxModelTurns }
|
|
@@ -4132,14 +4052,10 @@ export class ClaudeSessionService {
|
|
|
4132
4052
|
if (shellCommand === undefined) {
|
|
4133
4053
|
await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
|
|
4134
4054
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
...activeTurnMessages(),
|
|
4140
|
-
...projectMemoryRecallMessages,
|
|
4141
|
-
]),
|
|
4142
|
-
];
|
|
4055
|
+
const projection = contextPreparation.project();
|
|
4056
|
+
runtimeRequest.stableSystemMessageCount =
|
|
4057
|
+
projection.stableSystemMessageCount;
|
|
4058
|
+
return projection.envelope.messages;
|
|
4143
4059
|
},
|
|
4144
4060
|
...(this.options.hooks ||
|
|
4145
4061
|
subagentExecutor ||
|
|
@@ -4268,14 +4184,10 @@ export class ClaudeSessionService {
|
|
|
4268
4184
|
}
|
|
4269
4185
|
// The single reactive retry must use the compacted transcript, not
|
|
4270
4186
|
// the stale request copy captured before the compact boundary.
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
...projectMemoryRecallMessages,
|
|
4276
|
-
]),
|
|
4277
|
-
];
|
|
4278
|
-
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
4187
|
+
const projection = contextPreparation.project();
|
|
4188
|
+
runtimeRequest.messages = projection.envelope.messages;
|
|
4189
|
+
runtimeRequest.stableSystemMessageCount =
|
|
4190
|
+
projection.stableSystemMessageCount;
|
|
4279
4191
|
runtimeRequest.deferFailureKinds = true;
|
|
4280
4192
|
try {
|
|
4281
4193
|
result = await attemptMainTurn();
|
|
@@ -4411,11 +4323,11 @@ export class ClaudeSessionService {
|
|
|
4411
4323
|
? projectNativeSessionEntries(nativeLease.activeEvents()).at(-1)
|
|
4412
4324
|
?.uuid
|
|
4413
4325
|
: undefined;
|
|
4414
|
-
const
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
const definitions =
|
|
4326
|
+
const providerProjection = contextPreparation.project({
|
|
4327
|
+
includeMemory: false,
|
|
4328
|
+
});
|
|
4329
|
+
const providerVisibleMessages = providerProjection.envelope.messages;
|
|
4330
|
+
const definitions = providerProjection.envelope.tools;
|
|
4419
4331
|
const currentContextTokens = contextEngine.report({
|
|
4420
4332
|
messages: providerVisibleMessages,
|
|
4421
4333
|
tools: definitions,
|
|
@@ -4501,27 +4413,8 @@ export class ClaudeSessionService {
|
|
|
4501
4413
|
return scratchResult.value;
|
|
4502
4414
|
});
|
|
4503
4415
|
}
|
|
4504
|
-
controller.complete();
|
|
4505
4416
|
return result;
|
|
4506
|
-
}
|
|
4507
|
-
catch (error) {
|
|
4508
|
-
controller.fail(error, signal);
|
|
4509
|
-
throw error;
|
|
4510
|
-
}
|
|
4511
|
-
finally {
|
|
4512
|
-
if (activeTurnInput !== undefined) {
|
|
4513
|
-
for (const item of activeTurnInput.close()) {
|
|
4514
|
-
this.options.eventSink?.({
|
|
4515
|
-
type: 'user-input-rejected',
|
|
4516
|
-
id: item.id,
|
|
4517
|
-
content: item.content,
|
|
4518
|
-
reason: signal?.aborted ? 'cancelled' : 'failed',
|
|
4519
|
-
});
|
|
4520
|
-
}
|
|
4521
|
-
}
|
|
4522
|
-
if (this.activeTurnInputs.get(sessionId) === activeTurnRecord)
|
|
4523
|
-
this.activeTurnInputs.delete(sessionId);
|
|
4524
|
-
}
|
|
4417
|
+
});
|
|
4525
4418
|
}
|
|
4526
4419
|
async ensureFileResources(sessionId, signal) {
|
|
4527
4420
|
const resources = this.options.fileResources ?? [];
|
|
@@ -76,6 +76,7 @@ export interface ClaudeSubagentExecutorOptions {
|
|
|
76
76
|
maxCalls?: number;
|
|
77
77
|
maxOutputBytes?: number;
|
|
78
78
|
providerForModel?: (model: string) => ModelProvider;
|
|
79
|
+
providerForTurn?: (model?: string) => ModelProvider;
|
|
79
80
|
toolNames?: readonly string[];
|
|
80
81
|
backgroundTaskNotifications?: (waitForRunning: boolean) => Promise<string[]>;
|
|
81
82
|
notificationDelivered?: (notification: {
|
|
@@ -117,6 +118,7 @@ export declare class ClaudeSubagentExecutor {
|
|
|
117
118
|
sendBackgroundMessage(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
|
|
118
119
|
stopAllBackgroundTasks(): readonly string[];
|
|
119
120
|
private cwd;
|
|
121
|
+
private providerForTurn;
|
|
120
122
|
private agentDefinition;
|
|
121
123
|
private resolveAgentInput;
|
|
122
124
|
registry(sessionId: string, depth: number, promptIdForCall: (callId: string) => string | null, parentAgentId?: string): ToolRegistry;
|
|
@@ -586,6 +586,15 @@ export class ClaudeSubagentExecutor {
|
|
|
586
586
|
cwd() {
|
|
587
587
|
return this.options.cwdProvider?.() ?? this.options.cwd;
|
|
588
588
|
}
|
|
589
|
+
providerForTurn(model) {
|
|
590
|
+
if (this.options.providerForTurn) {
|
|
591
|
+
return this.options.providerForTurn(model);
|
|
592
|
+
}
|
|
593
|
+
if (model !== undefined) {
|
|
594
|
+
return this.options.providerForModel?.(model) ?? this.options.provider;
|
|
595
|
+
}
|
|
596
|
+
return this.options.provider;
|
|
597
|
+
}
|
|
589
598
|
agentDefinition(input) {
|
|
590
599
|
return this.options.extensions?.agent(input.subagentType) ?? null;
|
|
591
600
|
}
|
|
@@ -753,7 +762,9 @@ export class ClaudeSubagentExecutor {
|
|
|
753
762
|
!this.options.extensions?.agent(input.subagentType)) {
|
|
754
763
|
throw new Error(`Unknown Claude agent ${input.subagentType}`);
|
|
755
764
|
}
|
|
756
|
-
if (input.model &&
|
|
765
|
+
if (input.model &&
|
|
766
|
+
!this.options.providerForModel &&
|
|
767
|
+
!this.options.providerForTurn) {
|
|
757
768
|
throw new Error('Agent model overrides are unavailable for this provider');
|
|
758
769
|
}
|
|
759
770
|
if (input.permissionMode &&
|
|
@@ -850,6 +861,7 @@ export class ClaudeSubagentExecutor {
|
|
|
850
861
|
...(input.permissionMode
|
|
851
862
|
? { permissionMode: input.permissionMode }
|
|
852
863
|
: {}),
|
|
864
|
+
...(input.model ? { model: input.model } : {}),
|
|
853
865
|
...(input.isolation ? { isolation: input.isolation } : {}),
|
|
854
866
|
...(parentAgentId ? { parentAgentId } : {}),
|
|
855
867
|
...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
|
|
@@ -863,9 +875,7 @@ export class ClaudeSubagentExecutor {
|
|
|
863
875
|
catch (error) {
|
|
864
876
|
return settleInitialSetupFailure(error, initialIsolation);
|
|
865
877
|
}
|
|
866
|
-
const provider = input.model
|
|
867
|
-
? (this.options.providerForModel?.(input.model) ?? this.options.provider)
|
|
868
|
-
: this.options.provider;
|
|
878
|
+
const provider = this.providerForTurn(input.model);
|
|
869
879
|
const backgroundRun = this.createBackgroundAgentRun({
|
|
870
880
|
input,
|
|
871
881
|
parentCwd,
|
|
@@ -874,11 +884,14 @@ export class ClaudeSubagentExecutor {
|
|
|
874
884
|
...(initialIsolation ? { initialIsolation } : {}),
|
|
875
885
|
createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
|
|
876
886
|
execute: async (cwd, message, signal, continuation) => {
|
|
887
|
+
const turnProvider = continuation
|
|
888
|
+
? this.providerForTurn(input.model)
|
|
889
|
+
: provider;
|
|
877
890
|
const run = (lease) => this.runSidechain({
|
|
878
891
|
...lease,
|
|
879
892
|
sessionId,
|
|
880
893
|
input,
|
|
881
|
-
provider,
|
|
894
|
+
provider: turnProvider,
|
|
882
895
|
agentId,
|
|
883
896
|
spawnDepth,
|
|
884
897
|
promptId,
|
|
@@ -1346,13 +1359,12 @@ export class ClaudeSubagentExecutor {
|
|
|
1346
1359
|
!this.options.extensions?.agent(options.agentType)) {
|
|
1347
1360
|
throw new Error(`Unknown Claude agent ${options.agentType}`);
|
|
1348
1361
|
}
|
|
1349
|
-
if (options.model &&
|
|
1362
|
+
if (options.model &&
|
|
1363
|
+
!this.options.providerForModel &&
|
|
1364
|
+
!this.options.providerForTurn) {
|
|
1350
1365
|
throw new Error('Workflow agent model overrides are unavailable for this provider');
|
|
1351
1366
|
}
|
|
1352
|
-
const provider = options.model
|
|
1353
|
-
? (this.options.providerForModel?.(options.model) ??
|
|
1354
|
-
this.options.provider)
|
|
1355
|
-
: this.options.provider;
|
|
1367
|
+
const provider = this.providerForTurn(options.model);
|
|
1356
1368
|
const input = {
|
|
1357
1369
|
description: options.label ?? 'Workflow agent',
|
|
1358
1370
|
prompt: options.prompt,
|
|
@@ -1395,6 +1407,7 @@ export class ClaudeSubagentExecutor {
|
|
|
1395
1407
|
spawnDepth: 1,
|
|
1396
1408
|
cwd: agentCwd,
|
|
1397
1409
|
promptId: options.promptId,
|
|
1410
|
+
...(input.model ? { model: input.model } : {}),
|
|
1398
1411
|
...(options.isolation ? { isolation: options.isolation } : {}),
|
|
1399
1412
|
...(isolation ? { worktreePath: isolation.cwd } : {}),
|
|
1400
1413
|
});
|
|
@@ -1716,11 +1729,11 @@ export class ClaudeSubagentExecutor {
|
|
|
1716
1729
|
prompt,
|
|
1717
1730
|
subagentType: agentType,
|
|
1718
1731
|
...(name ? { name } : {}),
|
|
1732
|
+
...(metadata?.model ? { model: metadata.model } : {}),
|
|
1719
1733
|
...(permissionMode ? { permissionMode } : {}),
|
|
1720
1734
|
...(isolation ? { isolation } : {}),
|
|
1721
1735
|
runInBackground: true,
|
|
1722
1736
|
};
|
|
1723
|
-
const provider = this.options.provider;
|
|
1724
1737
|
const recoveredPromptId = metadata?.promptId ?? randomUUID();
|
|
1725
1738
|
const backgroundRun = this.createBackgroundAgentRun({
|
|
1726
1739
|
input,
|
|
@@ -1731,6 +1744,9 @@ export class ClaudeSubagentExecutor {
|
|
|
1731
1744
|
...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
|
|
1732
1745
|
createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
|
|
1733
1746
|
execute: async (cwd, message, signal, continuation) => {
|
|
1747
|
+
// Recovery never restores provider-native route state. Each recovered
|
|
1748
|
+
// execution, including every later follow-up, gets a fresh turn.
|
|
1749
|
+
const provider = this.providerForTurn(input.model);
|
|
1734
1750
|
const run = (lease) => this.runSidechain({
|
|
1735
1751
|
...lease,
|
|
1736
1752
|
sessionId,
|
|
@@ -1757,7 +1773,7 @@ export class ClaudeSubagentExecutor {
|
|
|
1757
1773
|
prompt,
|
|
1758
1774
|
toolUseId,
|
|
1759
1775
|
outputFile: sidechainPaths.transcriptFile,
|
|
1760
|
-
resolvedModel: provider.model ?? 'praxis/provider',
|
|
1776
|
+
resolvedModel: metadata?.model ?? this.options.provider.model ?? 'praxis/provider',
|
|
1761
1777
|
lifecycle: backgroundRun.lifecycle,
|
|
1762
1778
|
run: backgroundRun.run,
|
|
1763
1779
|
markBackground: backgroundRun.markBackground,
|
|
@@ -14,6 +14,7 @@ export interface ClaudeTeamAgentRuntimeOptions {
|
|
|
14
14
|
readonly hooks?: ClaudeHookRunner;
|
|
15
15
|
readonly contextAssembler?: ContextAssembler;
|
|
16
16
|
readonly providerForModel?: (model: string) => ModelProvider;
|
|
17
|
+
readonly providerForTurn?: (model?: string) => ModelProvider;
|
|
17
18
|
readonly permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
|
|
18
19
|
readonly eventSink?: RuntimeEventSink;
|
|
19
20
|
readonly approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
|
|
@@ -79,6 +79,9 @@ export class ClaudeTeamAgentRuntime {
|
|
|
79
79
|
...(this.options.providerForModel
|
|
80
80
|
? { providerForModel: this.options.providerForModel }
|
|
81
81
|
: {}),
|
|
82
|
+
...(this.options.providerForTurn
|
|
83
|
+
? { providerForTurn: this.options.providerForTurn }
|
|
84
|
+
: {}),
|
|
82
85
|
...(this.options.permissionResolverForMode
|
|
83
86
|
? {
|
|
84
87
|
permissionResolverForMode: this.options.permissionResolverForMode,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ModelDocument, type ModelImage, type RuntimeEventSink } from '../core/runtime.js';
|
|
2
|
-
import type
|
|
3
|
-
export type TurnTerminalState = Extract<LifecycleState, 'completed' | 'failed' | 'cancelled'>;
|
|
2
|
+
import { type ActiveTurnInputCommandResult, type ActiveTurnInputPort } from '../core/active-turn-input.js';
|
|
4
3
|
export type TurnActivation = {
|
|
5
4
|
kind: 'start';
|
|
6
5
|
sessionId: string;
|
|
@@ -28,13 +27,26 @@ export interface TurnRequest {
|
|
|
28
27
|
submission: TurnSubmission;
|
|
29
28
|
signal?: AbortSignal;
|
|
30
29
|
}
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
export interface TurnScope {
|
|
31
|
+
readonly emit: RuntimeEventSink;
|
|
32
|
+
readonly steering?: ActiveTurnInputPort;
|
|
33
|
+
}
|
|
34
|
+
export interface TurnCoordinatorOptions {
|
|
35
|
+
readonly eventSink: RuntimeEventSink;
|
|
36
|
+
readonly createSteeringId: () => string;
|
|
37
|
+
}
|
|
38
|
+
/** Owns the lifecycle and active-turn coordination for one session service. */
|
|
39
|
+
export declare class TurnCoordinator {
|
|
40
|
+
private readonly options;
|
|
41
|
+
private readonly activeTurns;
|
|
42
|
+
constructor(options: TurnCoordinatorOptions);
|
|
43
|
+
run<T>(request: TurnRequest, work: (scope: TurnScope) => Promise<T>): Promise<T>;
|
|
44
|
+
steer(sessionId: string, content: string): ActiveTurnInputCommandResult;
|
|
45
|
+
withdrawSteering(sessionId: string, id: string): ActiveTurnInputCommandResult;
|
|
46
|
+
close(): void;
|
|
47
|
+
private validateRequest;
|
|
48
|
+
private terminalState;
|
|
38
49
|
private transition;
|
|
50
|
+
private rejectPending;
|
|
39
51
|
}
|
|
40
52
|
//# sourceMappingURL=turn-lifecycle.d.ts.map
|
|
@@ -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
|
package/dist/cli-runtime.js
CHANGED
|
@@ -1477,9 +1477,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1477
1477
|
});
|
|
1478
1478
|
if (memoryDirectory)
|
|
1479
1479
|
await mkdir(memoryDirectory, { recursive: true });
|
|
1480
|
-
const projectMemoryProviderFactory =
|
|
1481
|
-
? () =>
|
|
1482
|
-
:
|
|
1480
|
+
const projectMemoryProviderFactory = providerForTurn
|
|
1481
|
+
? () => providerForTurn()
|
|
1482
|
+
: providerForMainModel && model
|
|
1483
|
+
? () => providerForMainModel(model)
|
|
1484
|
+
: undefined;
|
|
1483
1485
|
const projectMemoryRecall = projectMemoryPolicy.recall &&
|
|
1484
1486
|
memoryDirectory &&
|
|
1485
1487
|
projectMemoryProviderFactory
|
|
@@ -1911,6 +1913,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1911
1913
|
...(hooks ? { hooks } : {}),
|
|
1912
1914
|
...(contextAssembler ? { contextAssembler } : {}),
|
|
1913
1915
|
...(providerForModel ? { providerForModel } : {}),
|
|
1916
|
+
...(providerForTurn ? { providerForTurn } : {}),
|
|
1914
1917
|
permissionResolverForMode,
|
|
1915
1918
|
eventSink: runtimeEventSink,
|
|
1916
1919
|
}),
|