praxis-agent 0.20.21 → 0.21.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.
- package/README.md +11 -8
- package/dist/application/background-agent-manager.js +15 -10
- package/dist/application/session-memory.d.ts +96 -0
- package/dist/application/session-memory.js +383 -0
- package/dist/application/session-service.d.ts +18 -0
- package/dist/application/session-service.js +325 -65
- package/dist/application/subagent-service.d.ts +2 -1
- package/dist/application/subagent-service.js +147 -51
- package/dist/cli/interactive.js +17 -5
- package/dist/cli/tui/claude-style.d.ts +4 -0
- package/dist/cli/tui/claude-style.js +11 -0
- package/dist/cli-runtime.js +22 -18
- package/dist/compatibility/claude/schema.d.ts +7 -0
- package/dist/compatibility/claude/schema.js +22 -10
- package/dist/compatibility/claude/sidechain.d.ts +5 -1
- package/dist/compatibility/claude/sidechain.js +15 -2
- package/dist/core/context-budget.d.ts +53 -2
- package/dist/core/context-budget.js +117 -5
- package/dist/tools/claude-capabilities.d.ts +79 -0
- package/dist/tools/claude-capabilities.js +187 -0
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, creat
|
|
|
17
17
|
import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
|
|
18
18
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
19
19
|
import { usageCostUsd } from '../core/usage.js';
|
|
20
|
-
import { ContextBudget, estimateModelRequestTokens, } from '../core/context-budget.js';
|
|
20
|
+
import { ContextBudget, ContextRecoveryPlanner, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
21
21
|
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
22
22
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
23
23
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
@@ -33,7 +33,9 @@ import { SessionWorktreeManager } from './session-worktree.js';
|
|
|
33
33
|
import { ClaudeSessionCostTracker, } from './session-cost-tracker.js';
|
|
34
34
|
import { ClaudeWorktreeToolRegistry } from '../tools/claude-worktree-tools.js';
|
|
35
35
|
import { completeMeteredModelRequest } from './metered-model-completion.js';
|
|
36
|
+
import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, } from './session-memory.js';
|
|
36
37
|
import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
|
|
38
|
+
import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
|
|
37
39
|
import { generateToolUseSummary } from './tool-use-summary.js';
|
|
38
40
|
import { ClaudeUserMessageToolRegistry, CLAUDE_USER_MESSAGE_PROMPT, } from '../tools/claude-user-message.js';
|
|
39
41
|
function agentPermissionMode(mode) {
|
|
@@ -286,6 +288,19 @@ Reply with ONLY the suggestion, no quotes or explanation.
|
|
|
286
288
|
Use 2-12 words. Do not ask a question, evaluate the prior response, introduce a new idea, or use a Claude voice. If the topic is unsafe or sensitive, reply with an empty string.`;
|
|
287
289
|
const SESSION_NAME_INSTRUCTION = `Generate a concise name for this coding session based on the conversation.
|
|
288
290
|
Use 2-5 short words in kebab-case. Reply with ONLY the name, without quotes, punctuation, or explanation.`;
|
|
291
|
+
const SESSION_MEMORY_MAX_LINES = 200;
|
|
292
|
+
const SESSION_MEMORY_MAX_CHARS = 32_000;
|
|
293
|
+
const SESSION_MEMORY_EXTRACTION_PROMPT = `You maintain durable session memory for one coding session.
|
|
294
|
+
|
|
295
|
+
Update the durable session memory from the conversation so far. Preserve:
|
|
296
|
+
- The user's intent and current goals
|
|
297
|
+
- Decisions made and the reasoning behind them
|
|
298
|
+
- Active constraints, requirements, and preferences
|
|
299
|
+
- Pending work, blockers, and next steps
|
|
300
|
+
|
|
301
|
+
Omit transient chatter, credentials, secrets, or personal data.
|
|
302
|
+
|
|
303
|
+
Return ONLY an updated Markdown document that becomes the session's durable memory. Do not call tools. Do not include any prose outside the Markdown document.`;
|
|
289
304
|
function validPromptSuggestion(value) {
|
|
290
305
|
const suggestion = value.trim();
|
|
291
306
|
if (!suggestion)
|
|
@@ -323,6 +338,7 @@ export class ClaudeSessionService {
|
|
|
323
338
|
sessionCostTrackers = new Map();
|
|
324
339
|
activeCostSessionId;
|
|
325
340
|
closeCostSavePromise;
|
|
341
|
+
sessionMemoryControllers = new Map();
|
|
326
342
|
runtimeCwd;
|
|
327
343
|
constructor(options) {
|
|
328
344
|
this.options = options;
|
|
@@ -456,6 +472,7 @@ export class ClaudeSessionService {
|
|
|
456
472
|
await Promise.all([...this.backgroundNotificationWrites.values()]);
|
|
457
473
|
this.hostedSubagents.clear();
|
|
458
474
|
this.backgroundTasks.clear();
|
|
475
|
+
this.sessionMemoryControllers.clear();
|
|
459
476
|
await this.workflowManager?.close();
|
|
460
477
|
this.closeCostSavePromise ??= this.persistActiveSessionCost();
|
|
461
478
|
await this.closeCostSavePromise;
|
|
@@ -467,7 +484,10 @@ export class ClaudeSessionService {
|
|
|
467
484
|
if (!baseTools)
|
|
468
485
|
throw new Error('Hosted tool registry requires base tools');
|
|
469
486
|
const paths = this.paths(sessionId);
|
|
470
|
-
const
|
|
487
|
+
const capabilities = this.toolCapabilities();
|
|
488
|
+
const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
|
|
489
|
+
const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
|
|
490
|
+
const taskTools = taskToolNames.length > 0
|
|
471
491
|
? new ClaudeTaskToolRegistry({
|
|
472
492
|
base: baseTools,
|
|
473
493
|
cwd: this.activeCwd(),
|
|
@@ -478,22 +498,17 @@ export class ClaudeSessionService {
|
|
|
478
498
|
...(this.options.eventSink
|
|
479
499
|
? { eventSink: this.options.eventSink }
|
|
480
500
|
: {}),
|
|
481
|
-
|
|
482
|
-
? { enabledTools: this.options.taskToolNames }
|
|
483
|
-
: {}),
|
|
501
|
+
enabledTools: taskToolNames,
|
|
484
502
|
})
|
|
485
503
|
: null;
|
|
486
504
|
if (taskTools)
|
|
487
505
|
this.backgroundTasks.registerBash(sessionId, taskTools);
|
|
488
|
-
const scheduledTools = this.scheduledPrompts &&
|
|
489
|
-
(this.options.scheduledToolNames?.length ?? 0) > 0
|
|
506
|
+
const scheduledTools = this.scheduledPrompts && scheduledToolNames.length > 0
|
|
490
507
|
? new ClaudeScheduledToolRegistry({
|
|
491
508
|
base: taskTools ?? baseTools,
|
|
492
509
|
manager: this.scheduledPrompts,
|
|
493
510
|
sessionId,
|
|
494
|
-
|
|
495
|
-
? { enabledTools: this.options.scheduledToolNames }
|
|
496
|
-
: {}),
|
|
511
|
+
enabledTools: scheduledToolNames,
|
|
497
512
|
})
|
|
498
513
|
: null;
|
|
499
514
|
const wrappedBase = scheduledTools ?? taskTools ?? baseTools;
|
|
@@ -522,7 +537,9 @@ export class ClaudeSessionService {
|
|
|
522
537
|
parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
|
|
523
538
|
this.options.permissionMode),
|
|
524
539
|
...(this.options.subagentToolNames
|
|
525
|
-
? {
|
|
540
|
+
? {
|
|
541
|
+
toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
|
|
542
|
+
}
|
|
526
543
|
: {}),
|
|
527
544
|
...(this.options.extensions
|
|
528
545
|
? { extensions: this.options.extensions }
|
|
@@ -574,7 +591,7 @@ export class ClaudeSessionService {
|
|
|
574
591
|
promptIdForCall: (callId) => callId,
|
|
575
592
|
defaultModel: this.options.provider?.model ?? 'praxis/provider',
|
|
576
593
|
tokenBudget: null,
|
|
577
|
-
enabled:
|
|
594
|
+
enabled: capabilities.has('Workflow'),
|
|
578
595
|
})
|
|
579
596
|
: agentTools;
|
|
580
597
|
if (this.worktreeManager)
|
|
@@ -602,6 +619,7 @@ export class ClaudeSessionService {
|
|
|
602
619
|
const interactiveRegistry = this.options.interactiveTools
|
|
603
620
|
? this.options.interactiveTools.registry(messageRegistry, sessionId)
|
|
604
621
|
: messageRegistry;
|
|
622
|
+
const capabilityRegistry = new ClaudeCapabilityToolRegistry(interactiveRegistry, capabilities);
|
|
605
623
|
const preferredOrder = [
|
|
606
624
|
'Agent',
|
|
607
625
|
'AskUserQuestion',
|
|
@@ -637,7 +655,7 @@ export class ClaudeSessionService {
|
|
|
637
655
|
];
|
|
638
656
|
const hostedRegistry = {
|
|
639
657
|
definitions: () => {
|
|
640
|
-
const definitions =
|
|
658
|
+
const definitions = capabilityRegistry.definitions();
|
|
641
659
|
return [...definitions].sort((left, right) => {
|
|
642
660
|
const leftIndex = preferredOrder.indexOf(left.name);
|
|
643
661
|
const rightIndex = preferredOrder.indexOf(right.name);
|
|
@@ -645,8 +663,8 @@ export class ClaudeSessionService {
|
|
|
645
663
|
(rightIndex < 0 ? preferredOrder.length : rightIndex));
|
|
646
664
|
});
|
|
647
665
|
},
|
|
648
|
-
prepare: (call, context) =>
|
|
649
|
-
execute: (call, context) =>
|
|
666
|
+
prepare: (call, context) => capabilityRegistry.prepare(call, context),
|
|
667
|
+
execute: (call, context) => capabilityRegistry.execute(call, context),
|
|
650
668
|
};
|
|
651
669
|
if (subagentExecutor) {
|
|
652
670
|
this.hostedSubagentsByRegistry.set(hostedRegistry, subagentExecutor);
|
|
@@ -718,6 +736,7 @@ export class ClaudeSessionService {
|
|
|
718
736
|
...(onDelta ? { onTextDelta: onDelta } : {}),
|
|
719
737
|
onMetrics: (recorded) => this.recordAuxiliaryMetrics(activeSessionId, recorded),
|
|
720
738
|
});
|
|
739
|
+
budget?.observeUsage(metrics.usage);
|
|
721
740
|
if (metrics.toolCalls.length > 0) {
|
|
722
741
|
throw new Error('Side questions cannot call tools; press f to fork');
|
|
723
742
|
}
|
|
@@ -1865,7 +1884,10 @@ export class ClaudeSessionService {
|
|
|
1865
1884
|
snapshot = { entries: history, tail: appendResult.tail };
|
|
1866
1885
|
pendingRecoveryHookOutcomes.length = 0;
|
|
1867
1886
|
};
|
|
1868
|
-
const
|
|
1887
|
+
const capabilities = this.toolCapabilities();
|
|
1888
|
+
const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
|
|
1889
|
+
const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
|
|
1890
|
+
const taskTools = this.options.tools && taskToolNames.length > 0
|
|
1869
1891
|
? new ClaudeTaskToolRegistry({
|
|
1870
1892
|
base: this.options.tools,
|
|
1871
1893
|
cwd: this.activeCwd(),
|
|
@@ -1876,21 +1898,17 @@ export class ClaudeSessionService {
|
|
|
1876
1898
|
...(this.options.eventSink
|
|
1877
1899
|
? { eventSink: this.options.eventSink }
|
|
1878
1900
|
: {}),
|
|
1879
|
-
|
|
1880
|
-
? { enabledTools: this.options.taskToolNames }
|
|
1881
|
-
: {}),
|
|
1901
|
+
enabledTools: taskToolNames,
|
|
1882
1902
|
})
|
|
1883
1903
|
: null;
|
|
1884
1904
|
const scheduledTools = this.scheduledPrompts &&
|
|
1885
1905
|
this.options.tools &&
|
|
1886
|
-
|
|
1906
|
+
scheduledToolNames.length > 0
|
|
1887
1907
|
? new ClaudeScheduledToolRegistry({
|
|
1888
1908
|
base: taskTools ?? this.options.tools,
|
|
1889
1909
|
manager: this.scheduledPrompts,
|
|
1890
1910
|
sessionId,
|
|
1891
|
-
|
|
1892
|
-
? { enabledTools: this.options.scheduledToolNames }
|
|
1893
|
-
: {}),
|
|
1911
|
+
enabledTools: scheduledToolNames,
|
|
1894
1912
|
})
|
|
1895
1913
|
: null;
|
|
1896
1914
|
const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
|
|
@@ -1919,7 +1937,9 @@ export class ClaudeSessionService {
|
|
|
1919
1937
|
parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
|
|
1920
1938
|
this.options.permissionMode),
|
|
1921
1939
|
...(this.options.subagentToolNames
|
|
1922
|
-
? {
|
|
1940
|
+
? {
|
|
1941
|
+
toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
|
|
1942
|
+
}
|
|
1923
1943
|
: {}),
|
|
1924
1944
|
...(this.options.extensions
|
|
1925
1945
|
? { extensions: this.options.extensions }
|
|
@@ -1975,7 +1995,7 @@ export class ClaudeSessionService {
|
|
|
1975
1995
|
this.promptIdForToolCall(snapshot.entries, callId),
|
|
1976
1996
|
defaultModel: provider.model ?? 'praxis/provider',
|
|
1977
1997
|
tokenBudget: workflowTokenTarget(effectivePrompt),
|
|
1978
|
-
enabled:
|
|
1998
|
+
enabled: capabilities.has('Workflow'),
|
|
1979
1999
|
})
|
|
1980
2000
|
: agentTools;
|
|
1981
2001
|
const workspaceTools = this.worktreeManager &&
|
|
@@ -2048,14 +2068,17 @@ export class ClaudeSessionService {
|
|
|
2048
2068
|
},
|
|
2049
2069
|
}
|
|
2050
2070
|
: interactiveMessageTools;
|
|
2071
|
+
const capabilityTools = fileHistoryTools
|
|
2072
|
+
? new ClaudeCapabilityToolRegistry(fileHistoryTools, capabilities)
|
|
2073
|
+
: undefined;
|
|
2051
2074
|
const structuredCapture = this.options.structuredOutputSchema
|
|
2052
2075
|
? { calls: 0, value: undefined }
|
|
2053
2076
|
: undefined;
|
|
2054
|
-
const agentScopedTools = agent &&
|
|
2055
|
-
? new FilteredToolRegistry(
|
|
2056
|
-
tools: mainAgentToolNames(
|
|
2077
|
+
const agentScopedTools = agent && capabilityTools
|
|
2078
|
+
? new FilteredToolRegistry(capabilityTools, {
|
|
2079
|
+
tools: mainAgentToolNames(capabilityTools, agent),
|
|
2057
2080
|
})
|
|
2058
|
-
:
|
|
2081
|
+
: capabilityTools;
|
|
2059
2082
|
const structuredTools = this.options.structuredOutputSchema && structuredCapture
|
|
2060
2083
|
? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
|
|
2061
2084
|
: agentScopedTools;
|
|
@@ -2095,6 +2118,7 @@ export class ClaudeSessionService {
|
|
|
2095
2118
|
}),
|
|
2096
2119
|
});
|
|
2097
2120
|
let currentTurnUserMessages = null;
|
|
2121
|
+
let currentTurnToolCalls = 0;
|
|
2098
2122
|
const observer = {
|
|
2099
2123
|
assistantCompleted: async (message) => {
|
|
2100
2124
|
const [entry] = translateProviderEvents([
|
|
@@ -2117,6 +2141,7 @@ export class ClaudeSessionService {
|
|
|
2117
2141
|
typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
|
|
2118
2142
|
},
|
|
2119
2143
|
toolCompleted: async (call, toolResult) => {
|
|
2144
|
+
currentTurnToolCalls += 1;
|
|
2120
2145
|
const transition = this.worktreeManager?.consumeTransition(call.id);
|
|
2121
2146
|
if (transition) {
|
|
2122
2147
|
const stateEntry = {
|
|
@@ -2261,37 +2286,53 @@ export class ClaudeSessionService {
|
|
|
2261
2286
|
tail: settingTail,
|
|
2262
2287
|
};
|
|
2263
2288
|
}
|
|
2264
|
-
const
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
?
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2289
|
+
const sessionMemory = this.sessionMemoryController(sessionId);
|
|
2290
|
+
let assembledContext;
|
|
2291
|
+
let agentSystem = null;
|
|
2292
|
+
let planModeMessage;
|
|
2293
|
+
let sessionMemoryMessage = null;
|
|
2294
|
+
let contextMessages = [];
|
|
2295
|
+
const refreshRuntimeContext = async () => {
|
|
2296
|
+
assembledContext = await this.options.contextAssembler?.assemble({
|
|
2297
|
+
cwd: this.activeCwd(),
|
|
2298
|
+
});
|
|
2299
|
+
agentSystem = await this.mainAgentSystemPrompt(agent);
|
|
2300
|
+
const assembledSystemMessages = this.assembledSystemMessages(agent, assembledContext?.systemMessages ?? []);
|
|
2301
|
+
planModeMessage =
|
|
2302
|
+
this.options.interactiveTools?.contextMessage(sessionId);
|
|
2303
|
+
sessionMemoryMessage = sessionMemory
|
|
2304
|
+
? this.sessionMemoryMessage(await sessionMemory.summary())
|
|
2305
|
+
: null;
|
|
2306
|
+
contextMessages = [
|
|
2307
|
+
...(agentSystem
|
|
2308
|
+
? [{ role: 'system', content: agentSystem }]
|
|
2309
|
+
: []),
|
|
2310
|
+
...assembledSystemMessages,
|
|
2311
|
+
...(planModeMessage
|
|
2312
|
+
? [{ role: 'system', content: planModeMessage }]
|
|
2313
|
+
: []),
|
|
2314
|
+
...(sessionMemoryMessage
|
|
2315
|
+
? [{ role: 'system', content: sessionMemoryMessage }]
|
|
2316
|
+
: []),
|
|
2317
|
+
...(this.options.brief
|
|
2318
|
+
? [
|
|
2319
|
+
{
|
|
2320
|
+
role: 'system',
|
|
2321
|
+
content: CLAUDE_USER_MESSAGE_PROMPT,
|
|
2322
|
+
},
|
|
2323
|
+
]
|
|
2324
|
+
: []),
|
|
2325
|
+
...(this.options.structuredOutputSchema
|
|
2326
|
+
? [
|
|
2327
|
+
{
|
|
2328
|
+
role: 'system',
|
|
2329
|
+
content: 'You MUST call StructuredOutput exactly once at the end with a value matching the requested JSON Schema.',
|
|
2330
|
+
},
|
|
2331
|
+
]
|
|
2332
|
+
: []),
|
|
2333
|
+
];
|
|
2334
|
+
};
|
|
2335
|
+
await refreshRuntimeContext();
|
|
2295
2336
|
const expansion = skipUserPrompt
|
|
2296
2337
|
? { userMessages: [] }
|
|
2297
2338
|
: shellCommand === undefined
|
|
@@ -2321,6 +2362,7 @@ export class ClaudeSessionService {
|
|
|
2321
2362
|
? (structuredTools?.definitions() ?? [])
|
|
2322
2363
|
: [];
|
|
2323
2364
|
const budget = this.contextBudget(provider);
|
|
2365
|
+
const recoveryPlanner = new ContextRecoveryPlanner();
|
|
2324
2366
|
const pendingUserMessages = expandedMessages.map((message, index) => ({
|
|
2325
2367
|
role: 'user',
|
|
2326
2368
|
content: message.text,
|
|
@@ -2373,14 +2415,14 @@ export class ClaudeSessionService {
|
|
|
2373
2415
|
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, assembledContext?.firstUserMessageContext);
|
|
2374
2416
|
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
2375
2417
|
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
2376
|
-
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = []) => {
|
|
2418
|
+
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
|
|
2377
2419
|
if (!budget || this.options.autoCompact === false)
|
|
2378
2420
|
return;
|
|
2379
2421
|
const historyMessages = projectClaudeModelMessages(snapshot.entries);
|
|
2380
2422
|
const predicted = budget.evaluate([
|
|
2381
2423
|
...contextMessages,
|
|
2382
2424
|
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
2383
|
-
], definitions);
|
|
2425
|
+
], definitions, options);
|
|
2384
2426
|
if (!predicted.shouldCompact)
|
|
2385
2427
|
return;
|
|
2386
2428
|
const irreducibleMessages = [
|
|
@@ -2403,6 +2445,9 @@ export class ClaudeSessionService {
|
|
|
2403
2445
|
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
2404
2446
|
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
2405
2447
|
}
|
|
2448
|
+
if (sessionMemory) {
|
|
2449
|
+
await sessionMemory.waitForIdle();
|
|
2450
|
+
}
|
|
2406
2451
|
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
2407
2452
|
const compactEnvelope = budget.evaluate([
|
|
2408
2453
|
...irreducibleMessages,
|
|
@@ -2555,6 +2600,22 @@ export class ClaudeSessionService {
|
|
|
2555
2600
|
entries: [...snapshot.entries, ...entries],
|
|
2556
2601
|
tail: appendResult.tail,
|
|
2557
2602
|
};
|
|
2603
|
+
// The boundary is durable: mirror Claude's full-compact behavior by
|
|
2604
|
+
// rerunning SessionStart with source compact and refreshing the
|
|
2605
|
+
// runtime-only context so the next request retains current
|
|
2606
|
+
// instructions, plan state, session memory, and hook context.
|
|
2607
|
+
if (this.options.hooks) {
|
|
2608
|
+
const outcome = await this.options.hooks.run({
|
|
2609
|
+
...hookSession,
|
|
2610
|
+
hook_event_name: 'SessionStart',
|
|
2611
|
+
source: 'compact',
|
|
2612
|
+
}, 'compact', signal);
|
|
2613
|
+
await recordHookOutcome(outcome);
|
|
2614
|
+
if (outcome.blockedReason) {
|
|
2615
|
+
throw new Error(`SessionStart hook error: ${outcome.blockedReason}`);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
await refreshRuntimeContext();
|
|
2558
2619
|
this.options.eventSink?.({
|
|
2559
2620
|
type: 'compact-boundary',
|
|
2560
2621
|
trigger: 'auto',
|
|
@@ -2823,9 +2884,55 @@ export class ClaudeSessionService {
|
|
|
2823
2884
|
permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
|
|
2824
2885
|
onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
|
|
2825
2886
|
};
|
|
2826
|
-
const
|
|
2827
|
-
?
|
|
2828
|
-
:
|
|
2887
|
+
const attemptMainTurn = () => signal
|
|
2888
|
+
? runtime.run({ ...runtimeRequest, signal })
|
|
2889
|
+
: runtime.run(runtimeRequest);
|
|
2890
|
+
let result;
|
|
2891
|
+
try {
|
|
2892
|
+
result = await attemptMainTurn();
|
|
2893
|
+
}
|
|
2894
|
+
catch (error) {
|
|
2895
|
+
if (!budget || !isPromptTooLongError(error))
|
|
2896
|
+
throw error;
|
|
2897
|
+
if (recoveryPlanner.consumeReactiveRetry() !== 'reactive-retry')
|
|
2898
|
+
throw error;
|
|
2899
|
+
try {
|
|
2900
|
+
await compactIfNeeded([], currentTurnUserMessages ?? [], {
|
|
2901
|
+
promptTooLong: true,
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
catch {
|
|
2905
|
+
// Compaction could not free the provider-bounded context; surface
|
|
2906
|
+
// the original prompt-too-long error rather than the compaction
|
|
2907
|
+
// failure.
|
|
2908
|
+
throw error;
|
|
2909
|
+
}
|
|
2910
|
+
// The single reactive retry must use the compacted transcript, not
|
|
2911
|
+
// the stale request copy captured before the compact boundary.
|
|
2912
|
+
runtimeRequest.messages = [
|
|
2913
|
+
...contextMessages,
|
|
2914
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
2915
|
+
];
|
|
2916
|
+
try {
|
|
2917
|
+
result = await attemptMainTurn();
|
|
2918
|
+
}
|
|
2919
|
+
catch {
|
|
2920
|
+
// Exactly one reactive retry is consumed; fail deterministically
|
|
2921
|
+
// and surface the original prompt-too-long error.
|
|
2922
|
+
recoveryPlanner.recordFailure();
|
|
2923
|
+
throw error;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
recoveryPlanner.recordSuccess();
|
|
2927
|
+
const mainModel = provider.model !== undefined && provider.model.trim() !== ''
|
|
2928
|
+
? provider.model
|
|
2929
|
+
: undefined;
|
|
2930
|
+
const observedUsage = (mainModel !== undefined
|
|
2931
|
+
? result.modelUsage?.[mainModel]
|
|
2932
|
+
: undefined) ??
|
|
2933
|
+
Object.values(result.modelUsage ?? {})[0] ??
|
|
2934
|
+
result.usage;
|
|
2935
|
+
budget?.observeUsage(observedUsage);
|
|
2829
2936
|
if (structuredCapture && structuredCapture.calls !== 1) {
|
|
2830
2937
|
throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
|
|
2831
2938
|
}
|
|
@@ -2920,6 +3027,21 @@ export class ClaudeSessionService {
|
|
|
2920
3027
|
: { linesRemoved: foregroundLineChanges.linesRemoved }),
|
|
2921
3028
|
});
|
|
2922
3029
|
}
|
|
3030
|
+
if (sessionMemory && finalLeafUuid) {
|
|
3031
|
+
const turnInputTokens = result.usage?.inputTokens ?? 0;
|
|
3032
|
+
try {
|
|
3033
|
+
await sessionMemory.observeDelta(turnInputTokens, currentTurnToolCalls, finalLeafUuid, projectClaudeModelMessages(snapshot.entries));
|
|
3034
|
+
await sessionMemory.waitForIdle();
|
|
3035
|
+
}
|
|
3036
|
+
catch (error) {
|
|
3037
|
+
// A failed extraction must not fail the user turn; the sidecar
|
|
3038
|
+
// retains a retryable error for the next observation.
|
|
3039
|
+
this.options.eventSink?.({
|
|
3040
|
+
type: 'warning',
|
|
3041
|
+
message: `Session memory extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
2923
3045
|
return {
|
|
2924
3046
|
sessionId,
|
|
2925
3047
|
text: structuredCapture && structuredCapture.calls === 1
|
|
@@ -3621,11 +3743,149 @@ export class ClaudeSessionService {
|
|
|
3621
3743
|
return null;
|
|
3622
3744
|
return new ContextBudget({
|
|
3623
3745
|
contextWindowTokens,
|
|
3746
|
+
windowSource: 'capability',
|
|
3624
3747
|
...(this.options.contextReserveTokens === undefined
|
|
3625
3748
|
? {}
|
|
3626
3749
|
: { reserveTokens: this.options.contextReserveTokens }),
|
|
3627
3750
|
});
|
|
3628
3751
|
}
|
|
3752
|
+
sessionMemoryEnabled() {
|
|
3753
|
+
if (this.options.enableSessionMemory === false)
|
|
3754
|
+
return false;
|
|
3755
|
+
if (this.options.sessionPersistence === false)
|
|
3756
|
+
return false;
|
|
3757
|
+
return true;
|
|
3758
|
+
}
|
|
3759
|
+
sessionMemoryController(sessionId) {
|
|
3760
|
+
if (!this.sessionMemoryEnabled() || !isClaudeSessionId(sessionId)) {
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
let controller = this.sessionMemoryControllers.get(sessionId);
|
|
3764
|
+
if (!controller) {
|
|
3765
|
+
controller = new SessionMemoryController({
|
|
3766
|
+
store: new SessionMemoryStore({
|
|
3767
|
+
configRoot: this.options.configRoot,
|
|
3768
|
+
sessionId,
|
|
3769
|
+
}),
|
|
3770
|
+
extractor: (input) => this.extractSessionMemory(sessionId, input),
|
|
3771
|
+
});
|
|
3772
|
+
this.sessionMemoryControllers.set(sessionId, controller);
|
|
3773
|
+
}
|
|
3774
|
+
return controller;
|
|
3775
|
+
}
|
|
3776
|
+
sessionMemoryMessage(summary) {
|
|
3777
|
+
const bounded = this.boundSessionMemorySummary(summary.trim());
|
|
3778
|
+
if (bounded.length === 0)
|
|
3779
|
+
return null;
|
|
3780
|
+
return `# Session Memory\n\n${bounded}`;
|
|
3781
|
+
}
|
|
3782
|
+
boundSessionMemorySummary(summary) {
|
|
3783
|
+
const lines = summary.split('\n');
|
|
3784
|
+
const content = lines.length > SESSION_MEMORY_MAX_LINES
|
|
3785
|
+
? lines.slice(0, SESSION_MEMORY_MAX_LINES).join('\n')
|
|
3786
|
+
: summary;
|
|
3787
|
+
return content.length > SESSION_MEMORY_MAX_CHARS
|
|
3788
|
+
? content.slice(0, SESSION_MEMORY_MAX_CHARS)
|
|
3789
|
+
: content;
|
|
3790
|
+
}
|
|
3791
|
+
formatSessionMemoryConversation(messages) {
|
|
3792
|
+
const parts = [];
|
|
3793
|
+
for (const message of messages) {
|
|
3794
|
+
if (message.role === 'system') {
|
|
3795
|
+
parts.push(`System: ${message.content}`);
|
|
3796
|
+
}
|
|
3797
|
+
else if (message.role === 'user') {
|
|
3798
|
+
const body = message.contentBlocks === undefined
|
|
3799
|
+
? message.content
|
|
3800
|
+
: message.contentBlocks
|
|
3801
|
+
.map((block) => block.type === 'text'
|
|
3802
|
+
? block.text
|
|
3803
|
+
: block.type === 'image'
|
|
3804
|
+
? '[image]'
|
|
3805
|
+
: '[document]')
|
|
3806
|
+
.join('\n');
|
|
3807
|
+
parts.push(`User: ${body}`);
|
|
3808
|
+
}
|
|
3809
|
+
else if (message.role === 'assistant') {
|
|
3810
|
+
const toolCalls = message.toolCalls === undefined || message.toolCalls.length === 0
|
|
3811
|
+
? ''
|
|
3812
|
+
: `\n${message.toolCalls
|
|
3813
|
+
.map((call) => `${call.name}(${JSON.stringify(call.input)})`)
|
|
3814
|
+
.join('\n')}`;
|
|
3815
|
+
parts.push(`Assistant: ${message.content}${toolCalls}`);
|
|
3816
|
+
}
|
|
3817
|
+
else {
|
|
3818
|
+
parts.push(`Tool result: ${message.content}`);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
return this.boundSessionMemorySummary(parts.join('\n\n'));
|
|
3822
|
+
}
|
|
3823
|
+
async extractSessionMemory(sessionId, input) {
|
|
3824
|
+
const provider = this.provider();
|
|
3825
|
+
const messages = [
|
|
3826
|
+
{ role: 'system', content: SESSION_MEMORY_EXTRACTION_PROMPT },
|
|
3827
|
+
...(input.summary.trim().length === 0
|
|
3828
|
+
? []
|
|
3829
|
+
: [
|
|
3830
|
+
{
|
|
3831
|
+
role: 'system',
|
|
3832
|
+
content: `Previous session memory summary:\n\n${this.boundSessionMemorySummary(input.summary)}`,
|
|
3833
|
+
},
|
|
3834
|
+
]),
|
|
3835
|
+
...(input.messages === undefined || input.messages.length === 0
|
|
3836
|
+
? []
|
|
3837
|
+
: [
|
|
3838
|
+
{
|
|
3839
|
+
role: 'user',
|
|
3840
|
+
content: `Conversation so far:\n\n${this.formatSessionMemoryConversation(input.messages)}`,
|
|
3841
|
+
},
|
|
3842
|
+
]),
|
|
3843
|
+
];
|
|
3844
|
+
const metrics = await completeMeteredModelRequest(provider, { messages }, {
|
|
3845
|
+
onMetrics: (recorded) => this.recordAuxiliaryMetrics(sessionId, recorded),
|
|
3846
|
+
});
|
|
3847
|
+
if (metrics.toolCalls.length > 0) {
|
|
3848
|
+
throw new SessionMemoryStateError('Session memory extraction must not call tools');
|
|
3849
|
+
}
|
|
3850
|
+
const summary = metrics.text.trim();
|
|
3851
|
+
if (summary.length === 0) {
|
|
3852
|
+
throw new SessionMemoryStateError('Session memory extractor returned an empty summary');
|
|
3853
|
+
}
|
|
3854
|
+
return summary;
|
|
3855
|
+
}
|
|
3856
|
+
toolCapabilities() {
|
|
3857
|
+
const taskNames = this.options.taskToolNames ?? [];
|
|
3858
|
+
const input = {
|
|
3859
|
+
role: this.options.toolRole ?? 'main',
|
|
3860
|
+
interactive: this.options.interactiveTools !== undefined,
|
|
3861
|
+
simpleMode: this.options.simpleMode ?? false,
|
|
3862
|
+
tasks: taskNames.some((name) => ['TaskCreate', 'TaskGet', 'TaskList', 'TaskUpdate'].includes(name)),
|
|
3863
|
+
agentTriggers: (this.options.scheduledToolNames?.length ?? 0) > 0,
|
|
3864
|
+
backgroundAgents: taskNames.some((name) => ['TaskOutput', 'TaskStop'].includes(name)),
|
|
3865
|
+
...(this.options.enableWorkflows === undefined
|
|
3866
|
+
? {}
|
|
3867
|
+
: { workflowScripts: this.options.enableWorkflows }),
|
|
3868
|
+
...(this.options.enableSubagents === undefined
|
|
3869
|
+
? {}
|
|
3870
|
+
: { subagents: this.options.enableSubagents }),
|
|
3871
|
+
...(this.options.toolCapabilityEnvironment
|
|
3872
|
+
? { env: this.options.toolCapabilityEnvironment }
|
|
3873
|
+
: {}),
|
|
3874
|
+
};
|
|
3875
|
+
return resolveClaudeToolCapabilities(input);
|
|
3876
|
+
}
|
|
3877
|
+
capabilityToolNames(names, capabilities) {
|
|
3878
|
+
return (names ?? [])
|
|
3879
|
+
.filter((name) => !name.startsWith('Task') || capabilities.has(name))
|
|
3880
|
+
.filter((name) => ![
|
|
3881
|
+
'Workflow',
|
|
3882
|
+
'Agent',
|
|
3883
|
+
'CronCreate',
|
|
3884
|
+
'CronDelete',
|
|
3885
|
+
'CronList',
|
|
3886
|
+
'ScheduleWakeup',
|
|
3887
|
+
].includes(name) || capabilities.has(name));
|
|
3888
|
+
}
|
|
3629
3889
|
async append(lease, tail, entry) {
|
|
3630
3890
|
const result = await lease.append(tail, entry);
|
|
3631
3891
|
if (result.status === 'conflict') {
|
|
@@ -112,7 +112,8 @@ export declare class ClaudeSubagentExecutor {
|
|
|
112
112
|
durationApiWithoutRetriesMs?: number;
|
|
113
113
|
}>;
|
|
114
114
|
private hydrateCompletedTask;
|
|
115
|
-
private
|
|
115
|
+
private resolvePersistedSidechain;
|
|
116
|
+
private discoverSidechainCandidates;
|
|
116
117
|
private asyncLaunchResult;
|
|
117
118
|
private runSidechain;
|
|
118
119
|
}
|