Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
AgentTool,
ThinkingLevel,
} from "@juliusbrussee/caveman-agent";
import { checkpoints, LLMLinguaMiddleware, memory as memoryNs } from "@juliusbrussee/caveman-agent";
import { mcp as agentMcp, checkpoints, LLMLinguaMiddleware, memory as memoryNs } from "@juliusbrussee/caveman-agent";

const { CheckpointManager } = checkpoints;
type CheckpointManagerInstance = InstanceType<typeof CheckpointManager>;
Expand Down Expand Up @@ -270,6 +270,14 @@ export function promptTimingMark(label: string): void {
}
}

function mergeHooksConfig(base: HooksConfig | undefined, extra: HooksConfig): HooksConfig {
const merged: HooksConfig = { ...(base ?? {}) };
for (const [event, groups] of Object.entries(extra)) {
merged[event] = [...(merged[event] ?? []), ...(groups ?? [])];
}
return merged;
}

/** Standard thinking levels */
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];

Expand All @@ -281,6 +289,21 @@ const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = ["off", "minimal", "low", "m
// ============================================================================

export class AgentSession {
private _mcpHub?: agentMcp.McpHub;
private async mcpHub(): Promise<agentMcp.McpHub> {
if (this._mcpHub) return this._mcpHub;

const loaded = agentMcp.loadMcpConfig(this._cwd);
const hub = new agentMcp.McpHub({ settings: loaded.settings });

for (const server of loaded.servers) {
hub.addServer(server);
}

this._mcpHub = hub;
return hub;
}

readonly agent: Agent;
readonly sessionManager: SessionManager;
readonly settingsManager: SettingsManager;
Expand Down Expand Up @@ -862,7 +885,12 @@ export class AgentSession {
async memoryProvider(): Promise<MemoryProviderInstance | undefined> {
if (this._memoryProvider) return this._memoryProvider;
if (!this._memoryProviderInit) {
this._memoryProviderInit = resolveMemoryProvider({ cwd: this._cwd })
this._memoryProviderInit = resolveMemoryProvider({
cwd: this._cwd,
cavememOptions: {
hub: await this.mcpHub(),
},
})
.then((p) => {
this._memoryProvider = p;
return p;
Expand Down Expand Up @@ -3177,19 +3205,16 @@ export class AgentSession {
sessionId: () => this.sessionId,
registry: { disableAllHooks: sm.getDisableAllHooks?.() ?? false },
});
const global = sm.getGlobalHooks?.() as HooksConfig | undefined;
let global = sm.getGlobalHooks?.() as HooksConfig | undefined;
const project = sm.getProjectHooks?.() as HooksConfig | undefined;
if (global) manager.registry.setLayer("global", global);
if (project) manager.registry.setLayer("project", project);
// Auto-record cavemem hooks when CAVE_MEMORY_AUTO_RECORD=1 (opt-in, no
// settings-manager dependency yet). When set, merge cavemem hook
// recipes on top of the global layer.
// settings-manager dependency yet). Append cavemem recipes to the global
// layer instead of replacing user hooks for the same event.
if (process.env.CAVE_MEMORY_AUTO_RECORD === "1") {
manager.registry.setLayer("global", {
...((global ?? {}) as HooksConfig),
...buildDefaultCavememHooks(),
} as HooksConfig);
global = mergeHooksConfig(global, buildDefaultCavememHooks() as HooksConfig);
}
if (global) manager.registry.setLayer("global", global);
if (project) manager.registry.setLayer("project", project);
return manager;
}

Expand Down Expand Up @@ -3281,7 +3306,12 @@ export class AgentSession {
// Cached factory means cavemem (or FilesProvider fallback) is built once
// and shared with the `/memory` slash command + the recall transform.
try {
const memoryProvider = await resolveMemoryProvider({ cwd: this._cwd });
const memoryProvider = await resolveMemoryProvider({
cwd: this._cwd,
cavememOptions: {
hub: await this.mcpHub(),
},
});
this._memoryProvider = memoryProvider;
const available = await memoryProvider.isAvailable().catch(() => false);
// Always register; the tools themselves short-circuit when unavailable.
Expand Down
7 changes: 4 additions & 3 deletions packages/coding-agent/src/core/memory-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ interface CacheEntry {

const _cache = new Map<string, CacheEntry>();

function cacheKey(cwd: string, allowCavemem: boolean): string {
return `${allowCavemem ? "cm" : "fs"}::${cwd}`;
function cacheKey(cwd: string, allowCavemem: boolean, hasHub: boolean): string {
return `${allowCavemem ? "cm" : "fs"}:${hasHub ? "hub" : "nohub"}::${cwd}`;
}

/**
Expand All @@ -40,7 +40,8 @@ function cacheKey(cwd: string, allowCavemem: boolean): string {
*/
export async function resolveMemoryProvider(opts: MemoryFactoryOptions): Promise<MemoryProvider> {
const allowCavemem = opts.allowCavemem !== false;
const key = cacheKey(opts.cwd, allowCavemem);
const hasHub = Boolean(opts.cavememOptions?.hub);
const key = cacheKey(opts.cwd, allowCavemem, hasHub);
const cached = _cache.get(key);
if (cached) return cached.provider;

Expand Down
Loading