@compilr-dev/sdk 0.10.30 → 0.10.32
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/delegation.d.ts +21 -0
- package/dist/delegation.js +49 -0
- package/dist/episodes/index.d.ts +19 -0
- package/dist/episodes/index.js +20 -0
- package/dist/episodes/recorder.d.ts +52 -0
- package/dist/episodes/recorder.js +195 -0
- package/dist/episodes/significant-work.d.ts +26 -0
- package/dist/episodes/significant-work.js +59 -0
- package/dist/episodes/store.d.ts +38 -0
- package/dist/episodes/store.js +203 -0
- package/dist/episodes/types.d.ts +36 -0
- package/dist/episodes/types.js +7 -0
- package/dist/episodes/work-at-risk.d.ts +13 -0
- package/dist/episodes/work-at-risk.js +39 -0
- package/dist/episodes/work-summary-anchor.d.ts +23 -0
- package/dist/episodes/work-summary-anchor.js +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared delegation defaults for tool result auto-summarization.
|
|
3
|
+
*
|
|
4
|
+
* `DelegationConfig` controls when large tool results get summarized
|
|
5
|
+
* by a delegate agent (instead of being inlined into the conversation
|
|
6
|
+
* context) — a per-turn token economy win that the agents library wires
|
|
7
|
+
* via the AfterToolHook.
|
|
8
|
+
*
|
|
9
|
+
* Each host (CLI, Desktop) used to keep its own copy of this config.
|
|
10
|
+
* The two had drifted: identical baselines (8000 default threshold etc.)
|
|
11
|
+
* but diverged tool overrides — Desktop had 6 extra tools tuned
|
|
12
|
+
* (run_tests, run_lint, run_build, web_fetch, artifact_get,
|
|
13
|
+
* find_references, propose_alternatives) plus a tighter `git_diff`
|
|
14
|
+
* threshold (4000 vs CLI's 6000). The baseline below converges to
|
|
15
|
+
* Desktop's set since it's the more recently tuned version, and hosts
|
|
16
|
+
* spread + override per-tool as needed.
|
|
17
|
+
*
|
|
18
|
+
* Audit ref: TODO-13.
|
|
19
|
+
*/
|
|
20
|
+
import type { DelegationConfig } from '@compilr-dev/agents';
|
|
21
|
+
export declare const DEFAULT_DELEGATION_CONFIG: DelegationConfig;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared delegation defaults for tool result auto-summarization.
|
|
3
|
+
*
|
|
4
|
+
* `DelegationConfig` controls when large tool results get summarized
|
|
5
|
+
* by a delegate agent (instead of being inlined into the conversation
|
|
6
|
+
* context) — a per-turn token economy win that the agents library wires
|
|
7
|
+
* via the AfterToolHook.
|
|
8
|
+
*
|
|
9
|
+
* Each host (CLI, Desktop) used to keep its own copy of this config.
|
|
10
|
+
* The two had drifted: identical baselines (8000 default threshold etc.)
|
|
11
|
+
* but diverged tool overrides — Desktop had 6 extra tools tuned
|
|
12
|
+
* (run_tests, run_lint, run_build, web_fetch, artifact_get,
|
|
13
|
+
* find_references, propose_alternatives) plus a tighter `git_diff`
|
|
14
|
+
* threshold (4000 vs CLI's 6000). The baseline below converges to
|
|
15
|
+
* Desktop's set since it's the more recently tuned version, and hosts
|
|
16
|
+
* spread + override per-tool as needed.
|
|
17
|
+
*
|
|
18
|
+
* Audit ref: TODO-13.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_DELEGATION_CONFIG = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
delegationThreshold: 8000,
|
|
23
|
+
summaryMaxTokens: 800,
|
|
24
|
+
resultTTL: 600_000,
|
|
25
|
+
maxStoredResults: 50,
|
|
26
|
+
strategy: 'auto',
|
|
27
|
+
toolOverrides: {
|
|
28
|
+
// Bash often produces large output; allow a higher inline budget
|
|
29
|
+
// before summarising.
|
|
30
|
+
bash: { threshold: 12000 },
|
|
31
|
+
grep: { threshold: 4000 },
|
|
32
|
+
// Large-output tools — tighter thresholds for earlier summarisation
|
|
33
|
+
git_diff: { threshold: 4000 },
|
|
34
|
+
run_tests: { threshold: 6000 },
|
|
35
|
+
run_lint: { threshold: 4000 },
|
|
36
|
+
run_build: { threshold: 6000 },
|
|
37
|
+
web_fetch: { threshold: 6000 },
|
|
38
|
+
artifact_get: { threshold: 6000 },
|
|
39
|
+
find_references: { threshold: 4000 },
|
|
40
|
+
// Disable delegation on small / interactive / read-only tools where
|
|
41
|
+
// the round-trip cost would exceed the saving.
|
|
42
|
+
get_tool_info: { enabled: false },
|
|
43
|
+
list_tools: { enabled: false },
|
|
44
|
+
ask_user: { enabled: false },
|
|
45
|
+
ask_user_simple: { enabled: false },
|
|
46
|
+
propose_alternatives: { enabled: false },
|
|
47
|
+
todo_read: { enabled: false },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episodes Module
|
|
3
|
+
*
|
|
4
|
+
* Work history tracking — file-based persistence + automatic
|
|
5
|
+
* recording via an AfterToolHook. Both CLI and Desktop consume these
|
|
6
|
+
* exports; the global episode store helpers (setGlobalEpisodeStore /
|
|
7
|
+
* getGlobalEpisodeStore) intentionally remain CLI-side because the
|
|
8
|
+
* "global singleton" pattern is CLI-specific. Desktop wires the store
|
|
9
|
+
* via direct dependency injection through the IEpisodeService.
|
|
10
|
+
*/
|
|
11
|
+
export { FileEpisodeStore } from './store.js';
|
|
12
|
+
export type { FileEpisodeStoreOptions } from './store.js';
|
|
13
|
+
export { EpisodeRecorder } from './recorder.js';
|
|
14
|
+
export type { EpisodeRecorderConfig } from './recorder.js';
|
|
15
|
+
export { isSignificantWork, extractAffectedFiles, extractLinesChanged, } from './significant-work.js';
|
|
16
|
+
export { queryWorkAtRisk } from './work-at-risk.js';
|
|
17
|
+
export { buildWorkSummaryContent, updateWorkSummaryAnchor } from './work-summary-anchor.js';
|
|
18
|
+
export type { WorkSummaryAnchorConfig } from './work-summary-anchor.js';
|
|
19
|
+
export type { PendingToolSignal, EpisodeFile } from './types.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episodes Module
|
|
3
|
+
*
|
|
4
|
+
* Work history tracking — file-based persistence + automatic
|
|
5
|
+
* recording via an AfterToolHook. Both CLI and Desktop consume these
|
|
6
|
+
* exports; the global episode store helpers (setGlobalEpisodeStore /
|
|
7
|
+
* getGlobalEpisodeStore) intentionally remain CLI-side because the
|
|
8
|
+
* "global singleton" pattern is CLI-specific. Desktop wires the store
|
|
9
|
+
* via direct dependency injection through the IEpisodeService.
|
|
10
|
+
*/
|
|
11
|
+
// Store
|
|
12
|
+
export { FileEpisodeStore } from './store.js';
|
|
13
|
+
// Recorder
|
|
14
|
+
export { EpisodeRecorder } from './recorder.js';
|
|
15
|
+
// Significant work detection
|
|
16
|
+
export { isSignificantWork, extractAffectedFiles, extractLinesChanged, } from './significant-work.js';
|
|
17
|
+
// Work-at-risk query (used by guardrails / rehearsal)
|
|
18
|
+
export { queryWorkAtRisk } from './work-at-risk.js';
|
|
19
|
+
// Work summary anchor (auto-updates a session anchor with cumulative work)
|
|
20
|
+
export { buildWorkSummaryContent, updateWorkSummaryAnchor } from './work-summary-anchor.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episode Recorder
|
|
3
|
+
*
|
|
4
|
+
* Collects tool call signals via an AfterToolHook, batches them within
|
|
5
|
+
* a time window, and persists as WorkEpisode records.
|
|
6
|
+
*/
|
|
7
|
+
import type { AfterToolHook, Effort, EpisodeStore } from '@compilr-dev/agents';
|
|
8
|
+
export interface EpisodeRecorderConfig {
|
|
9
|
+
/** Episode store for persistence */
|
|
10
|
+
store: EpisodeStore;
|
|
11
|
+
/** Agent ID (e.g., 'default', 'backend') */
|
|
12
|
+
agentId: string;
|
|
13
|
+
/** Terminal/window session prefix (first 8 chars). Hosts that don't
|
|
14
|
+
* have a terminal can pass any short stable identifier (e.g. window id). */
|
|
15
|
+
terminalPrefix: string;
|
|
16
|
+
/** Session ID for grouping */
|
|
17
|
+
sessionId: string;
|
|
18
|
+
/** Enable automatic recording via hook (default: true) */
|
|
19
|
+
autoRecord?: boolean;
|
|
20
|
+
/** Minimum effort level to record (default: 'low') */
|
|
21
|
+
minEffortToRecord?: Effort;
|
|
22
|
+
/** Batch window in milliseconds (default: 5000) */
|
|
23
|
+
batchWindowMs?: number;
|
|
24
|
+
/** Called after each batch is flushed as a WorkEpisode */
|
|
25
|
+
onBatchFlushed?: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare class EpisodeRecorder {
|
|
28
|
+
private readonly store;
|
|
29
|
+
private readonly agentId;
|
|
30
|
+
private readonly terminalPrefix;
|
|
31
|
+
private readonly sessionId;
|
|
32
|
+
private readonly autoRecord;
|
|
33
|
+
private readonly minEffortToRecord;
|
|
34
|
+
private readonly batchWindowMs;
|
|
35
|
+
private readonly onBatchFlushed?;
|
|
36
|
+
private pendingSignals;
|
|
37
|
+
private batchTimer;
|
|
38
|
+
private batchStartTime;
|
|
39
|
+
constructor(config: EpisodeRecorderConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Create an AfterToolHook for observing tool calls.
|
|
42
|
+
* Always returns undefined (observation only — never modifies results).
|
|
43
|
+
*/
|
|
44
|
+
createHook(): AfterToolHook;
|
|
45
|
+
/**
|
|
46
|
+
* Force-flush all pending signals as a WorkEpisode.
|
|
47
|
+
* Called on shutdown to avoid losing data.
|
|
48
|
+
*/
|
|
49
|
+
flush(): void;
|
|
50
|
+
private scheduleBatch;
|
|
51
|
+
private buildAndSaveEpisode;
|
|
52
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episode Recorder
|
|
3
|
+
*
|
|
4
|
+
* Collects tool call signals via an AfterToolHook, batches them within
|
|
5
|
+
* a time window, and persists as WorkEpisode records.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { estimateEffort, EFFORT_ORDER } from '@compilr-dev/agents';
|
|
9
|
+
import { isSignificantWork, extractAffectedFiles, extractLinesChanged, } from './significant-work.js';
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// =============================================================================
|
|
13
|
+
const DEFAULT_BATCH_WINDOW_MS = 5000;
|
|
14
|
+
const DEFAULT_MIN_EFFORT = 'low';
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Implementation
|
|
17
|
+
// =============================================================================
|
|
18
|
+
export class EpisodeRecorder {
|
|
19
|
+
store;
|
|
20
|
+
agentId;
|
|
21
|
+
terminalPrefix;
|
|
22
|
+
sessionId;
|
|
23
|
+
autoRecord;
|
|
24
|
+
minEffortToRecord;
|
|
25
|
+
batchWindowMs;
|
|
26
|
+
onBatchFlushed;
|
|
27
|
+
pendingSignals = [];
|
|
28
|
+
batchTimer = null;
|
|
29
|
+
batchStartTime = null;
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this.store = config.store;
|
|
32
|
+
this.agentId = config.agentId;
|
|
33
|
+
this.terminalPrefix = config.terminalPrefix;
|
|
34
|
+
this.sessionId = config.sessionId;
|
|
35
|
+
this.autoRecord = config.autoRecord ?? true;
|
|
36
|
+
this.minEffortToRecord = config.minEffortToRecord ?? DEFAULT_MIN_EFFORT;
|
|
37
|
+
this.batchWindowMs = config.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
|
|
38
|
+
this.onBatchFlushed = config.onBatchFlushed;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create an AfterToolHook for observing tool calls.
|
|
42
|
+
* Always returns undefined (observation only — never modifies results).
|
|
43
|
+
*/
|
|
44
|
+
createHook() {
|
|
45
|
+
return (context) => {
|
|
46
|
+
if (!this.autoRecord)
|
|
47
|
+
return undefined;
|
|
48
|
+
const success = context.result.success;
|
|
49
|
+
if (!isSignificantWork(context.toolName, context.input, success)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const signal = {
|
|
53
|
+
toolName: context.toolName,
|
|
54
|
+
files: extractAffectedFiles(context.toolName, context.input),
|
|
55
|
+
linesChanged: extractLinesChanged(context.result),
|
|
56
|
+
durationMs: context.durationMs,
|
|
57
|
+
success,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
this.pendingSignals.push(signal);
|
|
61
|
+
this.scheduleBatch();
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Force-flush all pending signals as a WorkEpisode.
|
|
67
|
+
* Called on shutdown to avoid losing data.
|
|
68
|
+
*/
|
|
69
|
+
flush() {
|
|
70
|
+
if (this.batchTimer) {
|
|
71
|
+
clearTimeout(this.batchTimer);
|
|
72
|
+
this.batchTimer = null;
|
|
73
|
+
}
|
|
74
|
+
this.buildAndSaveEpisode();
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Internal
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
scheduleBatch() {
|
|
80
|
+
if (!this.batchStartTime) {
|
|
81
|
+
this.batchStartTime = Date.now();
|
|
82
|
+
}
|
|
83
|
+
// Clear existing timer and set a new one
|
|
84
|
+
if (this.batchTimer) {
|
|
85
|
+
clearTimeout(this.batchTimer);
|
|
86
|
+
}
|
|
87
|
+
this.batchTimer = setTimeout(() => {
|
|
88
|
+
this.batchTimer = null;
|
|
89
|
+
this.buildAndSaveEpisode();
|
|
90
|
+
}, this.batchWindowMs);
|
|
91
|
+
}
|
|
92
|
+
buildAndSaveEpisode() {
|
|
93
|
+
if (this.pendingSignals.length === 0) {
|
|
94
|
+
this.batchStartTime = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const signals = [...this.pendingSignals];
|
|
98
|
+
this.pendingSignals = [];
|
|
99
|
+
const startTime = this.batchStartTime;
|
|
100
|
+
this.batchStartTime = null;
|
|
101
|
+
// Collect unique files, tool names, total lines
|
|
102
|
+
const allFiles = new Set();
|
|
103
|
+
const toolNames = new Set();
|
|
104
|
+
let totalLines = 0;
|
|
105
|
+
let totalDurationMs = 0;
|
|
106
|
+
for (const s of signals) {
|
|
107
|
+
for (const f of s.files)
|
|
108
|
+
allFiles.add(f);
|
|
109
|
+
toolNames.add(s.toolName);
|
|
110
|
+
totalLines += s.linesChanged;
|
|
111
|
+
totalDurationMs += s.durationMs;
|
|
112
|
+
}
|
|
113
|
+
const files = [...allFiles];
|
|
114
|
+
// Build effort signals
|
|
115
|
+
const effortSignals = {
|
|
116
|
+
fileCount: files.length,
|
|
117
|
+
linesChanged: totalLines,
|
|
118
|
+
toolCallCount: signals.length,
|
|
119
|
+
durationMs: totalDurationMs,
|
|
120
|
+
iterationCount: signals.length,
|
|
121
|
+
complexityIndicators: {
|
|
122
|
+
newFiles: toolNames.has('write_file'),
|
|
123
|
+
multiLanguage: hasMultipleLanguages(files),
|
|
124
|
+
tests: files.some(isTestFile),
|
|
125
|
+
configChanges: files.some(isConfigFile),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const effort = estimateEffort(effortSignals);
|
|
129
|
+
// Skip if below minimum effort threshold
|
|
130
|
+
if (EFFORT_ORDER.indexOf(effort) < EFFORT_ORDER.indexOf(this.minEffortToRecord)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const episode = {
|
|
134
|
+
id: randomUUID(),
|
|
135
|
+
agentId: this.agentId,
|
|
136
|
+
terminalPrefix: this.terminalPrefix,
|
|
137
|
+
action: inferAction([...toolNames]),
|
|
138
|
+
summary: buildSummary([...toolNames], files, totalLines),
|
|
139
|
+
files,
|
|
140
|
+
linesChanged: totalLines > 0 ? totalLines : undefined,
|
|
141
|
+
timestamp: signals[0].timestamp,
|
|
142
|
+
sessionId: this.sessionId,
|
|
143
|
+
effort,
|
|
144
|
+
durationMs: startTime ? Date.now() - startTime : totalDurationMs,
|
|
145
|
+
toolCalls: signals.length,
|
|
146
|
+
};
|
|
147
|
+
void this.store.save(episode);
|
|
148
|
+
this.onBatchFlushed?.();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Helpers (module-private)
|
|
153
|
+
// =============================================================================
|
|
154
|
+
function inferAction(toolNames) {
|
|
155
|
+
if (toolNames.includes('git_commit'))
|
|
156
|
+
return 'commit';
|
|
157
|
+
if (toolNames.includes('run_tests'))
|
|
158
|
+
return 'test';
|
|
159
|
+
if (toolNames.includes('run_lint'))
|
|
160
|
+
return 'lint';
|
|
161
|
+
if (toolNames.includes('run_build'))
|
|
162
|
+
return 'build';
|
|
163
|
+
if (toolNames.includes('write_file') && !toolNames.includes('edit'))
|
|
164
|
+
return 'create';
|
|
165
|
+
if (toolNames.includes('edit'))
|
|
166
|
+
return 'edit';
|
|
167
|
+
return 'modify';
|
|
168
|
+
}
|
|
169
|
+
function buildSummary(toolNames, files, totalLines) {
|
|
170
|
+
const action = inferAction(toolNames);
|
|
171
|
+
const fileStr = files.length === 1 ? files[0] : `${String(files.length)} files`;
|
|
172
|
+
const lineStr = totalLines > 0 ? ` (${String(totalLines)} lines)` : '';
|
|
173
|
+
return `${action}: ${fileStr}${lineStr}`;
|
|
174
|
+
}
|
|
175
|
+
function hasMultipleLanguages(files) {
|
|
176
|
+
const extensions = new Set();
|
|
177
|
+
for (const f of files) {
|
|
178
|
+
const ext = f.split('.').pop();
|
|
179
|
+
if (ext)
|
|
180
|
+
extensions.add(ext);
|
|
181
|
+
}
|
|
182
|
+
return extensions.size > 1;
|
|
183
|
+
}
|
|
184
|
+
function isTestFile(file) {
|
|
185
|
+
return /\.(test|spec)\.[a-z]+$/i.test(file) || file.includes('__tests__');
|
|
186
|
+
}
|
|
187
|
+
function isConfigFile(file) {
|
|
188
|
+
const base = file.split('/').pop() ?? '';
|
|
189
|
+
return (base.startsWith('.') ||
|
|
190
|
+
base === 'package.json' ||
|
|
191
|
+
base === 'tsconfig.json' ||
|
|
192
|
+
base.endsWith('.config.ts') ||
|
|
193
|
+
base.endsWith('.config.js') ||
|
|
194
|
+
base.endsWith('.config.mjs'));
|
|
195
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Significant Work Detection
|
|
3
|
+
*
|
|
4
|
+
* Determines which tool calls represent meaningful work (writes, edits,
|
|
5
|
+
* commits, runs) versus read-only exploration (reads, greps, globs).
|
|
6
|
+
*
|
|
7
|
+
* Uses hardcoded tool-name strings rather than a TOOL_NAMES constant so
|
|
8
|
+
* this module has zero dependency on any host's tool registry. Both
|
|
9
|
+
* agents-coding tools and platform tools use stable canonical names.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Check if a tool call represents significant work worth recording.
|
|
13
|
+
* Returns false for failed tool calls (fail-closed: don't record
|
|
14
|
+
* broken work).
|
|
15
|
+
*/
|
|
16
|
+
export declare function isSignificantWork(toolName: string, _input: Record<string, unknown>, success: boolean): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Extract affected file paths from a tool call's input.
|
|
19
|
+
* Returns an empty array if no files can be extracted.
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractAffectedFiles(toolName: string, input: Record<string, unknown>): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Extract lines changed from a tool result, best-effort.
|
|
24
|
+
* Returns 0 if the information is not available.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractLinesChanged(result: Record<string, unknown>): number;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Significant Work Detection
|
|
3
|
+
*
|
|
4
|
+
* Determines which tool calls represent meaningful work (writes, edits,
|
|
5
|
+
* commits, runs) versus read-only exploration (reads, greps, globs).
|
|
6
|
+
*
|
|
7
|
+
* Uses hardcoded tool-name strings rather than a TOOL_NAMES constant so
|
|
8
|
+
* this module has zero dependency on any host's tool registry. Both
|
|
9
|
+
* agents-coding tools and platform tools use stable canonical names.
|
|
10
|
+
*/
|
|
11
|
+
const SIGNIFICANT_TOOLS = new Set([
|
|
12
|
+
'write_file',
|
|
13
|
+
'edit',
|
|
14
|
+
'git_commit',
|
|
15
|
+
'git_stash',
|
|
16
|
+
'run_tests',
|
|
17
|
+
'run_lint',
|
|
18
|
+
'run_build',
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Check if a tool call represents significant work worth recording.
|
|
22
|
+
* Returns false for failed tool calls (fail-closed: don't record
|
|
23
|
+
* broken work).
|
|
24
|
+
*/
|
|
25
|
+
export function isSignificantWork(toolName, _input, success) {
|
|
26
|
+
if (!success)
|
|
27
|
+
return false;
|
|
28
|
+
return SIGNIFICANT_TOOLS.has(toolName);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract affected file paths from a tool call's input.
|
|
32
|
+
* Returns an empty array if no files can be extracted.
|
|
33
|
+
*/
|
|
34
|
+
export function extractAffectedFiles(toolName, input) {
|
|
35
|
+
if (toolName === 'write_file' && typeof input.path === 'string') {
|
|
36
|
+
return [input.path];
|
|
37
|
+
}
|
|
38
|
+
if (toolName === 'edit' && typeof input.filePath === 'string') {
|
|
39
|
+
return [input.filePath];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract lines changed from a tool result, best-effort.
|
|
45
|
+
* Returns 0 if the information is not available.
|
|
46
|
+
*/
|
|
47
|
+
export function extractLinesChanged(result) {
|
|
48
|
+
// Some tools report linesWritten directly
|
|
49
|
+
if (typeof result.linesWritten === 'number') {
|
|
50
|
+
return result.linesWritten;
|
|
51
|
+
}
|
|
52
|
+
// Edit results may have linesAdded + linesRemoved
|
|
53
|
+
const added = typeof result.linesAdded === 'number' ? result.linesAdded : 0;
|
|
54
|
+
const removed = typeof result.linesRemoved === 'number' ? result.linesRemoved : 0;
|
|
55
|
+
if (added > 0 || removed > 0) {
|
|
56
|
+
return added + removed;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-Based Episode Store
|
|
3
|
+
*
|
|
4
|
+
* Persists work episodes to a JSON file per project. Uses an in-memory
|
|
5
|
+
* cache — all reads are synchronous, writes are async (fire-and-forget).
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkEpisode, ProjectWorkSummary, Effort, EpisodeStore } from '@compilr-dev/agents';
|
|
8
|
+
export interface FileEpisodeStoreOptions {
|
|
9
|
+
/** Path to the episodes JSON file */
|
|
10
|
+
filePath: string;
|
|
11
|
+
/** Maximum episodes to keep (oldest pruned on flush). Default: 1000 */
|
|
12
|
+
maxEpisodes?: number;
|
|
13
|
+
/** Maximum age in milliseconds for cleanup. Default: 30 days */
|
|
14
|
+
maxAgeMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class FileEpisodeStore implements EpisodeStore {
|
|
17
|
+
private readonly filePath;
|
|
18
|
+
private readonly maxEpisodes;
|
|
19
|
+
private readonly maxAgeMs;
|
|
20
|
+
private _episodes;
|
|
21
|
+
private flushPromise;
|
|
22
|
+
constructor(options: FileEpisodeStoreOptions);
|
|
23
|
+
/** Lazy-load and return the in-memory episode cache */
|
|
24
|
+
private get episodes();
|
|
25
|
+
save(episode: WorkEpisode): void;
|
|
26
|
+
saveBatch(episodes: WorkEpisode[]): void;
|
|
27
|
+
getAll(): WorkEpisode[];
|
|
28
|
+
getByFiles(files: string[]): WorkEpisode[];
|
|
29
|
+
getByAgent(agentId: string): WorkEpisode[];
|
|
30
|
+
getBySession(sessionId: string): WorkEpisode[];
|
|
31
|
+
getByTimeRange(start: string, end: string): WorkEpisode[];
|
|
32
|
+
getRecent(count: number): WorkEpisode[];
|
|
33
|
+
getWorkSummary(): ProjectWorkSummary;
|
|
34
|
+
getTotalEffort(episodes?: WorkEpisode[]): Effort;
|
|
35
|
+
cleanup(maxAgeMs?: number): number;
|
|
36
|
+
private loadFromDisk;
|
|
37
|
+
private flush;
|
|
38
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-Based Episode Store
|
|
3
|
+
*
|
|
4
|
+
* Persists work episodes to a JSON file per project. Uses an in-memory
|
|
5
|
+
* cache — all reads are synchronous, writes are async (fire-and-forget).
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { EFFORT_ORDER } from '@compilr-dev/agents';
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// =============================================================================
|
|
13
|
+
const DEFAULT_MAX_EPISODES = 1000;
|
|
14
|
+
const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Implementation
|
|
17
|
+
// =============================================================================
|
|
18
|
+
export class FileEpisodeStore {
|
|
19
|
+
filePath;
|
|
20
|
+
maxEpisodes;
|
|
21
|
+
maxAgeMs;
|
|
22
|
+
_episodes = null; // lazy-loaded
|
|
23
|
+
flushPromise = null;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.filePath = options.filePath;
|
|
26
|
+
this.maxEpisodes = options.maxEpisodes ?? DEFAULT_MAX_EPISODES;
|
|
27
|
+
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
28
|
+
}
|
|
29
|
+
/** Lazy-load and return the in-memory episode cache */
|
|
30
|
+
get episodes() {
|
|
31
|
+
if (this._episodes === null) {
|
|
32
|
+
this._episodes = this.loadFromDisk();
|
|
33
|
+
}
|
|
34
|
+
return this._episodes;
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Write methods
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
save(episode) {
|
|
40
|
+
this.episodes.push(episode);
|
|
41
|
+
void this.flush();
|
|
42
|
+
}
|
|
43
|
+
saveBatch(episodes) {
|
|
44
|
+
if (episodes.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
this.episodes.push(...episodes);
|
|
47
|
+
void this.flush();
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Read methods (synchronous — read from cache)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
getAll() {
|
|
53
|
+
return [...this.episodes];
|
|
54
|
+
}
|
|
55
|
+
getByFiles(files) {
|
|
56
|
+
const fileSet = new Set(files);
|
|
57
|
+
return this.episodes.filter((ep) => ep.files.some((f) => fileSet.has(f)));
|
|
58
|
+
}
|
|
59
|
+
getByAgent(agentId) {
|
|
60
|
+
return this.episodes.filter((ep) => ep.agentId === agentId);
|
|
61
|
+
}
|
|
62
|
+
getBySession(sessionId) {
|
|
63
|
+
return this.episodes.filter((ep) => ep.sessionId === sessionId);
|
|
64
|
+
}
|
|
65
|
+
getByTimeRange(start, end) {
|
|
66
|
+
return this.episodes.filter((ep) => ep.timestamp >= start && ep.timestamp <= end);
|
|
67
|
+
}
|
|
68
|
+
getRecent(count) {
|
|
69
|
+
return this.episodes.slice(-count);
|
|
70
|
+
}
|
|
71
|
+
getWorkSummary() {
|
|
72
|
+
const eps = this.episodes;
|
|
73
|
+
// Agent breakdown
|
|
74
|
+
const agentMap = new Map();
|
|
75
|
+
for (const ep of eps) {
|
|
76
|
+
const entry = agentMap.get(ep.agentId) ?? {
|
|
77
|
+
count: 0,
|
|
78
|
+
maxEffort: 'trivial',
|
|
79
|
+
timeMs: 0,
|
|
80
|
+
};
|
|
81
|
+
entry.count++;
|
|
82
|
+
if (EFFORT_ORDER.indexOf(ep.effort) > EFFORT_ORDER.indexOf(entry.maxEffort)) {
|
|
83
|
+
entry.maxEffort = ep.effort;
|
|
84
|
+
}
|
|
85
|
+
entry.timeMs += ep.durationMs ?? 0;
|
|
86
|
+
agentMap.set(ep.agentId, entry);
|
|
87
|
+
}
|
|
88
|
+
// Top files
|
|
89
|
+
const fileCount = new Map();
|
|
90
|
+
for (const ep of eps) {
|
|
91
|
+
for (const f of ep.files) {
|
|
92
|
+
fileCount.set(f, (fileCount.get(f) ?? 0) + 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const topFiles = [...fileCount.entries()]
|
|
96
|
+
.sort((a, b) => b[1] - a[1])
|
|
97
|
+
.slice(0, 10)
|
|
98
|
+
.map(([p, touchCount]) => ({ path: p, touchCount }));
|
|
99
|
+
// Uncommitted work: episodes after last git_commit episode
|
|
100
|
+
let lastCommitIdx = -1;
|
|
101
|
+
for (let i = eps.length - 1; i >= 0; i--) {
|
|
102
|
+
if (eps[i].action === 'commit') {
|
|
103
|
+
lastCommitIdx = i;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const uncommittedWork = lastCommitIdx >= 0 ? eps.slice(lastCommitIdx + 1) : [...eps];
|
|
108
|
+
return {
|
|
109
|
+
episodeCount: eps.length,
|
|
110
|
+
totalEffort: this.getTotalEffort(eps),
|
|
111
|
+
timeSpentMs: eps.reduce((sum, ep) => sum + (ep.durationMs ?? 0), 0),
|
|
112
|
+
agentBreakdown: [...agentMap.entries()].map(([agentId, entry]) => ({
|
|
113
|
+
agentId,
|
|
114
|
+
episodeCount: entry.count,
|
|
115
|
+
maxEffort: entry.maxEffort,
|
|
116
|
+
timeSpentMs: entry.timeMs,
|
|
117
|
+
})),
|
|
118
|
+
topFiles,
|
|
119
|
+
uncommittedWork,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
getTotalEffort(episodes) {
|
|
123
|
+
const eps = episodes ?? this.episodes;
|
|
124
|
+
if (eps.length === 0)
|
|
125
|
+
return 'trivial';
|
|
126
|
+
return maxEffort(eps);
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Cleanup
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
cleanup(maxAgeMs) {
|
|
132
|
+
const cutoff = new Date(Date.now() - (maxAgeMs ?? this.maxAgeMs)).toISOString();
|
|
133
|
+
const before = this.episodes.length;
|
|
134
|
+
this._episodes = this.episodes.filter((ep) => ep.timestamp > cutoff);
|
|
135
|
+
const removed = before - this._episodes.length;
|
|
136
|
+
if (removed > 0) {
|
|
137
|
+
void this.flush();
|
|
138
|
+
}
|
|
139
|
+
return removed;
|
|
140
|
+
}
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Internal
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
loadFromDisk() {
|
|
145
|
+
try {
|
|
146
|
+
if (!fs.existsSync(this.filePath))
|
|
147
|
+
return [];
|
|
148
|
+
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
149
|
+
const data = JSON.parse(raw);
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data from disk, version may differ
|
|
151
|
+
if (data.version === 1 && Array.isArray(data.episodes)) {
|
|
152
|
+
return data.episodes;
|
|
153
|
+
}
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Corrupted file — start fresh
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async flush() {
|
|
162
|
+
// Coalesce concurrent flushes
|
|
163
|
+
if (this.flushPromise)
|
|
164
|
+
return this.flushPromise;
|
|
165
|
+
this.flushPromise = (async () => {
|
|
166
|
+
try {
|
|
167
|
+
const dir = path.dirname(this.filePath);
|
|
168
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
169
|
+
// Prune to max episodes (keep most recent)
|
|
170
|
+
const eps = this.episodes;
|
|
171
|
+
const prunedEpisodes = eps.length > this.maxEpisodes ? eps.slice(-this.maxEpisodes) : eps;
|
|
172
|
+
if (prunedEpisodes !== eps) {
|
|
173
|
+
this._episodes = prunedEpisodes;
|
|
174
|
+
}
|
|
175
|
+
const data = {
|
|
176
|
+
version: 1,
|
|
177
|
+
episodes: prunedEpisodes,
|
|
178
|
+
lastCleanup: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
await fs.promises.writeFile(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Silently fail — episodes are best-effort, never crash the host
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
this.flushPromise = null;
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
return this.flushPromise;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// =============================================================================
|
|
193
|
+
// Helpers (module-private)
|
|
194
|
+
// =============================================================================
|
|
195
|
+
function maxEffort(episodes) {
|
|
196
|
+
let max = 'trivial';
|
|
197
|
+
for (const ep of episodes) {
|
|
198
|
+
if (EFFORT_ORDER.indexOf(ep.effort) > EFFORT_ORDER.indexOf(max)) {
|
|
199
|
+
max = ep.effort;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return max;
|
|
203
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episode Module Internal Types
|
|
3
|
+
*
|
|
4
|
+
* Re-exports core types from `@compilr-dev/agents` and adds the
|
|
5
|
+
* internal types needed by the file-based store and the recorder.
|
|
6
|
+
*/
|
|
7
|
+
export type { Effort, WorkEpisode, EffortSignals, EffortWeights, EffortSummary, ProjectWorkSummary, WorkAtRisk, EpisodeStore, } from '@compilr-dev/agents';
|
|
8
|
+
/**
|
|
9
|
+
* Pending tool signal — accumulated during a batch window before
|
|
10
|
+
* being rolled into a WorkEpisode.
|
|
11
|
+
*/
|
|
12
|
+
export interface PendingToolSignal {
|
|
13
|
+
/** Tool name that was invoked */
|
|
14
|
+
toolName: string;
|
|
15
|
+
/** Files affected by this tool call */
|
|
16
|
+
files: string[];
|
|
17
|
+
/** Lines changed, if known */
|
|
18
|
+
linesChanged: number;
|
|
19
|
+
/** Duration of the tool call in milliseconds */
|
|
20
|
+
durationMs: number;
|
|
21
|
+
/** Whether the tool call succeeded */
|
|
22
|
+
success: boolean;
|
|
23
|
+
/** ISO timestamp of the tool call */
|
|
24
|
+
timestamp: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* On-disk format for the episodes JSON file.
|
|
28
|
+
*/
|
|
29
|
+
export interface EpisodeFile {
|
|
30
|
+
/** File format version */
|
|
31
|
+
version: 1;
|
|
32
|
+
/** All stored episodes */
|
|
33
|
+
episodes: import('@compilr-dev/agents').WorkEpisode[];
|
|
34
|
+
/** ISO timestamp of last cleanup */
|
|
35
|
+
lastCleanup: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work At Risk Query
|
|
3
|
+
*
|
|
4
|
+
* Pure function to query episodes by affected files and build a
|
|
5
|
+
* WorkAtRisk summary. Used by guardrails and rehearsal to warn about
|
|
6
|
+
* destructive operations.
|
|
7
|
+
*/
|
|
8
|
+
import type { EpisodeStore, WorkAtRisk } from '@compilr-dev/agents';
|
|
9
|
+
/**
|
|
10
|
+
* Query episodes that touch any of the given files and build a
|
|
11
|
+
* WorkAtRisk summary. Returns null if no episodes are at risk.
|
|
12
|
+
*/
|
|
13
|
+
export declare function queryWorkAtRisk(store: EpisodeStore, files: string[]): WorkAtRisk | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work At Risk Query
|
|
3
|
+
*
|
|
4
|
+
* Pure function to query episodes by affected files and build a
|
|
5
|
+
* WorkAtRisk summary. Used by guardrails and rehearsal to warn about
|
|
6
|
+
* destructive operations.
|
|
7
|
+
*/
|
|
8
|
+
import { EFFORT_ORDER } from '@compilr-dev/agents';
|
|
9
|
+
/**
|
|
10
|
+
* Query episodes that touch any of the given files and build a
|
|
11
|
+
* WorkAtRisk summary. Returns null if no episodes are at risk.
|
|
12
|
+
*/
|
|
13
|
+
export function queryWorkAtRisk(store, files) {
|
|
14
|
+
const episodes = store.getByFiles(files);
|
|
15
|
+
if (episodes.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
// Max effort
|
|
18
|
+
let totalEffort = 'trivial';
|
|
19
|
+
for (const ep of episodes) {
|
|
20
|
+
if (EFFORT_ORDER.indexOf(ep.effort) > EFFORT_ORDER.indexOf(totalEffort)) {
|
|
21
|
+
totalEffort = ep.effort;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Agent attribution: "agentId (terminalPrefix)"
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const agentAttribution = [];
|
|
27
|
+
for (const ep of episodes) {
|
|
28
|
+
const key = `${ep.agentId} (${ep.terminalPrefix})`;
|
|
29
|
+
if (!seen.has(key)) {
|
|
30
|
+
seen.add(key);
|
|
31
|
+
agentAttribution.push(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Warning message
|
|
35
|
+
const fileCount = new Set(episodes.flatMap((ep) => ep.files)).size;
|
|
36
|
+
const warningMessage = `⚠️ ${String(episodes.length)} episode(s) of work (effort: ${totalEffort}) ` +
|
|
37
|
+
`on ${String(fileCount)} file(s) by ${agentAttribution.join(', ')} would be affected.`;
|
|
38
|
+
return { episodes, totalEffort, agentAttribution, warningMessage };
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Summary Anchor
|
|
3
|
+
*
|
|
4
|
+
* Manages an auto-generated anchor that summarizes work done in the
|
|
5
|
+
* current session. Updated automatically after each episode batch is
|
|
6
|
+
* flushed (wire via `EpisodeRecorderConfig.onBatchFlushed`).
|
|
7
|
+
*/
|
|
8
|
+
import type { EpisodeStore, ProjectWorkSummary, AnchorManager } from '@compilr-dev/agents';
|
|
9
|
+
export interface WorkSummaryAnchorConfig {
|
|
10
|
+
store: EpisodeStore;
|
|
11
|
+
projectId: string;
|
|
12
|
+
getAnchorManager: (projectId: string) => AnchorManager;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the anchor content string from the store's work summary.
|
|
16
|
+
* Returns null when there are no episodes to summarize.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildWorkSummaryContent(summary: ProjectWorkSummary): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Update the work summary anchor for a project.
|
|
21
|
+
* Removes existing anchor (if any) and adds updated one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function updateWorkSummaryAnchor(config: WorkSummaryAnchorConfig): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Summary Anchor
|
|
3
|
+
*
|
|
4
|
+
* Manages an auto-generated anchor that summarizes work done in the
|
|
5
|
+
* current session. Updated automatically after each episode batch is
|
|
6
|
+
* flushed (wire via `EpisodeRecorderConfig.onBatchFlushed`).
|
|
7
|
+
*/
|
|
8
|
+
const ANCHOR_TAG = 'episode-work-summary';
|
|
9
|
+
/**
|
|
10
|
+
* Build the anchor content string from the store's work summary.
|
|
11
|
+
* Returns null when there are no episodes to summarize.
|
|
12
|
+
*/
|
|
13
|
+
export function buildWorkSummaryContent(summary) {
|
|
14
|
+
if (summary.episodeCount === 0)
|
|
15
|
+
return null;
|
|
16
|
+
const lines = ['[Session Work Summary]'];
|
|
17
|
+
lines.push(`Episodes: ${String(summary.episodeCount)}, Effort: ${summary.totalEffort}, Time: ${formatMs(summary.timeSpentMs)}`);
|
|
18
|
+
if (summary.agentBreakdown.length > 0) {
|
|
19
|
+
const agents = summary.agentBreakdown
|
|
20
|
+
.map((a) => `${a.agentId}(${String(a.episodeCount)})`)
|
|
21
|
+
.join(', ');
|
|
22
|
+
lines.push(`Agents: ${agents}`);
|
|
23
|
+
}
|
|
24
|
+
if (summary.topFiles.length > 0) {
|
|
25
|
+
const files = summary.topFiles
|
|
26
|
+
.slice(0, 5)
|
|
27
|
+
.map((f) => f.path.split('/').pop() ?? f.path)
|
|
28
|
+
.join(', ');
|
|
29
|
+
lines.push(`Top files: ${files}`);
|
|
30
|
+
}
|
|
31
|
+
if (summary.uncommittedWork.length > 0) {
|
|
32
|
+
lines.push(`Uncommitted: ${String(summary.uncommittedWork.length)} episode(s)`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Update the work summary anchor for a project.
|
|
38
|
+
* Removes existing anchor (if any) and adds updated one.
|
|
39
|
+
*/
|
|
40
|
+
export function updateWorkSummaryAnchor(config) {
|
|
41
|
+
const { store, projectId, getAnchorManager: getManager } = config;
|
|
42
|
+
const manager = getManager(projectId);
|
|
43
|
+
const summary = store.getWorkSummary();
|
|
44
|
+
const content = buildWorkSummaryContent(summary);
|
|
45
|
+
// Remove existing work summary anchor
|
|
46
|
+
const existing = manager.getAll().find((a) => a.tags?.includes(ANCHOR_TAG));
|
|
47
|
+
if (existing)
|
|
48
|
+
manager.remove(existing.id);
|
|
49
|
+
// Add new anchor if there's content
|
|
50
|
+
if (content) {
|
|
51
|
+
manager.add({
|
|
52
|
+
content,
|
|
53
|
+
priority: 'info',
|
|
54
|
+
scope: 'persistent',
|
|
55
|
+
tags: [ANCHOR_TAG],
|
|
56
|
+
projectId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Format milliseconds to a human-readable string (e.g., "2h 15m", "45s").
|
|
62
|
+
*/
|
|
63
|
+
function formatMs(ms) {
|
|
64
|
+
if (ms < 1000)
|
|
65
|
+
return `${String(ms)}ms`;
|
|
66
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
67
|
+
if (totalSeconds < 60)
|
|
68
|
+
return `${String(totalSeconds)}s`;
|
|
69
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
70
|
+
if (minutes < 60)
|
|
71
|
+
return `${String(minutes)}m`;
|
|
72
|
+
const hours = Math.floor(minutes / 60);
|
|
73
|
+
const remainingMinutes = minutes % 60;
|
|
74
|
+
return remainingMinutes > 0
|
|
75
|
+
? `${String(hours)}h ${String(remainingMinutes)}m`
|
|
76
|
+
: `${String(hours)}h`;
|
|
77
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,9 @@ export { defineTool, createSuccessResult, createErrorResult, mergeHooks, createL
|
|
|
81
81
|
export type { Tool, HooksConfig, AgentEvent, Message, LLMProvider, AnchorInput, ToolExecutionResult, AgentRunResult, PermissionHandler, PermissionHandlerResponse, ToolPermission, AgentTypeConfig, GuardrailTriggeredHandler, BeforeLLMHookResult, BeforeToolHook, BeforeToolHookResult, AfterToolHook, AgentState, AgentConfig, SessionInfo, Anchor, AnchorScope, AnchorClearOptions, AnchorPriority, AnchorQueryOptions, FileAccessType, FileAccess, GuardrailResult, GuardrailContext, MCPClient, MCPToolDefinition, } from '@compilr-dev/agents';
|
|
82
82
|
export { DEFAULT_PERMISSION_RULES, findMatchingRule, permissionModeLabel, permissionLevelLabel, } from './permissions.js';
|
|
83
83
|
export type { PermissionRule, PermissionMode, PermissionLevel } from './permissions.js';
|
|
84
|
+
export { DEFAULT_DELEGATION_CONFIG } from './delegation.js';
|
|
85
|
+
export { FileEpisodeStore, EpisodeRecorder, isSignificantWork, extractAffectedFiles, extractLinesChanged, queryWorkAtRisk, buildWorkSummaryContent, updateWorkSummaryAnchor, } from './episodes/index.js';
|
|
86
|
+
export type { FileEpisodeStoreOptions, EpisodeRecorderConfig, WorkSummaryAnchorConfig, PendingToolSignal, EpisodeFile, } from './episodes/index.js';
|
|
84
87
|
export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, } from './mcp-config.js';
|
|
85
88
|
export type { MCPServerEntry, MCPConfigFile, ResolvedMCPServer } from './mcp-config.js';
|
|
86
89
|
export { generateProject, isGitConfigured, generateCompilrMd, generateConfigJson, generateReadmeMd, generateCodingStandardsMd, generatePackageJson, generateTsconfig, generateGitignore, generateCompilrMdForImport, detectProjectInfo, detectGitInfo, prettifyName, getLanguageLabel, getFrameworkLabel, validateImportPath, isValidProjectName, projectExists, TECH_STACK_LABELS, CODING_STANDARDS_LABELS, REPO_PATTERN_LABELS, WORKFLOW_VERSION, } from './project-generator/index.js';
|
package/dist/index.js
CHANGED
|
@@ -198,6 +198,14 @@ AgentError, ProviderError, ToolError, ToolTimeoutError, MaxIterationsError, Abor
|
|
|
198
198
|
// =============================================================================
|
|
199
199
|
export { DEFAULT_PERMISSION_RULES, findMatchingRule, permissionModeLabel, permissionLevelLabel, } from './permissions.js';
|
|
200
200
|
// =============================================================================
|
|
201
|
+
// Shared Delegation Config Defaults
|
|
202
|
+
// =============================================================================
|
|
203
|
+
export { DEFAULT_DELEGATION_CONFIG } from './delegation.js';
|
|
204
|
+
// =============================================================================
|
|
205
|
+
// Episodes — Work History Tracking
|
|
206
|
+
// =============================================================================
|
|
207
|
+
export { FileEpisodeStore, EpisodeRecorder, isSignificantWork, extractAffectedFiles, extractLinesChanged, queryWorkAtRisk, buildWorkSummaryContent, updateWorkSummaryAnchor, } from './episodes/index.js';
|
|
208
|
+
// =============================================================================
|
|
201
209
|
// Shared MCP Configuration
|
|
202
210
|
// =============================================================================
|
|
203
211
|
export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, } from './mcp-config.js';
|