@compilr-dev/cli 0.8.7 → 0.8.8
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/commands-v2/handlers/settings.js +22 -3
- package/dist/commands-v2/types.d.ts +10 -0
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/index.js +68 -1
- package/dist/repl-v2.d.ts +19 -1
- package/dist/repl-v2.js +37 -0
- package/dist/ui/overlay-manager.js +10 -2
- package/dist/utils/model-tiers.js +14 -9
- package/package.json +3 -3
- package/dist/episodes/recorder.d.ts +0 -51
- package/dist/episodes/recorder.js +0 -195
- package/dist/episodes/significant-work.d.ts +0 -21
- package/dist/episodes/significant-work.js +0 -56
- package/dist/episodes/store.d.ts +0 -38
- package/dist/episodes/store.js +0 -199
- package/dist/episodes/types.d.ts +0 -35
- package/dist/episodes/types.js +0 -6
- package/dist/episodes/work-at-risk.d.ts +0 -12
- package/dist/episodes/work-at-risk.js +0 -38
- package/dist/episodes/work-summary-anchor.d.ts +0 -23
- package/dist/episodes/work-summary-anchor.js +0 -73
- package/dist/tools/consult.d.ts +0 -14
- package/dist/tools/consult.js +0 -73
- package/dist/tools/delegate-background.d.ts +0 -27
- package/dist/tools/delegate-background.js +0 -115
- package/dist/tools/delegate.d.ts +0 -28
- package/dist/tools/delegate.js +0 -113
- package/dist/tools/handoff.d.ts +0 -17
- package/dist/tools/handoff.js +0 -79
- package/dist/tools/meta-tools.d.ts +0 -31
- package/dist/tools/meta-tools.js +0 -78
- package/dist/ui/overlay/impl/skill-editor-overlay.d.ts +0 -56
- package/dist/ui/overlay/impl/skill-editor-overlay.js +0 -493
|
@@ -146,11 +146,30 @@ export const modelCommand = {
|
|
|
146
146
|
currentProvider: currentProvider === 'auto' ? undefined : currentProvider,
|
|
147
147
|
});
|
|
148
148
|
await ctx.ui.showOverlay(modelOverlay);
|
|
149
|
-
// After overlay closes,
|
|
150
|
-
//
|
|
149
|
+
// After overlay closes, apply the selection (the overlay writes both
|
|
150
|
+
// defaultModel and defaultProvider to settings).
|
|
151
151
|
if (agent) {
|
|
152
152
|
const newModel = getSetting('defaultModel');
|
|
153
|
-
|
|
153
|
+
const newProvider = getSetting('defaultProvider');
|
|
154
|
+
const providerChanged = !!newProvider && !!ctx.provider && newProvider !== ctx.provider;
|
|
155
|
+
if (providerChanged) {
|
|
156
|
+
// setModel() only swaps the model string on the existing provider
|
|
157
|
+
// client — it can't cross providers. Rebuild the agent on the new
|
|
158
|
+
// provider (history preserved) instead.
|
|
159
|
+
if (ctx.rebuildActiveAgent) {
|
|
160
|
+
const res = await ctx.rebuildActiveAgent(newProvider, newModel ?? '');
|
|
161
|
+
if (res.ok) {
|
|
162
|
+
ctx.ui.print({ type: 'success', message: `Switched to ${newProvider}${newModel ? ` · ${newModel}` : ''}` });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
ctx.ui.print({ type: 'error', message: res.error ?? 'Failed to switch provider' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
ctx.ui.print({ type: 'error', message: 'Provider switch takes effect on the next restart in this mode.' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (newModel && newModel !== agentModel) {
|
|
154
173
|
try {
|
|
155
174
|
agent.setModel(newModel);
|
|
156
175
|
const agentId = ctx.team ? ctx.team.getActive().id : 'default';
|
|
@@ -52,6 +52,16 @@ export interface CommandContextV2 {
|
|
|
52
52
|
* Used by /resume command to restore from a saved session.
|
|
53
53
|
*/
|
|
54
54
|
restoreAgent?: (agent: Agent) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Rebuild the active agent on a new provider (+ model), preserving history.
|
|
57
|
+
* Used by /model when the provider changes: setModel() only swaps the model
|
|
58
|
+
* string on the existing provider client, so it can't cross providers.
|
|
59
|
+
* Resolves { ok: false, error } if the target provider has no API key.
|
|
60
|
+
*/
|
|
61
|
+
rebuildActiveAgent?: (provider: string, model: string) => Promise<{
|
|
62
|
+
ok: boolean;
|
|
63
|
+
error?: string;
|
|
64
|
+
}>;
|
|
55
65
|
/** Current LLM model ID */
|
|
56
66
|
model?: string;
|
|
57
67
|
/** Current LLM provider (claude, openai, gemini, ollama) */
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -595,6 +595,63 @@ async function main() {
|
|
|
595
595
|
sharedState.grantSession = (toolName) => {
|
|
596
596
|
defaultAgentRef?.grantSessionPermission(toolName);
|
|
597
597
|
};
|
|
598
|
+
// Build a full-fidelity default agent for a given provider+model. Mirrors the
|
|
599
|
+
// initial createAgent() above (incl. plan-mode callbacks) so a hot-switched
|
|
600
|
+
// agent keeps every capability. Used by the REPL's rebuildActiveAgent() when
|
|
601
|
+
// /model changes the provider — setModel() can't cross providers. Throws if
|
|
602
|
+
// the target provider has no API key (surfaced to the user by the REPL).
|
|
603
|
+
const buildDefaultAgent = (agentProvider, agentModel) => {
|
|
604
|
+
const built = createAgent({
|
|
605
|
+
provider: agentProvider,
|
|
606
|
+
model: agentModel,
|
|
607
|
+
minimal: options.minimal,
|
|
608
|
+
quiet: true,
|
|
609
|
+
enableMetaTools: !options.minimal && getProviderMetadata(agentProvider).supportsMetaTools,
|
|
610
|
+
onPermissionRequest,
|
|
611
|
+
onIterationLimitReached,
|
|
612
|
+
onSuggest: (event) => {
|
|
613
|
+
sharedState.pendingSuggestion = event.action;
|
|
614
|
+
},
|
|
615
|
+
projectName: liveProjectState.dbProject?.displayName,
|
|
616
|
+
projectSlug: liveProjectState.dbProject?.name,
|
|
617
|
+
projectId: liveProjectState.dbProject?.id,
|
|
618
|
+
projectContext: liveProjectState.projectContext,
|
|
619
|
+
guidedModeContext: liveProjectState.guidedModeContext,
|
|
620
|
+
enableAnchors: true,
|
|
621
|
+
persistedAnchors: persistedAnchors.length > 0 ? persistedAnchors : undefined,
|
|
622
|
+
enableGuardrails: true,
|
|
623
|
+
onGuardrailTriggered,
|
|
624
|
+
onSubagentStart: (toolUseId, agentType, description) => {
|
|
625
|
+
sharedState.onSubagentStart?.(toolUseId, agentType, description);
|
|
626
|
+
},
|
|
627
|
+
onSubagentEnd: (toolUseId, success, tokenCount, error) => {
|
|
628
|
+
sharedState.onSubagentEnd?.(toolUseId, success, tokenCount, error);
|
|
629
|
+
},
|
|
630
|
+
onSubagentToolUse: (toolUseId, toolName, summary) => {
|
|
631
|
+
sharedState.onSubagentToolUse?.(toolUseId, toolName, summary);
|
|
632
|
+
},
|
|
633
|
+
onSubagentTrackingReady: (clearTracking) => {
|
|
634
|
+
sharedState.clearSubagentTracking = clearTracking;
|
|
635
|
+
},
|
|
636
|
+
episodeRecorder: defaultEpisodeRecorder,
|
|
637
|
+
planModeCallbacks: {
|
|
638
|
+
onPlanSubmit: async (info) => {
|
|
639
|
+
if (sharedState.onPlanSubmit)
|
|
640
|
+
return sharedState.onPlanSubmit(info);
|
|
641
|
+
return { action: 'reject' };
|
|
642
|
+
},
|
|
643
|
+
onPlanModeExit: async (info) => {
|
|
644
|
+
if (sharedState.onPlanModeExit)
|
|
645
|
+
await sharedState.onPlanModeExit(info);
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
defaultAgentRef = built;
|
|
650
|
+
sharedState.grantSession = (toolName) => {
|
|
651
|
+
defaultAgentRef?.grantSessionPermission(toolName);
|
|
652
|
+
};
|
|
653
|
+
return built;
|
|
654
|
+
};
|
|
598
655
|
// ==========================================================================
|
|
599
656
|
// Create AgentTeam with the default agent (or restore from persistence)
|
|
600
657
|
// ==========================================================================
|
|
@@ -828,6 +885,16 @@ async function main() {
|
|
|
828
885
|
onDefaultAgentSwapped: (newAgent) => {
|
|
829
886
|
defaultAgentRef = newAgent;
|
|
830
887
|
},
|
|
888
|
+
// Rebuild the default agent on a new provider (+ model) for /model's
|
|
889
|
+
// provider hot-switch. Throws if the provider has no API key. Also
|
|
890
|
+
// updates the outer provider/model so later team-agent creation and
|
|
891
|
+
// status reflect the switch.
|
|
892
|
+
onRebuildAgent: (rebuildProvider, rebuildModel) => {
|
|
893
|
+
const built = buildDefaultAgent(rebuildProvider, rebuildModel);
|
|
894
|
+
provider = rebuildProvider;
|
|
895
|
+
model = rebuildModel;
|
|
896
|
+
return Promise.resolve(built);
|
|
897
|
+
},
|
|
831
898
|
// Set session prefix for VS Code diff IPC after session registration
|
|
832
899
|
onSessionRegistered: (sessionId) => {
|
|
833
900
|
sharedState.sessionPrefix = sessionId.slice(0, 8);
|
|
@@ -968,7 +1035,7 @@ function getDefaultModel(provider) {
|
|
|
968
1035
|
return model.id;
|
|
969
1036
|
// Fallback to any supported model for this provider
|
|
970
1037
|
const anyModel = getDefaultModelForTier(provider, 'fast') ?? getDefaultModelForTier(provider, 'powerful');
|
|
971
|
-
return anyModel?.id ?? 'claude-sonnet-
|
|
1038
|
+
return anyModel?.id ?? 'claude-sonnet-5';
|
|
972
1039
|
}
|
|
973
1040
|
// =============================================================================
|
|
974
1041
|
// Run
|
package/dist/repl-v2.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface ReplV2Options {
|
|
|
34
34
|
team?: AgentTeam;
|
|
35
35
|
/** Agent factory function for creating new agents (for team switching) */
|
|
36
36
|
agentFactory?: (config: Record<string, unknown>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean) => Promise<Agent>;
|
|
37
|
+
/**
|
|
38
|
+
* Rebuild the default agent on a new provider (+ model), full fidelity.
|
|
39
|
+
* Implemented by index.ts (which holds the full createAgent context).
|
|
40
|
+
* Throws if the provider has no API key. Used by rebuildActiveAgent() to
|
|
41
|
+
* hot-switch providers without a restart.
|
|
42
|
+
*/
|
|
43
|
+
onRebuildAgent?: (provider: string, model: string) => Promise<Agent>;
|
|
37
44
|
/** Current model ID */
|
|
38
45
|
model?: string;
|
|
39
46
|
/** Current provider */
|
|
@@ -235,8 +242,9 @@ export declare class ReplV2 {
|
|
|
235
242
|
private agent?;
|
|
236
243
|
private team?;
|
|
237
244
|
private readonly agentFactory?;
|
|
245
|
+
private readonly onRebuildAgent?;
|
|
238
246
|
private readonly model;
|
|
239
|
-
private
|
|
247
|
+
private provider;
|
|
240
248
|
private readonly version;
|
|
241
249
|
private readonly updateAvailable;
|
|
242
250
|
private readonly projectName;
|
|
@@ -461,6 +469,16 @@ export declare class ReplV2 {
|
|
|
461
469
|
* Called by /resume command to swap in a restored agent.
|
|
462
470
|
*/
|
|
463
471
|
restoreAgent(newAgent: Agent): void;
|
|
472
|
+
/**
|
|
473
|
+
* Hot-switch the active agent to a new provider (+ model), preserving the
|
|
474
|
+
* conversation history. setModel() only swaps the model string on the
|
|
475
|
+
* existing provider client, so a provider change needs a full rebuild.
|
|
476
|
+
* Returns { ok: false, error } if rebuild fails (e.g. no API key).
|
|
477
|
+
*/
|
|
478
|
+
rebuildActiveAgent(provider: string, model: string): Promise<{
|
|
479
|
+
ok: boolean;
|
|
480
|
+
error?: string;
|
|
481
|
+
}>;
|
|
464
482
|
/**
|
|
465
483
|
* Get the current agent instance.
|
|
466
484
|
* Used by commands that need to serialize/checkpoint the agent state.
|
package/dist/repl-v2.js
CHANGED
|
@@ -171,6 +171,7 @@ export class ReplV2 {
|
|
|
171
171
|
agent;
|
|
172
172
|
team;
|
|
173
173
|
agentFactory;
|
|
174
|
+
onRebuildAgent;
|
|
174
175
|
model;
|
|
175
176
|
provider;
|
|
176
177
|
version;
|
|
@@ -235,6 +236,7 @@ export class ReplV2 {
|
|
|
235
236
|
this.agent = options.agent;
|
|
236
237
|
this.team = options.team;
|
|
237
238
|
this.agentFactory = options.agentFactory;
|
|
239
|
+
this.onRebuildAgent = options.onRebuildAgent;
|
|
238
240
|
this.model = options.model ?? 'simulation';
|
|
239
241
|
this.provider = options.provider ?? 'simulation';
|
|
240
242
|
this.version = options.version ?? STANDALONE_VERSION;
|
|
@@ -933,6 +935,38 @@ export class ReplV2 {
|
|
|
933
935
|
this.agent = newAgent;
|
|
934
936
|
this.onDefaultAgentSwapped?.(newAgent);
|
|
935
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Hot-switch the active agent to a new provider (+ model), preserving the
|
|
940
|
+
* conversation history. setModel() only swaps the model string on the
|
|
941
|
+
* existing provider client, so a provider change needs a full rebuild.
|
|
942
|
+
* Returns { ok: false, error } if rebuild fails (e.g. no API key).
|
|
943
|
+
*/
|
|
944
|
+
async rebuildActiveAgent(provider, model) {
|
|
945
|
+
if (!this.onRebuildAgent) {
|
|
946
|
+
return { ok: false, error: 'Provider switching is not available in this mode.' };
|
|
947
|
+
}
|
|
948
|
+
let fresh;
|
|
949
|
+
try {
|
|
950
|
+
fresh = await this.onRebuildAgent(provider, model);
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
954
|
+
}
|
|
955
|
+
// Carry the conversation history across the provider swap.
|
|
956
|
+
if (this.agent) {
|
|
957
|
+
try {
|
|
958
|
+
const history = this.agent.getHistory();
|
|
959
|
+
if (history.length > 0)
|
|
960
|
+
await fresh.setHistory(history);
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// Best-effort — a fresh agent on the right provider still beats a broken one.
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
this.restoreAgent(fresh);
|
|
967
|
+
this.provider = provider;
|
|
968
|
+
return { ok: true };
|
|
969
|
+
}
|
|
936
970
|
/**
|
|
937
971
|
* Get the current agent instance.
|
|
938
972
|
* Used by commands that need to serialize/checkpoint the agent state.
|
|
@@ -2000,6 +2034,9 @@ export class ReplV2 {
|
|
|
2000
2034
|
restoreAgent: (newAgent) => {
|
|
2001
2035
|
this.restoreAgent(newAgent);
|
|
2002
2036
|
},
|
|
2037
|
+
rebuildActiveAgent: (provider, model) => {
|
|
2038
|
+
return this.rebuildActiveAgent(provider, model);
|
|
2039
|
+
},
|
|
2003
2040
|
model: this.model,
|
|
2004
2041
|
provider: this.provider,
|
|
2005
2042
|
getContextManager: () => {
|
|
@@ -38,8 +38,14 @@ export class OverlayManager {
|
|
|
38
38
|
this.clearOverlayRender();
|
|
39
39
|
}
|
|
40
40
|
this.stack.push(overlay);
|
|
41
|
-
overlay
|
|
42
|
-
|
|
41
|
+
// Enter overlay mode (resets renderState + clears the footer) BEFORE wiring
|
|
42
|
+
// the render callback and mounting. Otherwise an async render triggered
|
|
43
|
+
// during the `await onMount` microtask gap — e.g. ModelOverlay's
|
|
44
|
+
// fetchOllamaModels() resolving and calling requestRender() — draws a frame
|
|
45
|
+
// and sets renderState.maxLineCount, then enterInlineOverlayMode() wipes
|
|
46
|
+
// that count to 0, so the final renderOverlay() clears nothing and draws a
|
|
47
|
+
// second frame below the first (the "ghost" overlay). Resetting first means
|
|
48
|
+
// any gap render is tracked and the final render clears it correctly.
|
|
43
49
|
if (!overlay.usesAlternateScreen) {
|
|
44
50
|
if (overlay.type === 'fullscreen') {
|
|
45
51
|
this.enterFullscreenOverlayMode();
|
|
@@ -48,6 +54,8 @@ export class OverlayManager {
|
|
|
48
54
|
this.enterInlineOverlayMode();
|
|
49
55
|
}
|
|
50
56
|
}
|
|
57
|
+
overlay.setRenderCallback?.(() => { this.requestOverlayRender(); });
|
|
58
|
+
await overlay.onMount?.();
|
|
51
59
|
this.renderOverlay();
|
|
52
60
|
return new Promise((resolve) => {
|
|
53
61
|
this.resolvers.set(overlay.id, resolve);
|
|
@@ -18,19 +18,23 @@
|
|
|
18
18
|
// Model Tier Mappings
|
|
19
19
|
// =============================================================================
|
|
20
20
|
const MODEL_TIERS = {
|
|
21
|
-
// Claude (Anthropic) -
|
|
21
|
+
// Claude (Anthropic) - current
|
|
22
|
+
'claude-fable-5': { tier: 'large', provider: 'anthropic' },
|
|
23
|
+
'claude-opus-4-8': { tier: 'large', provider: 'anthropic' },
|
|
24
|
+
'claude-sonnet-5': { tier: 'large', provider: 'anthropic' },
|
|
25
|
+
'claude-haiku-4-5': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-5' },
|
|
26
|
+
// Claude legacy (for backward compatibility)
|
|
27
|
+
'claude-opus-4-7': { tier: 'large', provider: 'anthropic' },
|
|
22
28
|
'claude-opus-4-6': { tier: 'large', provider: 'anthropic' },
|
|
23
29
|
'claude-sonnet-4-6': { tier: 'large', provider: 'anthropic' },
|
|
24
|
-
'claude-haiku-4-5': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-4-6' },
|
|
25
|
-
// Claude legacy (for backward compatibility)
|
|
26
30
|
'claude-opus-4-5': { tier: 'large', provider: 'anthropic' },
|
|
27
31
|
'claude-sonnet-4-5': { tier: 'large', provider: 'anthropic' },
|
|
28
32
|
'claude-opus-4-1': { tier: 'large', provider: 'anthropic' },
|
|
29
33
|
'claude-opus-4': { tier: 'large', provider: 'anthropic' },
|
|
30
34
|
'claude-sonnet-4': { tier: 'large', provider: 'anthropic' },
|
|
31
35
|
'claude-3-5-sonnet': { tier: 'large', provider: 'anthropic' },
|
|
32
|
-
'claude-3-5-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-
|
|
33
|
-
'claude-3-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-
|
|
36
|
+
'claude-3-5-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-5' },
|
|
37
|
+
'claude-3-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-5' },
|
|
34
38
|
// OpenAI - GPT-5 series (current)
|
|
35
39
|
'gpt-5.2': { tier: 'large', provider: 'openai' },
|
|
36
40
|
'gpt-5-mini': { tier: 'large', provider: 'openai' },
|
|
@@ -56,10 +60,11 @@ const MODEL_TIERS = {
|
|
|
56
60
|
'gemini-1.5-pro': { tier: 'large', provider: 'google' },
|
|
57
61
|
'gemini-1.5-flash': { tier: 'medium', provider: 'google', suggestedUpgrade: 'gemini-3-flash-preview' },
|
|
58
62
|
// Ollama / Local models (common ones)
|
|
59
|
-
'llama3.
|
|
60
|
-
'llama3.
|
|
61
|
-
'llama3.2': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.
|
|
62
|
-
'llama3.
|
|
63
|
+
'llama3.3:70b': { tier: 'large', provider: 'ollama' },
|
|
64
|
+
'llama3.3': { tier: 'large', provider: 'ollama' },
|
|
65
|
+
'llama3.2:3b': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.3:70b' },
|
|
66
|
+
'llama3.2': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.3:70b' },
|
|
67
|
+
'llama3.1': { tier: 'medium', provider: 'ollama', suggestedUpgrade: 'llama3.3:70b' },
|
|
63
68
|
'llama3.1:70b': { tier: 'large', provider: 'ollama' },
|
|
64
69
|
'mistral': { tier: 'small', provider: 'ollama' },
|
|
65
70
|
'mixtral': { tier: 'medium', provider: 'ollama' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compilr-dev/cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "AI-powered coding assistant CLI using @compilr-dev/agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
58
|
-
"@compilr-dev/agents": "^0.6.
|
|
58
|
+
"@compilr-dev/agents": "^0.6.8",
|
|
59
59
|
"@compilr-dev/agents-coding": "^1.0.4",
|
|
60
60
|
"@compilr-dev/editor-core": "^0.0.3",
|
|
61
61
|
"@compilr-dev/factory": "^0.1.35",
|
|
62
62
|
"@compilr-dev/logger": "^0.1.0",
|
|
63
|
-
"@compilr-dev/sdk": "^0.18.
|
|
63
|
+
"@compilr-dev/sdk": "^0.18.9",
|
|
64
64
|
"@compilr-dev/ui-core": "^0.0.1",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
66
66
|
"ansi-escapes": "^7.3.0",
|
|
@@ -1,51 +0,0 @@
|
|
|
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 session prefix (first 8 chars) */
|
|
14
|
-
terminalPrefix: string;
|
|
15
|
-
/** Session ID for grouping */
|
|
16
|
-
sessionId: string;
|
|
17
|
-
/** Enable automatic recording via hook (default: true) */
|
|
18
|
-
autoRecord?: boolean;
|
|
19
|
-
/** Minimum effort level to record (default: 'low') */
|
|
20
|
-
minEffortToRecord?: Effort;
|
|
21
|
-
/** Batch window in milliseconds (default: 5000) */
|
|
22
|
-
batchWindowMs?: number;
|
|
23
|
-
/** Called after each batch is flushed as a WorkEpisode */
|
|
24
|
-
onBatchFlushed?: () => void;
|
|
25
|
-
}
|
|
26
|
-
export declare class EpisodeRecorder {
|
|
27
|
-
private readonly store;
|
|
28
|
-
private readonly agentId;
|
|
29
|
-
private readonly terminalPrefix;
|
|
30
|
-
private readonly sessionId;
|
|
31
|
-
private readonly autoRecord;
|
|
32
|
-
private readonly minEffortToRecord;
|
|
33
|
-
private readonly batchWindowMs;
|
|
34
|
-
private readonly onBatchFlushed?;
|
|
35
|
-
private pendingSignals;
|
|
36
|
-
private batchTimer;
|
|
37
|
-
private batchStartTime;
|
|
38
|
-
constructor(config: EpisodeRecorderConfig);
|
|
39
|
-
/**
|
|
40
|
-
* Create an AfterToolHook for observing tool calls.
|
|
41
|
-
* Always returns undefined (observation only — never modifies results).
|
|
42
|
-
*/
|
|
43
|
-
createHook(): AfterToolHook;
|
|
44
|
-
/**
|
|
45
|
-
* Force-flush all pending signals as a WorkEpisode.
|
|
46
|
-
* Called on shutdown to avoid losing data.
|
|
47
|
-
*/
|
|
48
|
-
flush(): void;
|
|
49
|
-
private scheduleBatch;
|
|
50
|
-
private buildAndSaveEpisode;
|
|
51
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Significant Work Detection
|
|
3
|
-
*
|
|
4
|
-
* Determines which tool calls represent meaningful work (writes, edits, commits)
|
|
5
|
-
* versus read-only exploration (reads, greps, globs).
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Check if a tool call represents significant work worth recording.
|
|
9
|
-
* Returns false for failed tool calls (fail-closed: don't record broken work).
|
|
10
|
-
*/
|
|
11
|
-
export declare function isSignificantWork(toolName: string, _input: Record<string, unknown>, success: boolean): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Extract affected file paths from a tool call's input.
|
|
14
|
-
* Returns an empty array if no files can be extracted.
|
|
15
|
-
*/
|
|
16
|
-
export declare function extractAffectedFiles(toolName: string, input: Record<string, unknown>): string[];
|
|
17
|
-
/**
|
|
18
|
-
* Extract lines changed from a tool result, best-effort.
|
|
19
|
-
* Returns 0 if the information is not available.
|
|
20
|
-
*/
|
|
21
|
-
export declare function extractLinesChanged(result: Record<string, unknown>): number;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Significant Work Detection
|
|
3
|
-
*
|
|
4
|
-
* Determines which tool calls represent meaningful work (writes, edits, commits)
|
|
5
|
-
* versus read-only exploration (reads, greps, globs).
|
|
6
|
-
*/
|
|
7
|
-
import { TOOL_NAMES } from '../tool-names.js';
|
|
8
|
-
/** Tool names that represent significant (mutating) work */
|
|
9
|
-
const SIGNIFICANT_TOOLS = new Set([
|
|
10
|
-
TOOL_NAMES.WRITE_FILE,
|
|
11
|
-
TOOL_NAMES.EDIT,
|
|
12
|
-
TOOL_NAMES.GIT_COMMIT,
|
|
13
|
-
TOOL_NAMES.GIT_STASH,
|
|
14
|
-
TOOL_NAMES.RUN_TESTS,
|
|
15
|
-
TOOL_NAMES.RUN_LINT,
|
|
16
|
-
TOOL_NAMES.RUN_BUILD,
|
|
17
|
-
]);
|
|
18
|
-
/**
|
|
19
|
-
* Check if a tool call represents significant work worth recording.
|
|
20
|
-
* Returns false for failed tool calls (fail-closed: don't record broken work).
|
|
21
|
-
*/
|
|
22
|
-
export function isSignificantWork(toolName, _input, success) {
|
|
23
|
-
if (!success)
|
|
24
|
-
return false;
|
|
25
|
-
return SIGNIFICANT_TOOLS.has(toolName);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Extract affected file paths from a tool call's input.
|
|
29
|
-
* Returns an empty array if no files can be extracted.
|
|
30
|
-
*/
|
|
31
|
-
export function extractAffectedFiles(toolName, input) {
|
|
32
|
-
if (toolName === TOOL_NAMES.WRITE_FILE && typeof input.path === 'string') {
|
|
33
|
-
return [input.path];
|
|
34
|
-
}
|
|
35
|
-
if (toolName === TOOL_NAMES.EDIT && typeof input.filePath === 'string') {
|
|
36
|
-
return [input.filePath];
|
|
37
|
-
}
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Extract lines changed from a tool result, best-effort.
|
|
42
|
-
* Returns 0 if the information is not available.
|
|
43
|
-
*/
|
|
44
|
-
export function extractLinesChanged(result) {
|
|
45
|
-
// Some tools report linesWritten directly
|
|
46
|
-
if (typeof result.linesWritten === 'number') {
|
|
47
|
-
return result.linesWritten;
|
|
48
|
-
}
|
|
49
|
-
// Edit results may have linesAdded + linesRemoved
|
|
50
|
-
const added = typeof result.linesAdded === 'number' ? result.linesAdded : 0;
|
|
51
|
-
const removed = typeof result.linesRemoved === 'number' ? result.linesRemoved : 0;
|
|
52
|
-
if (added > 0 || removed > 0) {
|
|
53
|
-
return added + removed;
|
|
54
|
-
}
|
|
55
|
-
return 0;
|
|
56
|
-
}
|