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
71 changes: 71 additions & 0 deletions src/lib/ai/runCanvasAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,58 @@ function filterReadonly(tools: ToolSet): ToolSet {
return out;
}

// ---------------------------------------------------------------------------
// Code-research tool names — stripped when `stripCodeResearchTools: true` is
// requested.
//
// Use case: the plan-mode org-context scout (`scoutOrgContext`) only wants
// canvas/initiative/research/connection context, not deep codebase research.
// Without this filter the scout will reach for `learn_concept` /
// `read_concepts_for_repo` / `repo_agent` and end up reporting on code
// architecture instead of org-level information (initiatives, notes,
// decisions, research docs).
//
// Stripping happens by base name (after the `{slug}__` prefix in multi-WS
// mode). `web_search` is also stripped because the scout should not be
// doing external research.
//
// Tools that are NOT stripped — they're org-context-shaped, not code:
// - list_features / read_feature / list_tasks / read_task / check_status
// (MCP-backed workspace meta — useful for "what's this workspace
// working on" without diving into code)
// - everything in the canvas/initiative/research/connection toolsets.
// ---------------------------------------------------------------------------

const CODE_RESEARCH_BASE_NAMES: ReadonlySet<string> = new Set([
"list_concepts",
"learn_concept",
"recent_commits",
"recent_contributions",
"repo_agent",
"search_logs",
"read_concepts_for_repo",
]);

/**
* Strip the multi-workspace `{slug}__` prefix so we can compare a tool
* name against the base-name list. For single-WS mode (no namespace)
* the input is already the base name.
*/
function baseToolName(name: string): string {
const idx = name.indexOf("__");
return idx === -1 ? name : name.slice(idx + 2);
}

function filterCodeResearch(tools: ToolSet): ToolSet {
const out: ToolSet = {};
for (const [name, def] of Object.entries(tools)) {
if (name === "web_search") continue; // shared, not namespaced
if (CODE_RESEARCH_BASE_NAMES.has(baseToolName(name))) continue;
out[name] = def;
}
return out;
}

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -179,6 +231,20 @@ export interface RunCanvasAgentOptions {
* (no live UI subscriber) should set it `true`.
*/
silentPusher?: boolean;
/**
* When `true`, strip codebase-research tools (`learn_concept`,
* `read_concepts_for_repo`, `repo_agent`, `recent_commits`,
* `recent_contributions`, `search_logs`, `list_concepts`) and the
* shared `web_search` tool, leaving only canvas/initiative/
* research/connection tools plus MCP-backed workspace meta
* (`list_features`, `read_feature`, `list_tasks`, etc.).
*
* Use for callers that want org-level context only — the plan-mode
* org-context scout sets this so the agent doesn't wander into
* code exploration when it should be summarizing canvas notes,
* initiatives, decisions, and research docs.
*/
stripCodeResearchTools?: boolean;
/** Caller-owned side effects. */
hooks?: CanvasAgentHooks;
}
Expand Down Expand Up @@ -362,6 +428,7 @@ export async function runCanvasAgent(
messages,
readonly = false,
silentPusher = false,
stripCodeResearchTools = false,
hooks,
} = opts;

Expand Down Expand Up @@ -483,6 +550,9 @@ export async function runCanvasAgent(
if (readonly) {
tools = filterReadonly(tools);
}
if (stripCodeResearchTools) {
tools = filterCodeResearch(tools);
}

// ------------------------------------------------------------------
// Assemble final message list + sanitize
Expand All @@ -501,6 +571,7 @@ export async function runCanvasAgent(
workspaces: workspaceSlugs,
orgId: orgId ?? null,
readonly,
stripCodeResearchTools,
silentPusher,
});

Expand Down
41 changes: 35 additions & 6 deletions src/services/roadmap/orgContextScout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,15 @@ async function runScout(args: {
// tool set, so this guarantee holds even if a future prompt
// change tells the agent to write something.
readonly: true,
// Strip codebase-research tools (`learn_concept`,
// `read_concepts_for_repo`, `repo_agent`, etc.) and `web_search`.
// The scout's job is org-level context (initiatives, notes,
// decisions, research docs, connection docs, features in flight
// elsewhere) — not code architecture or external research.
// Without this filter the agent reaches for code-research tools
// when 9+ workspaces are in scope and ends up summarizing code
// instead of canvas content.
stripCodeResearchTools: true,
// Programmatic caller, no live UI subscriber — suppress the
// HIGHLIGHT_NODES Pusher fan-out that the org chat surface
// uses for "the agent is researching node X" animations.
Expand All @@ -266,18 +275,38 @@ async function runScout(args: {
}

/**
* Build the scout's user message. Kept intentionally short and high-
* level — we trust the org agent's existing system prompt + canvas
* tools to figure out what's relevant. The plan-request message is
* embedded so the agent can judge relevance against it.
* Build the scout's user message.
*
* The prompt is shaped to steer the agent toward CANVAS content
* (initiatives, milestones, features pinned to workspace canvases,
* research docs, connection docs, authored notes/decisions, edges
* between them) and AWAY from code spelunking. Code-research tools
* are also filtered out at the caller (`stripCodeResearchTools: true`
* on `runCanvasAgent`), so the agent literally cannot reach for
* `learn_concept` / `repo_agent` / web search even if it wanted to —
* but the prompt reinforces intent so the agent doesn't waste turns
* looking for tools that aren't there.
*
* Output format is constrained to terse bullets with no preface so
* the plan agent that consumes this downstream gets clean, scannable
* context instead of conversational framing.
*/
function buildScoutPrompt(planRequestMessage: string): string {
return [
`A user in this organization is starting plan mode for a new feature. Their request:`,
`A user is starting plan mode for a new feature. Their request:`,
"",
planRequestMessage.trim(),
"",
`Explore the root canvas and the workspace sub-canvases to get a high-level view of the org as a whole. Surface anything relevant to their plan request — be quick. If nothing in the org context seems relevant, reply with exactly: ${NO_CONTEXT_SENTINEL}`,
`Scan the org's CANVASES (root canvas, workspace sub-canvases, initiative sub-canvases) for context that may matter to this plan. Look at: initiatives in flight, milestones, features already pinned to workspaces, research docs, connection docs, authored notes/decisions, edges showing relationships. Do NOT explore code — focus on the planning layer.`,
"",
`Be QUICK. Aim for 3-6 tool calls total.`,
"",
`Output format — STRICT:`,
`- No preamble. No "Based on..." or "Here's what I found...". Just the content.`,
`- Terse bullets, one item per line, each starting with "- ".`,
`- Each bullet: a name in **bold**, then a one-line summary of why it's relevant.`,
`- Max 8 bullets. If you have more, keep only the most relevant.`,
`- If nothing in the org context is meaningfully relevant to this plan, reply with EXACTLY this single token and nothing else: ${NO_CONTEXT_SENTINEL}`,
"",
`End your reply with [END_OF_ANSWER].`,
].join("\n");
Expand Down
Loading