Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
426 changes: 426 additions & 0 deletions docs/design-docs/cortex-topic-synthesis.md

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,83 @@ export interface UpdateTaskRequest {
approved_by?: string;
}

// -- Topic Types --

export type TopicStatus = "active" | "paused" | "archived";
export type SearchMode = "hybrid" | "vector" | "fulltext" | "typed" | "recent";
export type SearchSort = "recent" | "importance" | "most_accessed";

export interface TopicCriteria {
query?: string;
memory_types?: MemoryType[];
min_importance?: number;
channel_ids?: string[];
max_age?: string;
mode?: SearchMode;
sort_by?: SearchSort;
max_memories?: number;
}
Comment on lines +954 to +963

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove stale channel_ids from TopicCriteria (API contract drift).

At Line 958, channel_ids is still exposed even though topics are now recall-layer based and no longer channel-assigned. Keeping it in the client model risks sending unsupported fields and drifting UI behavior from backend semantics.

Proposed fix
 export interface TopicCriteria {
 	query?: string;
 	memory_types?: MemoryType[];
 	min_importance?: number;
-	channel_ids?: string[];
 	max_age?: string;
 	mode?: SearchMode;
 	sort_by?: SearchSort;
 	max_memories?: number;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/api/client.ts` around lines 954 - 963, The TopicCriteria
interface exposes a stale channel_ids property; remove channel_ids from the
TopicCriteria type declaration and any related typing imports/usages to prevent
sending unsupported fields. Update all call sites that construct TopicCriteria
(search clients, UI form serializers, and tests) to stop setting channel_ids and
adjust any validation or request payload mapping logic that filters/reads
channel_ids; also update API docs/comments and run tests to ensure no remaining
references to TopicCriteria.channel_ids remain.


export interface TopicItem {
id: string;
agent_id: string;
title: string;
content: string;
criteria: TopicCriteria;
pin_ids: string[];
channel_ids: string[];
status: TopicStatus;
max_words: number;
last_memory_at: string | null;
last_synced_at: string | null;
created_at: string;
updated_at: string;
}

export interface TopicVersion {
id: string;
topic_id: string;
content: string;
memory_count: number;
created_at: string;
}

export interface TopicListResponse {
topics: TopicItem[];
}

export interface TopicResponse {
topic: TopicItem;
}

export interface TopicActionResponse {
success: boolean;
message: string;
}

export interface TopicVersionsResponse {
versions: TopicVersion[];
}

export interface CreateTopicRequest {
title: string;
criteria?: TopicCriteria;
channel_ids?: string[];
pin_ids?: string[];
status?: TopicStatus;
max_words?: number;
}

export interface UpdateTopicRequest {
title?: string;
criteria?: TopicCriteria;
channel_ids?: string[];
pin_ids?: string[];
status?: TopicStatus;
max_words?: number;
content?: string;
}

// -- Messaging / Bindings Types --

export interface PlatformStatus {
Expand Down Expand Up @@ -1999,6 +2076,54 @@ export const api = {
return response.json() as Promise<TaskResponse>;
},

// Topics API
listTopics: (agentId: string, status?: TopicStatus) => {
const search = new URLSearchParams({ agent_id: agentId });
if (status) search.set("status", status);
return fetchJson<TopicListResponse>(`/agents/topics?${search}`);
},
getTopic: (agentId: string, topicId: string) =>
fetchJson<TopicResponse>(`/agents/topics/${encodeURIComponent(topicId)}?agent_id=${encodeURIComponent(agentId)}`),
createTopic: async (agentId: string, request: CreateTopicRequest): Promise<TopicResponse> => {
const response = await fetch(`${API_BASE}/agents/topics`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...request, agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<TopicResponse>;
},
updateTopic: async (agentId: string, topicId: string, request: UpdateTopicRequest): Promise<TopicResponse> => {
const response = await fetch(`${API_BASE}/agents/topics/${encodeURIComponent(topicId)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...request, agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<TopicResponse>;
},
deleteTopic: async (agentId: string, topicId: string): Promise<TopicActionResponse> => {
const response = await fetch(
`${API_BASE}/agents/topics/${encodeURIComponent(topicId)}?agent_id=${encodeURIComponent(agentId)}`,
{ method: "DELETE" },
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<TopicActionResponse>;
},
syncTopic: async (agentId: string, topicId: string): Promise<TopicResponse> => {
const response = await fetch(`${API_BASE}/agents/topics/${encodeURIComponent(topicId)}/sync`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<TopicResponse>;
},
topicVersions: (agentId: string, topicId: string, limit = 20) =>
fetchJson<TopicVersionsResponse>(
`/agents/topics/${encodeURIComponent(topicId)}/versions?agent_id=${encodeURIComponent(agentId)}&limit=${limit}`,
),

// Secrets API
secretsStatus: () => fetchJson<SecretStoreStatus>("/secrets/status"),
listSecrets: () => fetchJson<SecretListResponse>("/secrets"),
Expand Down
1 change: 1 addition & 0 deletions interface/src/components/AgentTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const tabs = [
{ label: "Ingest", to: "/agents/$agentId/ingest" as const, exact: false },
{ label: "Workers", to: "/agents/$agentId/workers" as const, exact: false },
{ label: "Tasks", to: "/agents/$agentId/tasks" as const, exact: false },
{ label: "Topics", to: "/agents/$agentId/topics" as const, exact: false },
{ label: "Cortex", to: "/agents/$agentId/cortex" as const, exact: false },
{ label: "Skills", to: "/agents/$agentId/skills" as const, exact: false },
{ label: "Cron", to: "/agents/$agentId/cron" as const, exact: false },
Expand Down
18 changes: 18 additions & 0 deletions interface/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {AgentIngest} from "@/routes/AgentIngest";
import {AgentSkills} from "@/routes/AgentSkills";
import {AgentWorkers} from "@/routes/AgentWorkers";
import {AgentTasks} from "@/routes/AgentTasks";
import {AgentTopics} from "@/routes/AgentTopics";
import {AgentChat} from "@/routes/AgentChat";
import {Settings} from "@/routes/Settings";
import {useLiveContext} from "@/hooks/useLiveContext";
Expand Down Expand Up @@ -219,6 +220,22 @@ const agentTasksRoute = createRoute({
},
});

const agentTopicsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/agents/$agentId/topics",
component: function AgentTopicsPage() {
const {agentId} = agentTopicsRoute.useParams();
return (
<div className="flex h-full flex-col">
<AgentHeader agentId={agentId} />
<div className="flex-1 overflow-hidden">
<AgentTopics agentId={agentId} />
</div>
</div>
);
},
});

const agentCronRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/agents/$agentId/cron",
Expand Down Expand Up @@ -323,6 +340,7 @@ const routeTree = rootRoute.addChildren([
agentIngestRoute,
agentWorkersRoute,
agentTasksRoute,
agentTopicsRoute,
agentCortexRoute,
agentSkillsRoute,
agentCronRoute,
Expand Down
Loading
Loading