praxis-agent 0.30.0 → 0.32.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/dist/application/claude-project-purge.js +18 -5
- package/dist/application/project-memory.d.ts +141 -0
- package/dist/application/project-memory.js +607 -0
- package/dist/application/session-service.d.ts +24 -0
- package/dist/application/session-service.js +412 -44
- package/dist/application/subagent-service.js +6 -0
- package/dist/cli/interactive.d.ts +3 -1
- package/dist/cli/interactive.js +37 -4
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +24 -0
- package/dist/cli/tui/hook-settings.js +14 -1
- package/dist/cli/tui/memory-files.d.ts +2 -1
- package/dist/cli/tui/memory-files.js +30 -30
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +144 -24
- package/dist/compatibility/claude/context.d.ts +6 -1
- package/dist/compatibility/claude/context.js +16 -2
- package/dist/compatibility/claude/dynamic-context.js +2 -1
- package/dist/compatibility/claude/paths.js +2 -13
- package/dist/compatibility/claude/shared-resources.d.ts +4 -4
- package/dist/compatibility/claude/shared-resources.js +14 -35
- package/dist/core/context-budget.d.ts +9 -8
- package/dist/core/context-budget.js +16 -21
- package/dist/core/project-memory.d.ts +19 -0
- package/dist/core/project-memory.js +58 -0
- package/dist/core/runtime.d.ts +10 -1
- package/dist/core/runtime.js +47 -3
- package/dist/hooks/claude-file-change-watcher.d.ts +43 -0
- package/dist/hooks/claude-file-change-watcher.js +185 -0
- package/dist/hooks/claude-hook-tools.d.ts +2 -0
- package/dist/hooks/claude-hook-tools.js +45 -6
- package/dist/hooks/claude-hooks.d.ts +14 -2
- package/dist/hooks/claude-hooks.js +230 -5
- package/dist/hooks/claude-session-environment.d.ts +16 -0
- package/dist/hooks/claude-session-environment.js +112 -0
- package/dist/maintenance/doctor.js +29 -5
- package/dist/mcp/claude-mcp-management.js +2 -0
- package/dist/persistence/native-resources.d.ts +3 -1
- package/dist/persistence/native-resources.js +19 -6
- package/dist/platform/path-containment.d.ts +2 -0
- package/dist/platform/path-containment.js +7 -0
- package/dist/platform/project-identity.d.ts +6 -0
- package/dist/platform/project-identity.js +66 -0
- package/dist/platform/project-memory-paths.d.ts +13 -0
- package/dist/platform/project-memory-paths.js +28 -0
- package/dist/platform/project-path-key.d.ts +2 -0
- package/dist/platform/project-path-key.js +16 -0
- package/dist/providers/fallback-provider.js +3 -1
- package/dist/tools/claude-task-tools.d.ts +5 -0
- package/dist/tools/claude-task-tools.js +15 -1
- package/dist/tools/local-tools.d.ts +2 -0
- package/dist/tools/local-tools.js +11 -4
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, matchesGlob, relative, resolve, sep } from 'node:path';
|
|
2
2
|
import { PromptComposer, } from '../../core/prompt-composer.js';
|
|
3
|
+
import { boundProjectMemoryIndex } from '../../core/project-memory.js';
|
|
3
4
|
import { renderClaudeDynamicSystemContext, renderClaudeDynamicUserContext, } from './dynamic-context.js';
|
|
4
5
|
function renderResources(title, resources) {
|
|
5
6
|
const rendered = resources
|
|
@@ -12,7 +13,7 @@ function renderResources(title, resources) {
|
|
|
12
13
|
function limitMemoryIndex(resource) {
|
|
13
14
|
return {
|
|
14
15
|
...resource,
|
|
15
|
-
content: resource.content
|
|
16
|
+
content: boundProjectMemoryIndex(resource.content),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
export class ClaudeConditionalRuleResolver {
|
|
@@ -170,6 +171,8 @@ ${sections.join('\n\n')}`,
|
|
|
170
171
|
if (options.reason === 'resource-reload' ||
|
|
171
172
|
options.reason === 'compact') {
|
|
172
173
|
delete snapshot.resources;
|
|
174
|
+
snapshot.resourceLoadReason =
|
|
175
|
+
options.reason === 'compact' ? 'compact' : 'resource_reload';
|
|
173
176
|
continue;
|
|
174
177
|
}
|
|
175
178
|
if (options.reason === 'tool-pool') {
|
|
@@ -198,7 +201,18 @@ ${sections.join('\n\n')}`,
|
|
|
198
201
|
if (!snapshot)
|
|
199
202
|
return this.options.loadResources(cwd);
|
|
200
203
|
if (!snapshot.resources) {
|
|
201
|
-
const
|
|
204
|
+
const reason = snapshot.resourceLoadReason ?? 'session_start';
|
|
205
|
+
delete snapshot.resourceLoadReason;
|
|
206
|
+
const pending = this.options
|
|
207
|
+
.loadResources(cwd)
|
|
208
|
+
.then(async (resources) => {
|
|
209
|
+
await this.options.onInstructionsLoaded?.(resources.instructions, {
|
|
210
|
+
lifecycleId: snapshot.lifecycleId,
|
|
211
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
212
|
+
reason,
|
|
213
|
+
});
|
|
214
|
+
return resources;
|
|
215
|
+
});
|
|
202
216
|
snapshot.resources = pending;
|
|
203
217
|
void pending.catch(() => {
|
|
204
218
|
if (snapshot.resources === pending)
|
|
@@ -145,7 +145,8 @@ export async function loadClaudeDynamicContext(options) {
|
|
|
145
145
|
? [
|
|
146
146
|
'# Memory',
|
|
147
147
|
`Persistent project memory is stored at \`${options.memoryDirectory}\`.`,
|
|
148
|
-
'Use MEMORY.md as
|
|
148
|
+
'Use MEMORY.md as a concise index of one-line links. Keep detailed durable facts in linked Markdown topic files with name, description, and a type of user, feedback, project, or reference. Update existing entries instead of duplicating them.',
|
|
149
|
+
'Do not store codebase architecture, implementation patterns, git history, fix recipes, transient task or conversation state, or duplicates of repository instructions.',
|
|
149
150
|
].join('\n')
|
|
150
151
|
: undefined;
|
|
151
152
|
return {
|
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
import { lstat, readdir } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { extname, resolve } from 'node:path';
|
|
4
|
+
import { sanitizeProjectPath } from '../../platform/project-path-key.js';
|
|
4
5
|
import { getDataOwnership } from './ownership.js';
|
|
5
6
|
const MAX_SANITIZED_LENGTH = 200;
|
|
6
7
|
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
8
|
export function isClaudeSessionId(value) {
|
|
8
9
|
return SESSION_ID_PATTERN.test(value);
|
|
9
10
|
}
|
|
10
|
-
function stablePathHash(value) {
|
|
11
|
-
let hash = 0;
|
|
12
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
13
|
-
hash = Math.imul(hash, 31) + value.charCodeAt(index);
|
|
14
|
-
hash |= 0;
|
|
15
|
-
}
|
|
16
|
-
return Math.abs(hash);
|
|
17
|
-
}
|
|
18
11
|
export function sanitizeClaudeProjectPath(path) {
|
|
19
|
-
|
|
20
|
-
if (sanitized.length <= MAX_SANITIZED_LENGTH) {
|
|
21
|
-
return sanitized;
|
|
22
|
-
}
|
|
23
|
-
return `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}-${stablePathHash(path).toString(36)}`;
|
|
12
|
+
return sanitizeProjectPath(path);
|
|
24
13
|
}
|
|
25
14
|
async function isDirectory(path) {
|
|
26
15
|
try {
|
|
@@ -42,17 +42,17 @@ export interface LoadClaudeSharedResourcesOptions {
|
|
|
42
42
|
homeDirectory?: string;
|
|
43
43
|
claudeStatePath?: string;
|
|
44
44
|
settingSources?: readonly ClaudeResourceScope[];
|
|
45
|
+
includeProjectMemory?: boolean;
|
|
45
46
|
}
|
|
46
47
|
export declare function resolveClaudeInstructionImports(resources: readonly ClaudeTextResource[], { homeDirectory }?: {
|
|
47
48
|
homeDirectory?: string;
|
|
48
49
|
}): Promise<ClaudeTextResource[]>;
|
|
49
|
-
export declare function resolveClaudeProjectIdentity({ cwd,
|
|
50
|
+
export declare function resolveClaudeProjectIdentity({ cwd, }: {
|
|
50
51
|
cwd: string;
|
|
51
|
-
homeDirectory?: string;
|
|
52
52
|
}): Promise<string>;
|
|
53
53
|
export declare function loadClaudeSettings({ configRoot, cwd, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeJsonResource[]>;
|
|
54
54
|
export declare function resolveClaudeProjectMemoryDirectory({ configRoot, cwd, homeDirectory, }: LoadClaudeSharedResourcesOptions): Promise<string>;
|
|
55
55
|
export declare function loadClaudeMcpResources({ configRoot, cwd, homeDirectory, claudeStatePath, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeJsonResource[]>;
|
|
56
|
-
export declare function loadClaudeContextResources({ configRoot, cwd, homeDirectory, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeContextResources>;
|
|
57
|
-
export declare function loadClaudeSharedResources({ configRoot, cwd, homeDirectory, claudeStatePath, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeSharedResources>;
|
|
56
|
+
export declare function loadClaudeContextResources({ configRoot, cwd, homeDirectory, settingSources, includeProjectMemory, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeContextResources>;
|
|
57
|
+
export declare function loadClaudeSharedResources({ configRoot, cwd, homeDirectory, claudeStatePath, settingSources, includeProjectMemory, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeSharedResources>;
|
|
58
58
|
//# sourceMappingURL=shared-resources.d.ts.map
|
|
@@ -2,6 +2,7 @@ import { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { resolveProjectIdentity } from '../../platform/project-identity.js';
|
|
5
6
|
import { sanitizeClaudeProjectPath } from './paths.js';
|
|
6
7
|
function selectedScope(scope, settingSources) {
|
|
7
8
|
return settingSources === undefined || settingSources.includes(scope);
|
|
@@ -255,32 +256,6 @@ async function findGitRoot(cwd) {
|
|
|
255
256
|
}
|
|
256
257
|
return current;
|
|
257
258
|
}
|
|
258
|
-
async function readOptionalRaw(path) {
|
|
259
|
-
try {
|
|
260
|
-
return await readFile(path, 'utf8');
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
if (error.code === 'ENOENT')
|
|
264
|
-
return null;
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
async function findMemoryIdentityRoot(gitRoot) {
|
|
269
|
-
const gitMarker = join(gitRoot, '.git');
|
|
270
|
-
const marker = await stat(gitMarker);
|
|
271
|
-
if (!marker.isFile())
|
|
272
|
-
return gitRoot;
|
|
273
|
-
const gitFile = await readFile(gitMarker, 'utf8');
|
|
274
|
-
const match = /^gitdir:\s*(.+)\s*$/m.exec(gitFile);
|
|
275
|
-
if (!match?.[1])
|
|
276
|
-
return gitRoot;
|
|
277
|
-
const gitDirectory = resolve(gitRoot, match[1]);
|
|
278
|
-
const commonDirectory = await readOptionalRaw(join(gitDirectory, 'commondir'));
|
|
279
|
-
if (!commonDirectory)
|
|
280
|
-
return gitRoot;
|
|
281
|
-
const commonGitDirectory = await canonicalPath(resolve(gitDirectory, commonDirectory.trim()));
|
|
282
|
-
return dirname(commonGitDirectory);
|
|
283
|
-
}
|
|
284
259
|
async function resolveProjectContext(cwd, homeDirectory) {
|
|
285
260
|
const canonicalCwd = await canonicalPath(cwd);
|
|
286
261
|
const gitRoot = await findGitRoot(canonicalCwd);
|
|
@@ -289,7 +264,7 @@ async function resolveProjectContext(cwd, homeDirectory) {
|
|
|
289
264
|
directories: directoriesFromRoot(gitRoot, canonicalCwd),
|
|
290
265
|
extensionDirectories: directoriesFromRoot(gitRoot, canonicalCwd),
|
|
291
266
|
homeBoundary: null,
|
|
292
|
-
memoryIdentityRoot: await
|
|
267
|
+
memoryIdentityRoot: await resolveProjectIdentity(gitRoot),
|
|
293
268
|
};
|
|
294
269
|
}
|
|
295
270
|
const canonicalHome = await canonicalPath(homeDirectory);
|
|
@@ -303,8 +278,8 @@ async function resolveProjectContext(cwd, homeDirectory) {
|
|
|
303
278
|
memoryIdentityRoot: canonicalCwd,
|
|
304
279
|
};
|
|
305
280
|
}
|
|
306
|
-
export async function resolveClaudeProjectIdentity({ cwd,
|
|
307
|
-
return (
|
|
281
|
+
export async function resolveClaudeProjectIdentity({ cwd, }) {
|
|
282
|
+
return resolveProjectIdentity(cwd);
|
|
308
283
|
}
|
|
309
284
|
async function loadProjectSettings(configRoot, cwd) {
|
|
310
285
|
const userSettings = await readOptionalJson(join(configRoot, 'settings.json'), 'user');
|
|
@@ -411,13 +386,15 @@ async function loadProjectContextResources(projectDirectories, homeBoundary) {
|
|
|
411
386
|
function present(value) {
|
|
412
387
|
return value !== null;
|
|
413
388
|
}
|
|
414
|
-
async function loadResolvedClaudeContext(configRoot, projectDirectories, homeBoundary, memoryIdentityRoot) {
|
|
389
|
+
async function loadResolvedClaudeContext(configRoot, projectDirectories, homeBoundary, memoryIdentityRoot, includeProjectMemory) {
|
|
415
390
|
const projectMemoryDirectory = join(configRoot, 'projects', sanitizeClaudeProjectPath(memoryIdentityRoot), 'memory');
|
|
416
391
|
const [globalInstruction, globalRules, projectContext, memoryIndex] = await Promise.all([
|
|
417
392
|
readOptionalText(join(configRoot, 'CLAUDE.md'), 'user'),
|
|
418
393
|
loadRuleResources(join(configRoot, 'rules'), 'user', projectDirectories.at(-1) ?? memoryIdentityRoot),
|
|
419
394
|
loadProjectContextResources(projectDirectories, homeBoundary),
|
|
420
|
-
|
|
395
|
+
includeProjectMemory
|
|
396
|
+
? readOptionalText(join(projectMemoryDirectory, 'MEMORY.md'), 'project')
|
|
397
|
+
: Promise.resolve(null),
|
|
421
398
|
]);
|
|
422
399
|
return {
|
|
423
400
|
instructions: [
|
|
@@ -432,9 +409,9 @@ async function loadResolvedClaudeContext(configRoot, projectDirectories, homeBou
|
|
|
432
409
|
memoryIndex,
|
|
433
410
|
};
|
|
434
411
|
}
|
|
435
|
-
export async function loadClaudeContextResources({ configRoot, cwd, homeDirectory = homedir(), settingSources, }) {
|
|
412
|
+
export async function loadClaudeContextResources({ configRoot, cwd, homeDirectory = homedir(), settingSources, includeProjectMemory = true, }) {
|
|
436
413
|
const { directories, homeBoundary, memoryIdentityRoot } = await resolveProjectContext(cwd, homeDirectory);
|
|
437
|
-
const resources = await loadResolvedClaudeContext(configRoot, directories, homeBoundary, memoryIdentityRoot);
|
|
414
|
+
const resources = await loadResolvedClaudeContext(configRoot, directories, homeBoundary, memoryIdentityRoot, includeProjectMemory);
|
|
438
415
|
const instructions = resources.instructions.filter((resource) => selectedScope(resource.scope, settingSources));
|
|
439
416
|
return {
|
|
440
417
|
instructions: await resolveClaudeInstructionImports(instructions, {
|
|
@@ -447,14 +424,16 @@ export async function loadClaudeContextResources({ configRoot, cwd, homeDirector
|
|
|
447
424
|
: null,
|
|
448
425
|
};
|
|
449
426
|
}
|
|
450
|
-
export async function loadClaudeSharedResources({ configRoot, cwd, homeDirectory = homedir(), claudeStatePath = join(configRoot, '.claude.json'), settingSources, }) {
|
|
427
|
+
export async function loadClaudeSharedResources({ configRoot, cwd, homeDirectory = homedir(), claudeStatePath = join(configRoot, '.claude.json'), settingSources, includeProjectMemory = true, }) {
|
|
451
428
|
const { directories: projectDirectories, extensionDirectories, homeBoundary, memoryIdentityRoot, } = await resolveProjectContext(cwd, homeDirectory);
|
|
452
429
|
const projectMemoryDirectory = join(configRoot, 'projects', sanitizeClaudeProjectPath(memoryIdentityRoot), 'memory');
|
|
453
430
|
const [globalInstruction, globalRules, projectInstructions, memory, skills, commands, agents, settings, mcp,] = await Promise.all([
|
|
454
431
|
readOptionalText(join(configRoot, 'CLAUDE.md'), 'user'),
|
|
455
432
|
loadAllRules(join(configRoot, 'rules'), 'user'),
|
|
456
433
|
loadProjectInstructions(projectDirectories, homeBoundary),
|
|
457
|
-
|
|
434
|
+
includeProjectMemory
|
|
435
|
+
? findFiles(projectMemoryDirectory, (name) => name.endsWith('.md')).then((paths) => loadFiles(paths, 'project'))
|
|
436
|
+
: Promise.resolve([]),
|
|
458
437
|
loadScopedFiles(join(configRoot, 'skills'), extensionDirectories.map((path) => join(path, '.claude', 'skills')), (name) => name === 'SKILL.md'),
|
|
459
438
|
loadScopedFiles(join(configRoot, 'commands'), extensionDirectories.map((path) => join(path, '.claude', 'commands')), (name) => name.endsWith('.md')),
|
|
460
439
|
loadScopedFiles(join(configRoot, 'agents'), extensionDirectories.map((path) => join(path, '.claude', 'agents')), (name) => name.endsWith('.md')),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelMessage, ModelToolDefinition, ModelUsage } from './runtime.js';
|
|
2
|
+
import { ModelProviderError } from './runtime.js';
|
|
2
3
|
export interface ContextBudgetOptions {
|
|
3
4
|
contextWindowTokens: number;
|
|
4
5
|
reserveTokens?: number;
|
|
@@ -76,15 +77,16 @@ export type ContextRecoveryStage = 'preflight' | 'microcompact' | 'auto-compact'
|
|
|
76
77
|
export interface ContextRecoveryPlannerOptions {
|
|
77
78
|
/** Number of reactive retries allowed before the planner reports `blocked`. */
|
|
78
79
|
maxReactiveRetries?: number;
|
|
79
|
-
/** Consecutive failures that trip the circuit breaker to `blocked`. */
|
|
80
|
-
consecutiveFailureThreshold?: number;
|
|
81
80
|
}
|
|
81
|
+
export interface ContextRecoveryProgress {
|
|
82
|
+
beforeOccupancyTokens: number;
|
|
83
|
+
afterOccupancyTokens: number;
|
|
84
|
+
}
|
|
85
|
+
export declare function contextRecoveryMadeProgress(progress: ContextRecoveryProgress): boolean;
|
|
82
86
|
export declare class ContextRecoveryPlanner {
|
|
83
87
|
private readonly maxReactiveRetries;
|
|
84
|
-
private readonly consecutiveFailureThreshold;
|
|
85
88
|
private currentStage;
|
|
86
89
|
private reactiveRetriesUsed;
|
|
87
|
-
private consecutiveFailures;
|
|
88
90
|
constructor(options?: ContextRecoveryPlannerOptions);
|
|
89
91
|
get stage(): ContextRecoveryStage;
|
|
90
92
|
get reactiveRetriesRemaining(): number;
|
|
@@ -93,14 +95,13 @@ export declare class ContextRecoveryPlanner {
|
|
|
93
95
|
advance(): ContextRecoveryStage;
|
|
94
96
|
/** Consume one reactive retry decision; reports `blocked` once the maximum
|
|
95
97
|
* reactive retries have been consumed. */
|
|
96
|
-
consumeReactiveRetry(): ContextRecoveryStage;
|
|
97
|
-
/**
|
|
98
|
-
* after `consecutiveFailureThreshold` consecutive failures. */
|
|
98
|
+
consumeReactiveRetry(progress: ContextRecoveryProgress): ContextRecoveryStage;
|
|
99
|
+
/** A failed reactive attempt exhausts the only selected context transition. */
|
|
99
100
|
recordFailure(): ContextRecoveryStage;
|
|
100
101
|
/** Record a successful recovery/model attempt; resets the planner. */
|
|
101
102
|
recordSuccess(): ContextRecoveryStage;
|
|
102
103
|
}
|
|
103
104
|
/** True when a provider error signals that the request exceeded the model's
|
|
104
105
|
* context window and a reactive compaction retry is the relevant recovery. */
|
|
105
|
-
export declare function isPromptTooLongError(error: unknown):
|
|
106
|
+
export declare function isPromptTooLongError(error: unknown): error is ModelProviderError;
|
|
106
107
|
//# sourceMappingURL=context-budget.d.ts.map
|
|
@@ -203,26 +203,24 @@ export class ContextBudget {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
const DEFAULT_MAX_REACTIVE_RETRIES = 1;
|
|
206
|
-
|
|
206
|
+
export function contextRecoveryMadeProgress(progress) {
|
|
207
|
+
for (const [name, value] of Object.entries(progress)) {
|
|
208
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
209
|
+
throw new Error(`${name} must be a nonnegative safe integer`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return progress.afterOccupancyTokens < progress.beforeOccupancyTokens;
|
|
213
|
+
}
|
|
207
214
|
export class ContextRecoveryPlanner {
|
|
208
215
|
maxReactiveRetries;
|
|
209
|
-
consecutiveFailureThreshold;
|
|
210
216
|
currentStage = 'preflight';
|
|
211
217
|
reactiveRetriesUsed = 0;
|
|
212
|
-
consecutiveFailures = 0;
|
|
213
218
|
constructor(options = {}) {
|
|
214
219
|
const maxReactiveRetries = options.maxReactiveRetries ?? DEFAULT_MAX_REACTIVE_RETRIES;
|
|
215
|
-
const consecutiveFailureThreshold = options.consecutiveFailureThreshold ??
|
|
216
|
-
DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD;
|
|
217
220
|
if (!Number.isInteger(maxReactiveRetries) || maxReactiveRetries < 0) {
|
|
218
221
|
throw new Error('maxReactiveRetries must be a nonnegative integer');
|
|
219
222
|
}
|
|
220
|
-
if (!Number.isInteger(consecutiveFailureThreshold) ||
|
|
221
|
-
consecutiveFailureThreshold < 1) {
|
|
222
|
-
throw new Error('consecutiveFailureThreshold must be a positive integer');
|
|
223
|
-
}
|
|
224
223
|
this.maxReactiveRetries = maxReactiveRetries;
|
|
225
|
-
this.consecutiveFailureThreshold = consecutiveFailureThreshold;
|
|
226
224
|
}
|
|
227
225
|
get stage() {
|
|
228
226
|
return this.currentStage;
|
|
@@ -253,7 +251,11 @@ export class ContextRecoveryPlanner {
|
|
|
253
251
|
}
|
|
254
252
|
/** Consume one reactive retry decision; reports `blocked` once the maximum
|
|
255
253
|
* reactive retries have been consumed. */
|
|
256
|
-
consumeReactiveRetry() {
|
|
254
|
+
consumeReactiveRetry(progress) {
|
|
255
|
+
if (!contextRecoveryMadeProgress(progress)) {
|
|
256
|
+
this.currentStage = 'blocked';
|
|
257
|
+
return this.currentStage;
|
|
258
|
+
}
|
|
257
259
|
this.reactiveRetriesUsed += 1;
|
|
258
260
|
this.currentStage =
|
|
259
261
|
this.reactiveRetriesUsed <= this.maxReactiveRetries
|
|
@@ -261,28 +263,21 @@ export class ContextRecoveryPlanner {
|
|
|
261
263
|
: 'blocked';
|
|
262
264
|
return this.currentStage;
|
|
263
265
|
}
|
|
264
|
-
/**
|
|
265
|
-
* after `consecutiveFailureThreshold` consecutive failures. */
|
|
266
|
+
/** A failed reactive attempt exhausts the only selected context transition. */
|
|
266
267
|
recordFailure() {
|
|
267
|
-
this.
|
|
268
|
-
if (this.consecutiveFailures >= this.consecutiveFailureThreshold) {
|
|
269
|
-
this.currentStage = 'blocked';
|
|
270
|
-
}
|
|
268
|
+
this.currentStage = 'blocked';
|
|
271
269
|
return this.currentStage;
|
|
272
270
|
}
|
|
273
271
|
/** Record a successful recovery/model attempt; resets the planner. */
|
|
274
272
|
recordSuccess() {
|
|
275
|
-
this.consecutiveFailures = 0;
|
|
276
273
|
this.reactiveRetriesUsed = 0;
|
|
277
274
|
this.currentStage = 'preflight';
|
|
278
275
|
return this.currentStage;
|
|
279
276
|
}
|
|
280
277
|
}
|
|
281
|
-
const PROMPT_TOO_LONG_PATTERN = /context(?:[_\s-]?length|[_\s-]?window)|prompt(?:[_\s-]?is)?[_\s-]?too[_\s-]?long|too[_\s-]?many[_\s-]?tokens|maximum[_\s-]?context/iu;
|
|
282
278
|
/** True when a provider error signals that the request exceeded the model's
|
|
283
279
|
* context window and a reactive compaction retry is the relevant recovery. */
|
|
284
280
|
export function isPromptTooLongError(error) {
|
|
285
|
-
return
|
|
286
|
-
PROMPT_TOO_LONG_PATTERN.test(error.message));
|
|
281
|
+
return error instanceof ModelProviderError && error.kind === 'prompt_too_long';
|
|
287
282
|
}
|
|
288
283
|
//# sourceMappingURL=context-budget.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const PROJECT_MEMORY_INDEX_MAX_LINES = 200;
|
|
2
|
+
export declare const PROJECT_MEMORY_INDEX_MAX_BYTES: number;
|
|
3
|
+
export type ProjectMemoryDataPlane = 'native' | 'claude';
|
|
4
|
+
export interface ProjectMemoryPolicy {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
extraction: boolean;
|
|
7
|
+
recall: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ResolveProjectMemoryPolicyOptions {
|
|
10
|
+
dataPlane: ProjectMemoryDataPlane;
|
|
11
|
+
settings: readonly {
|
|
12
|
+
value: unknown;
|
|
13
|
+
}[];
|
|
14
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
15
|
+
}
|
|
16
|
+
export declare function boundProjectMemoryText(source: string, maxLines: number, maxBytes: number): string;
|
|
17
|
+
export declare function boundProjectMemoryIndex(source: string): string;
|
|
18
|
+
export declare function resolveProjectMemoryPolicy({ dataPlane, settings, environment, }: ResolveProjectMemoryPolicyOptions): ProjectMemoryPolicy;
|
|
19
|
+
//# sourceMappingURL=project-memory.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const PROJECT_MEMORY_INDEX_MAX_LINES = 200;
|
|
2
|
+
export const PROJECT_MEMORY_INDEX_MAX_BYTES = 25 * 1024;
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: null;
|
|
7
|
+
}
|
|
8
|
+
function utf8Prefix(source, maxBytes) {
|
|
9
|
+
const buffer = Buffer.from(source, 'utf8');
|
|
10
|
+
if (buffer.byteLength <= maxBytes)
|
|
11
|
+
return source;
|
|
12
|
+
for (let end = maxBytes; end >= Math.max(0, maxBytes - 4); end -= 1) {
|
|
13
|
+
try {
|
|
14
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, end));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Try the previous Unicode boundary.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
export function boundProjectMemoryText(source, maxLines, maxBytes) {
|
|
23
|
+
return utf8Prefix(source.split(/\r?\n/u).slice(0, maxLines).join('\n'), maxBytes);
|
|
24
|
+
}
|
|
25
|
+
export function boundProjectMemoryIndex(source) {
|
|
26
|
+
return boundProjectMemoryText(source, PROJECT_MEMORY_INDEX_MAX_LINES, PROJECT_MEMORY_INDEX_MAX_BYTES);
|
|
27
|
+
}
|
|
28
|
+
function settingBoolean(settings, path) {
|
|
29
|
+
let selected;
|
|
30
|
+
for (const resource of settings) {
|
|
31
|
+
let value = resource.value;
|
|
32
|
+
for (const key of path)
|
|
33
|
+
value = record(value)?.[key];
|
|
34
|
+
if (typeof value === 'boolean')
|
|
35
|
+
selected = value;
|
|
36
|
+
}
|
|
37
|
+
return selected;
|
|
38
|
+
}
|
|
39
|
+
function enabledEnvironmentValue(value) {
|
|
40
|
+
return value === '1' || value?.toLowerCase() === 'true';
|
|
41
|
+
}
|
|
42
|
+
export function resolveProjectMemoryPolicy({ dataPlane, settings, environment, }) {
|
|
43
|
+
const disabledByEnvironment = enabledEnvironmentValue(dataPlane === 'claude'
|
|
44
|
+
? environment.CLAUDE_CODE_DISABLE_AUTO_MEMORY
|
|
45
|
+
: environment.PRAXIS_DISABLE_AUTO_MEMORY);
|
|
46
|
+
const enabled = !disabledByEnvironment &&
|
|
47
|
+
(settingBoolean(settings, ['autoMemoryEnabled']) ?? true);
|
|
48
|
+
if (!enabled)
|
|
49
|
+
return { enabled: false, extraction: false, recall: false };
|
|
50
|
+
return {
|
|
51
|
+
enabled: true,
|
|
52
|
+
extraction: settingBoolean(settings, ['projectMemory', 'backgroundExtraction']) ??
|
|
53
|
+
enabledEnvironmentValue(environment.PRAXIS_PROJECT_MEMORY_EXTRACTION),
|
|
54
|
+
recall: settingBoolean(settings, ['projectMemory', 'selectiveRecall']) ??
|
|
55
|
+
enabledEnvironmentValue(environment.PRAXIS_PROJECT_MEMORY_RECALL),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=project-memory.js.map
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -181,6 +181,9 @@ export type RuntimeEvent = {
|
|
|
181
181
|
} | {
|
|
182
182
|
type: 'terminal';
|
|
183
183
|
reason: ModelTerminalReason;
|
|
184
|
+
} | {
|
|
185
|
+
type: 'model-attempt-discarded';
|
|
186
|
+
reason: ProviderErrorKind;
|
|
184
187
|
} | {
|
|
185
188
|
type: 'tool-call';
|
|
186
189
|
call: ModelToolCall;
|
|
@@ -316,6 +319,7 @@ export interface ToolExecutionResult {
|
|
|
316
319
|
}
|
|
317
320
|
export interface ToolExecutionContext {
|
|
318
321
|
cwd: string;
|
|
322
|
+
sessionId?: string;
|
|
319
323
|
messages?: readonly ModelMessage[];
|
|
320
324
|
signal?: AbortSignal;
|
|
321
325
|
toolResultDirectory?: string;
|
|
@@ -404,6 +408,7 @@ export type PermissionDecision = {
|
|
|
404
408
|
} | {
|
|
405
409
|
behavior: 'deny';
|
|
406
410
|
reason: string;
|
|
411
|
+
followUpUserMessages?: readonly string[];
|
|
407
412
|
source?: PermissionDecisionSource;
|
|
408
413
|
};
|
|
409
414
|
export interface PermissionResolver {
|
|
@@ -452,6 +457,7 @@ export interface AgentRuntimeOptions {
|
|
|
452
457
|
}
|
|
453
458
|
export interface AgentRunRequest {
|
|
454
459
|
messages: readonly ModelMessage[];
|
|
460
|
+
sessionId?: string;
|
|
455
461
|
/** Number of leading system messages that form the stable prompt prefix. */
|
|
456
462
|
stableSystemMessageCount?: number;
|
|
457
463
|
cwd?: string;
|
|
@@ -472,10 +478,13 @@ export interface AgentRunRequest {
|
|
|
472
478
|
effort?: string;
|
|
473
479
|
thinking?: ModelThinkingConfig;
|
|
474
480
|
collectMetrics?: boolean;
|
|
481
|
+
/** Provider failures whose presentation is owned by a caller recovery
|
|
482
|
+
* boundary. `true` defers every typed provider failure for an owned retry. */
|
|
483
|
+
deferFailureKinds?: true | readonly ProviderErrorKind[];
|
|
475
484
|
maxModelTurns?: number;
|
|
476
485
|
betas?: readonly string[];
|
|
477
486
|
}
|
|
478
|
-
export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'toolResultDirectory'> {
|
|
487
|
+
export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'sessionId' | 'toolResultDirectory'> {
|
|
479
488
|
approveRecovery?: (call: ModelToolCall) => boolean | Promise<boolean>;
|
|
480
489
|
messages?: readonly ModelMessage[];
|
|
481
490
|
}
|
package/dist/core/runtime.js
CHANGED
|
@@ -255,6 +255,8 @@ export class AgentRuntime {
|
|
|
255
255
|
this.options = options;
|
|
256
256
|
}
|
|
257
257
|
async run(request) {
|
|
258
|
+
const failurePresentationDeferred = (kind) => request.deferFailureKinds === true ||
|
|
259
|
+
request.deferFailureKinds?.includes(kind) === true;
|
|
258
260
|
if (this.options.emitInitialContextState !== false) {
|
|
259
261
|
this.emit({ type: 'state', state: 'assembling-context' });
|
|
260
262
|
}
|
|
@@ -273,6 +275,8 @@ export class AgentRuntime {
|
|
|
273
275
|
let durationToolMs = 0;
|
|
274
276
|
let linesAdded = 0;
|
|
275
277
|
let linesRemoved = 0;
|
|
278
|
+
let activeAttemptHasPresentation = false;
|
|
279
|
+
let activeAttemptDiscarded = false;
|
|
276
280
|
const definitions = this.provider.capabilities.tools
|
|
277
281
|
? (this.options.tools?.definitions() ?? [])
|
|
278
282
|
: [];
|
|
@@ -288,6 +292,8 @@ export class AgentRuntime {
|
|
|
288
292
|
try {
|
|
289
293
|
let modelTurns = 0;
|
|
290
294
|
while (true) {
|
|
295
|
+
activeAttemptHasPresentation = false;
|
|
296
|
+
activeAttemptDiscarded = false;
|
|
291
297
|
if (request.signal?.aborted)
|
|
292
298
|
return this.cancel();
|
|
293
299
|
if (maxModelTurns !== undefined && modelTurns >= maxModelTurns) {
|
|
@@ -379,9 +385,10 @@ export class AgentRuntime {
|
|
|
379
385
|
}),
|
|
380
386
|
resultCompleted: (call, result) => this.presentToolResult(call, result),
|
|
381
387
|
});
|
|
382
|
-
const failScheduledTurn = async (error) => {
|
|
388
|
+
const failScheduledTurn = async (error, afterSettled) => {
|
|
383
389
|
toolScheduler.abort(error);
|
|
384
390
|
await toolScheduler.settle().catch(() => undefined);
|
|
391
|
+
afterSettled?.();
|
|
385
392
|
throw error;
|
|
386
393
|
};
|
|
387
394
|
const apiStartedAt = request.collectMetrics ? performance.now() : 0;
|
|
@@ -418,6 +425,7 @@ export class AgentRuntime {
|
|
|
418
425
|
this.emit({ type: 'state', state: 'streaming' });
|
|
419
426
|
}
|
|
420
427
|
if (event.type === 'text-delta') {
|
|
428
|
+
activeAttemptHasPresentation = true;
|
|
421
429
|
textBytes += Buffer.byteLength(event.delta);
|
|
422
430
|
if (textBytes > maxModelOutputBytes) {
|
|
423
431
|
throw new Error(`Model output exceeded ${maxModelOutputBytes} bytes`);
|
|
@@ -427,6 +435,7 @@ export class AgentRuntime {
|
|
|
427
435
|
}
|
|
428
436
|
else if (event.type === 'thinking-delta' ||
|
|
429
437
|
event.type === 'thinking-signature-delta') {
|
|
438
|
+
activeAttemptHasPresentation = true;
|
|
430
439
|
textBytes += Buffer.byteLength(event.delta);
|
|
431
440
|
if (textBytes > maxModelOutputBytes) {
|
|
432
441
|
throw new Error(`Model output exceeded ${maxModelOutputBytes} bytes`);
|
|
@@ -434,10 +443,12 @@ export class AgentRuntime {
|
|
|
434
443
|
this.emit(event);
|
|
435
444
|
}
|
|
436
445
|
else if (event.type === 'thinking-stop') {
|
|
446
|
+
activeAttemptHasPresentation = true;
|
|
437
447
|
thinkingBlocks.push(event.block);
|
|
438
448
|
this.emit(event);
|
|
439
449
|
}
|
|
440
450
|
else if (event.type === 'thinking-start') {
|
|
451
|
+
activeAttemptHasPresentation = true;
|
|
441
452
|
const initialThinking = event.block.type === 'redacted_thinking'
|
|
442
453
|
? event.block.data
|
|
443
454
|
: event.block.thinking;
|
|
@@ -462,10 +473,12 @@ export class AgentRuntime {
|
|
|
462
473
|
toolScheduler.schedule(event.call, resolveToolSchedulingPolicy(this.options.tools, event.call));
|
|
463
474
|
}
|
|
464
475
|
else if (event.type === 'terminal') {
|
|
476
|
+
activeAttemptHasPresentation = true;
|
|
465
477
|
terminalReason = event.reason;
|
|
466
478
|
this.emit(event);
|
|
467
479
|
}
|
|
468
480
|
else {
|
|
481
|
+
activeAttemptHasPresentation = true;
|
|
469
482
|
turnUsage = event.usage;
|
|
470
483
|
this.emit(event);
|
|
471
484
|
}
|
|
@@ -495,6 +508,18 @@ export class AgentRuntime {
|
|
|
495
508
|
terminalReason === undefined) {
|
|
496
509
|
return failScheduledTurn(new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true }));
|
|
497
510
|
}
|
|
511
|
+
if (terminalReason === 'prompt_too_long') {
|
|
512
|
+
const discardAttempt = failurePresentationDeferred('prompt_too_long')
|
|
513
|
+
? () => {
|
|
514
|
+
this.emit({
|
|
515
|
+
type: 'model-attempt-discarded',
|
|
516
|
+
reason: 'prompt_too_long',
|
|
517
|
+
});
|
|
518
|
+
activeAttemptDiscarded = true;
|
|
519
|
+
}
|
|
520
|
+
: undefined;
|
|
521
|
+
await failScheduledTurn(new ModelProviderError('Provider reported that the active prompt exceeds its context window', { kind: 'prompt_too_long', retryable: false }), discardAttempt);
|
|
522
|
+
}
|
|
498
523
|
if (terminalReason === 'tool_use' && toolCalls.length === 0) {
|
|
499
524
|
return failScheduledTurn(new ModelProviderError('Provider reported tool_use without a completed tool call', { retryable: false }));
|
|
500
525
|
}
|
|
@@ -677,7 +702,18 @@ export class AgentRuntime {
|
|
|
677
702
|
return this.cancel();
|
|
678
703
|
const message = error instanceof Error ? error.message : String(error);
|
|
679
704
|
const retryable = error instanceof ModelProviderError ? error.retryable : false;
|
|
680
|
-
|
|
705
|
+
const kind = error instanceof ModelProviderError
|
|
706
|
+
? modelProviderErrorKind(error)
|
|
707
|
+
: undefined;
|
|
708
|
+
const failureDeferred = kind !== undefined && failurePresentationDeferred(kind);
|
|
709
|
+
if (failureDeferred &&
|
|
710
|
+
activeAttemptHasPresentation &&
|
|
711
|
+
!activeAttemptDiscarded) {
|
|
712
|
+
this.emit({ type: 'model-attempt-discarded', reason: kind });
|
|
713
|
+
}
|
|
714
|
+
if (!failureDeferred) {
|
|
715
|
+
this.emit({ type: 'failed', message, retryable });
|
|
716
|
+
}
|
|
681
717
|
throw error;
|
|
682
718
|
}
|
|
683
719
|
}
|
|
@@ -809,6 +845,8 @@ export class AgentRuntime {
|
|
|
809
845
|
permissionPhase: 'request',
|
|
810
846
|
permissionUpdates: request.permissionUpdates ?? [],
|
|
811
847
|
};
|
|
848
|
+
if (request.sessionId)
|
|
849
|
+
context.sessionId = request.sessionId;
|
|
812
850
|
if (request.signal)
|
|
813
851
|
context.signal = request.signal;
|
|
814
852
|
if (request.toolResultDirectory) {
|
|
@@ -890,7 +928,13 @@ export class AgentRuntime {
|
|
|
890
928
|
: decision.behavior === 'ask'
|
|
891
929
|
? (decision.reason ?? 'Permission approval was not provided')
|
|
892
930
|
: 'Permission approval was not provided');
|
|
893
|
-
return {
|
|
931
|
+
return {
|
|
932
|
+
content: reason,
|
|
933
|
+
isError: true,
|
|
934
|
+
...(decision.behavior === 'deny' && decision.followUpUserMessages
|
|
935
|
+
? { followUpUserMessages: decision.followUpUserMessages }
|
|
936
|
+
: {}),
|
|
937
|
+
};
|
|
894
938
|
}
|
|
895
939
|
if (request.signal?.aborted) {
|
|
896
940
|
throw request.signal.reason ?? new AgentRunCancelledError();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type ClaudeFileChangeEvent = 'add' | 'change' | 'unlink';
|
|
2
|
+
interface DirectoryWatcher {
|
|
3
|
+
close(): void;
|
|
4
|
+
on(event: 'error', listener: (error: Error) => void): DirectoryWatcher;
|
|
5
|
+
unref?(): void;
|
|
6
|
+
}
|
|
7
|
+
type WatchDirectory = (directory: string, listener: (filename: string | null) => void) => DirectoryWatcher;
|
|
8
|
+
export interface ClaudeFileChangeWatcherOptions {
|
|
9
|
+
cwd: string;
|
|
10
|
+
staticPaths: readonly string[];
|
|
11
|
+
onFileChanged(filePath: string, event: ClaudeFileChangeEvent, signal: AbortSignal): Promise<readonly string[] | undefined>;
|
|
12
|
+
warn(message: string): void;
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
pathExists?(path: string): boolean;
|
|
15
|
+
watchDirectory?: WatchDirectory;
|
|
16
|
+
}
|
|
17
|
+
export declare class ClaudeFileChangeWatcher {
|
|
18
|
+
private readonly options;
|
|
19
|
+
private cwd;
|
|
20
|
+
private staticPaths;
|
|
21
|
+
private dynamicPaths;
|
|
22
|
+
private readonly watchers;
|
|
23
|
+
private readonly knownExistence;
|
|
24
|
+
private readonly pending;
|
|
25
|
+
private readonly inFlight;
|
|
26
|
+
private readonly debounceMs;
|
|
27
|
+
private readonly pathExists;
|
|
28
|
+
private readonly watchDirectory;
|
|
29
|
+
private generation;
|
|
30
|
+
private closed;
|
|
31
|
+
constructor(options: ClaudeFileChangeWatcherOptions);
|
|
32
|
+
updateForCwd(cwd: string, staticPaths: readonly string[], dynamicPaths: readonly string[]): void;
|
|
33
|
+
updateDynamicPaths(paths: readonly string[]): void;
|
|
34
|
+
close(timeoutMs?: number): Promise<void>;
|
|
35
|
+
private validDynamicPaths;
|
|
36
|
+
private targetPaths;
|
|
37
|
+
private rebuild;
|
|
38
|
+
private schedule;
|
|
39
|
+
private dispatch;
|
|
40
|
+
private clearResources;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=claude-file-change-watcher.d.ts.map
|