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
37 changes: 31 additions & 6 deletions packages/daemon/src/lib/space/runtime/space-runtime-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,15 @@ export class SpaceRuntimeService {
},
},
});
this.attachLongTermAgentMcpServers(session, space, agent.displayName, sessionId, null, [
`@${agent.handle}`,
]);
this.attachLongTermAgentMcpServers(
session,
space,
agent.displayName,
sessionId,
null,
[`@${agent.handle}`],
agent.id
);
return session;
}

Expand Down Expand Up @@ -706,7 +712,15 @@ export class SpaceRuntimeService {
await session.resetQuery({ restartQuery: true });
}
if (created || this.missingLongTermAgentMcpServers(session)) {
this.attachLongTermAgentMcpServers(session, space, agent.name, sessionId, agent);
this.attachLongTermAgentMcpServers(
session,
space,
agent.name,
sessionId,
agent,
undefined,
agent.id
);
}
return session;
}
Expand Down Expand Up @@ -740,7 +754,15 @@ export class SpaceRuntimeService {
}
const agentName =
session.metadata.promptProvenance?.agentName ?? persistedAgent?.name ?? 'Space Agent';
this.attachLongTermAgentMcpServers(agentSession, space, agentName, session.id, persistedAgent);
this.attachLongTermAgentMcpServers(
agentSession,
space,
agentName,
session.id,
persistedAgent,
undefined,
agentId
);
agentSession.onMissingMemberSpaceMcpServers = async (_sessionId, missing) => {
log.warn(
`Long-term Space agent session ${session.id} missing MCP servers [${missing.join(', ')}]; re-attaching space-agent-tools before query start`
Expand All @@ -762,7 +784,8 @@ export class SpaceRuntimeService {
agentName: string,
sessionId: string,
agent: SpaceWorkerAgent | null,
agentHandleAliases?: string[]
agentHandleAliases?: string[],
ownerAgentId?: string
): void {
const mcpServers: Record<string, McpServerConfig> = {
'space-agent-tools': this.buildLongTermAgentMcpServer(
Expand All @@ -778,6 +801,8 @@ export class SpaceRuntimeService {
spaceId: space.id,
memoryRepo: this.config.memoryRepo,
mySessionId: sessionId,
// Scope this agent's memory writes/reads to its own private namespace.
ownerAgentId,
}) as unknown as McpServerConfig;
}
if (this.config.dbPath) {
Expand Down
66 changes: 58 additions & 8 deletions packages/daemon/src/lib/space/tools/agent-memory-tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
import type { AgentMemoryRepository } from '../../../storage/repositories/agent-memory-repository';
import type {
AgentMemoryRepository,
AgentMemoryScope,
AgentMemoryScopeFilter,
} from '../../../storage/repositories/agent-memory-repository';
import type { ToolResult } from './tool-result';
import { jsonResult } from './tool-result';

Expand All @@ -11,29 +15,61 @@ const MemoryWriteSchema = z.object({
.min(1)
.describe('Fact, convention, decision, or project knowledge to persist.'),
tags: z.array(z.string()).optional().describe('Keyword tags that improve retrieval.'),
scope: z
.enum(['agent', 'space'])
.optional()
.describe(
"Where to store this memory. 'agent' writes to this agent's private namespace (default for long-horizon agents); 'space' writes to the shared Space pool visible to everyone."
),
});

const MemorySearchSchema = z.object({
export const MemorySearchSchema = z.object({
query: z.string().min(1).describe('Natural language query or code identifier.'),
limit: z.number().int().min(1).max(20).default(10),
scope: z
.enum(['mine', 'space'])
.optional()
.describe(
"Which memories to search. Omit for this agent's private memories plus the shared Space pool. 'mine' = only this agent's private memories, 'space' = only the shared pool."
),
});

const MemoryReadSchema = z.object({
export const MemoryReadSchema = z.object({
key: z.string().min(1).max(200).describe('Memory key to read.'),
scope: z
.enum(['mine', 'space'])
.optional()
.describe(
"Which namespace to read from. Omit to prefer this agent's private memory, falling back to the shared pool. 'mine' = only this agent's, 'space' = only the shared pool."
),
});

const MemoryDeleteSchema = z.object({
export const MemoryDeleteSchema = z.object({
key: z.string().min(1).max(200).describe('Memory key to delete.'),
scope: z
.enum(['mine', 'space'])
.optional()
.describe(
"Which namespace to delete from. Omit to delete this agent's private memory and the shared entry under this key (never another agent's). 'space' = only the shared entry."
),
});

export interface AgentMemoryToolsConfig {
spaceId: string;
memoryRepo: AgentMemoryRepository;
mySessionId?: string;
/**
* The agent id that owns memories written/read through this tool instance.
* Resolved from the calling session's `promptProvenance.agentId` at attach
* time. When set, writes default to the agent's private namespace and
* searches default to "mine + space". Omit for shared-only access (member /
* space_chat / workflow-node sessions with no durable agent identity).
*/
ownerAgentId?: string;
}

export function createAgentMemoryToolHandlers(config: AgentMemoryToolsConfig) {
const { spaceId, memoryRepo, mySessionId } = config;
const { spaceId, memoryRepo, mySessionId, ownerAgentId } = config;
return {
async 'memory.write'(args: z.infer<typeof MemoryWriteSchema>): Promise<ToolResult> {
const memory = memoryRepo.write({
Expand All @@ -45,23 +81,37 @@ export function createAgentMemoryToolHandlers(config: AgentMemoryToolsConfig) {
// updates instead of clearing them with an explicit empty array.
tags: args.tags,
createdBySession: mySessionId ?? null,
ownerAgentId,
scope: args.scope as AgentMemoryScope | undefined,
});
return jsonResult({ success: true, memory });
},

async 'memory.search'(args: z.infer<typeof MemorySearchSchema>): Promise<ToolResult> {
const results = await memoryRepo.search(spaceId, args.query, args.limit);
const results = await memoryRepo.search(spaceId, args.query, args.limit, {
ownerAgentId,
scope: args.scope as AgentMemoryScopeFilter | undefined,
});
return jsonResult({ success: true, results });
},

async 'memory.read'(args: z.infer<typeof MemoryReadSchema>): Promise<ToolResult> {
const memory = memoryRepo.read(spaceId, args.key);
const memory = memoryRepo.read(spaceId, args.key, {
ownerAgentId,
scope: args.scope as AgentMemoryScopeFilter | undefined,
});
if (!memory) return jsonResult({ success: false, error: `Memory not found: ${args.key}` });
return jsonResult({ success: true, memory });
},

async 'memory.delete'(args: z.infer<typeof MemoryDeleteSchema>): Promise<ToolResult> {
return jsonResult({ success: true, deleted: memoryRepo.delete(spaceId, args.key) });
return jsonResult({
success: true,
deleted: memoryRepo.delete(spaceId, args.key, {
ownerAgentId,
scope: args.scope as AgentMemoryScopeFilter | undefined,
}),
});
},
};
}
Expand Down
Loading
Loading