Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
32af25f
feat: subagents
uinstinct Dec 11, 2025
a78be7c
support streaming into tool call output
uinstinct Dec 11, 2025
675378f
rename `subagent_type` to `subagent_name`
uinstinct Dec 11, 2025
1a06d69
refactor subagent namings
uinstinct Dec 11, 2025
c2af5a9
allow all tools inside subagents
uinstinct Dec 11, 2025
1c448c4
fix: resolve circular dependency in subagent tool
continue[bot] Dec 15, 2025
87fd9b6
fix: resolve circular dependency in subagent executor
continue[bot] Dec 15, 2025
6a5f80b
fix: break circular dependency by extracting tool names
continue[bot] Dec 15, 2025
26bef77
fix: correct built-in tool names list
continue[bot] Dec 15, 2025
2003128
fix: correct import order in ToolPermissionService
continue[bot] Dec 15, 2025
bcdf012
Merge branch 'main' into sub-agents
uinstinct Dec 18, 2025
85eb249
wip: use the first found apply model for subagent
uinstinct Dec 18, 2025
0cc7a16
get subagents from config
uinstinct Dec 19, 2025
53becf1
remove debug statements
uinstinct Dec 19, 2025
70d5be8
rename builtInAgents to get-agents
uinstinct Dec 19, 2025
08465a6
add `subagent` in model roles
uinstinct Dec 19, 2025
b427287
Merge branch 'main' into sub-agents
uinstinct Jan 6, 2026
5abe556
pass modelstate to read subagents
uinstinct Jan 7, 2026
27ac8cc
include model roles subagent
uinstinct Jan 7, 2026
8381812
allow escaping of subagent tool using event emit
uinstinct Jan 7, 2026
d510de9
remove builtintoolnames
uinstinct Jan 7, 2026
8e02122
cleanup listener after execution
uinstinct Jan 7, 2026
c366fa8
fix lint
uinstinct Jan 7, 2026
3bdac8d
move subagent behind beta
uinstinct Jan 7, 2026
a48bb94
fix more lints
uinstinct Jan 7, 2026
6182a3f
restore toolpermissionservice changes
uinstinct Jan 8, 2026
fc5e22f
fix cyclical import
uinstinct Jan 8, 2026
25b08e0
fix gitaiintegration test
uinstinct Jan 8, 2026
66fe98f
try to fix import issues for tests
uinstinct Jan 8, 2026
eb77a58
use subagent tool meta instead of calling the func
uinstinct Jan 8, 2026
3fbb428
add subagent tests
uinstinct Jan 8, 2026
2b347b6
use dynamic to prevent cyclical dependency
uinstinct Jan 8, 2026
c2d928d
fix subagent tests
uinstinct Jan 8, 2026
d55a138
decouple allbuiltins and subagent tool meta
uinstinct Jan 9, 2026
72da793
skip dynamic imports
uinstinct Jan 9, 2026
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
6 changes: 6 additions & 0 deletions core/config/yaml/loadYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export async function configYamlToContinueConfig(options: {
autocomplete: [],
rerank: [],
summarize: [],
subagent: [],
},
selectedModelByRole: {
chat: null,
Expand All @@ -196,6 +197,7 @@ export async function configYamlToContinueConfig(options: {
autocomplete: null,
rerank: null,
summarize: null,
subagent: null,
},
rules: [],
requestOptions: { ...config.requestOptions },
Expand Down Expand Up @@ -333,6 +335,10 @@ export async function configYamlToContinueConfig(options: {
if (model.roles?.includes("rerank")) {
continueConfig.modelsByRole.rerank.push(...llms);
}

if (model.roles?.includes("subagent")) {
continueConfig.modelsByRole.subagent.push(...llms);
}
} catch (e) {
localErrors.push({
fatal: false,
Expand Down
21 changes: 21 additions & 0 deletions extensions/cli/src/services/ModelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,25 @@ export class ModelService
return nameMatches;
});
}

static getSubagentModels(modelState: ModelServiceState) {
if (!modelState.assistant) {
return [];
}
const subagentModels = modelState.assistant.models
?.filter((model) => !!model)
.filter((model) => !!model.name) // filter out models without a name
.filter((model) => model.roles?.includes("subagent")) // filter with role subagent
.filter((model) => !!model.chatOptions?.baseSystemMessage); // filter those with a system message

if (!subagentModels) {
return [];
}
return subagentModels?.map((model) => ({
llmApi: createLlmApi(model, modelState.authConfig),
model,
assistant: modelState.assistant,
authConfig: modelState.authConfig,
}));
}
}
5 changes: 2 additions & 3 deletions extensions/cli/src/services/ToolPermissionService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ALL_BUILT_IN_TOOLS } from "src/tools/allBuiltIns.js";

import { ensurePermissionsYamlExists } from "../permissions/permissionsYamlLoader.js";
import { resolvePermissionPrecedence } from "../permissions/precedenceResolver.js";
import {
PermissionMode,
ToolPermissionPolicy,
ToolPermissions,
} from "../permissions/types.js";
import { BUILT_IN_TOOL_NAMES } from "../tools/builtInToolNames.js";
import { logger } from "../util/logger.js";

import { BaseService, ServiceWithDependencies } from "./BaseService.js";
Expand Down Expand Up @@ -143,7 +142,7 @@ export class ToolPermissionService
}));
policies.push(...allowed);
const specificBuiltInSet = new Set(specificBuiltIns);
const notMentioned = ALL_BUILT_IN_TOOLS.map((t) => t.name).filter(
const notMentioned = BUILT_IN_TOOL_NAMES.filter(
(name) => !specificBuiltInSet.has(name),
);
const disallowed: ToolPermissionPolicy[] = notMentioned.map((tool) => ({
Expand Down
2 changes: 0 additions & 2 deletions extensions/cli/src/stream/streamChatResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,6 @@ export async function streamChatResponse(
chatHistory = refreshChatHistoryFromService(chatHistory, isCompacting);
logger.debug("Starting conversation iteration");

logger.debug("debug1 streamChatResponse history", { chatHistory });

// Get system message once per iteration (can change based on tool permissions mode)
const systemMessage = await services.systemMessage.getSystemMessage(
services.toolPermissions.getState().currentMode,
Expand Down
209 changes: 209 additions & 0 deletions extensions/cli/src/subagent/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import type { ChatHistoryItem } from "core";

import {
ModelServiceState,
SERVICE_NAMES,
serviceContainer,
services,
} from "../services/index.js";
import { ToolPermissionServiceState } from "../services/ToolPermissionService.js";
import { streamChatResponse } from "../stream/streamChatResponse.js";
import { escapeEvents } from "../util/cli.js";
import { logger } from "../util/logger.js";

/**
* Options for executing a subagent
*/
export interface SubAgentExecutionOptions {
agent: ModelServiceState;
prompt: string;
parentSessionId: string;
abortController: AbortController;
onOutputUpdate?: (output: string) => void;
}

/**
* Result from executing a subagent
*/
export interface SubAgentResult {
success: boolean;
response: string;
error?: string;
}

/**
* Build system message for the agent
*/
async function buildAgentSystemMessage(
agent: ModelServiceState,
services: any,
): Promise<string> {
const baseMessage = services.systemMessage
? await services.systemMessage.getSystemMessage(
services.toolPermissions.getState().currentMode,
)
: "";

const agentPrompt = agent.model?.chatOptions?.baseSystemMessage || "";

// Combine base system message with agent-specific prompt
if (agentPrompt) {
return `${baseMessage}\n\n${agentPrompt}`;
}

return baseMessage;
}

/**
* Execute a subagent in a child session
*/
export async function executeSubAgent(
options: SubAgentExecutionOptions,
): Promise<SubAgentResult> {
const mainAgentPermissionsState =
await serviceContainer.get<ToolPermissionServiceState>(
SERVICE_NAMES.TOOL_PERMISSIONS,
);

const { agent: subAgent, prompt, abortController, onOutputUpdate } = options;

try {
logger.debug("Starting subagent execution", {
agent: subAgent.model?.name,
});

const { model, llmApi } = subAgent;
if (!model || !llmApi) {
throw new Error("Model or LLM API not available");
}

// allow all tools for now
// todo: eventually we want to show the same prompt in a dialog whether asking whether that tool call is allowed or not

serviceContainer.set<ToolPermissionServiceState>(
SERVICE_NAMES.TOOL_PERMISSIONS,
{
...mainAgentPermissionsState,
permissions: {
policies: [{ tool: "*", permission: "allow" }],
},
},
);

// Build agent system message
const systemMessage = await buildAgentSystemMessage(subAgent, services);

// Store original system message function
const originalGetSystemMessage = services.systemMessage?.getSystemMessage;

// Store original ChatHistoryService ready state
const chatHistorySvc = services.chatHistory;
const originalIsReady =
chatHistorySvc && typeof chatHistorySvc.isReady === "function"
? chatHistorySvc.isReady
: undefined;

// Override system message for this execution
if (services.systemMessage) {
services.systemMessage.getSystemMessage = async () => systemMessage;
}

// Temporarily disable ChatHistoryService to prevent it from interfering with child session
if (chatHistorySvc && originalIsReady) {
chatHistorySvc.isReady = () => false;
}

const chatHistory = [
{
message: {
role: "user",
content: prompt,
},
contextItems: [],
},
] as ChatHistoryItem[];

try {
let accumulatedOutput = "";

escapeEvents.on("user-escape", () => {
abortController.abort();
chatHistory.push({
message: {
role: "user",
content: "Subagent execution was cancelled by the user.",
},
contextItems: [],
});
});

// Execute the chat stream with child session
await streamChatResponse(
chatHistory,
model,
llmApi,
abortController,
{
onContent: (content: string) => {
accumulatedOutput += content;
if (onOutputUpdate) {
onOutputUpdate(accumulatedOutput);
}
},
onToolResult: (result: string) => {
// todo: skip tool outputs - show tool names and params
accumulatedOutput += `\n\n${result}`;
if (onOutputUpdate) {
onOutputUpdate(accumulatedOutput);
}
},
},
false, // Not compacting
);

// The last message (mostly) contains the important output to be submitted back to the main agent
const lastMessage = chatHistory.at(-1);
const response =
typeof lastMessage?.message?.content === "string"
? lastMessage.message.content
: "";

logger.debug("Subagent execution completed", {
agent: model?.name,
responseLength: response.length,
});

return {
success: true,
response,
};
} finally {
// Restore original system message function
if (services.systemMessage && originalGetSystemMessage) {
services.systemMessage.getSystemMessage = originalGetSystemMessage;
}

// Restore original ChatHistoryService ready state
if (chatHistorySvc && originalIsReady) {
chatHistorySvc.isReady = originalIsReady;
}

// Restore original main agent tool permissions
serviceContainer.set<ToolPermissionServiceState>(
SERVICE_NAMES.TOOL_PERMISSIONS,
mainAgentPermissionsState,
);
}
} catch (error: any) {
logger.error("Subagent execution failed", {
agent: subAgent.model?.name,
error: error.message,
});

return {
success: false,
response: "",
error: error.message,
};
}
}
40 changes: 40 additions & 0 deletions extensions/cli/src/subagent/get-agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ModelServiceState } from "../services/index.js";
import { ModelService } from "../services/ModelService.js";

/**
* Get an agent by name
*/
export function getSubagent(modelState: ModelServiceState, name: string) {
return (
ModelService.getSubagentModels(modelState).find(
(model) => model.model.name === name,
) ?? null
);
}

/**
* Generate dynamic tool description listing available agents
*/
export function generateSubagentToolDescription(
modelState: ModelServiceState,
): string {
const agentList = ModelService.getSubagentModels(modelState)
.map(
(subagentModel) =>
` - ${subagentModel.model.name}: ${subagentModel.model.chatOptions?.baseSystemMessage}`,
)
.join("\n");

// todo: refine this prompt later
return `Launch a specialized subagent to handle a specific task.

Here are the available subagents:
${agentList}
`;
}

export function getAgentNames(modelState: ModelServiceState): string[] {
return ModelService.getSubagentModels(modelState).map(
(model) => model.model.name,
);
}
2 changes: 2 additions & 0 deletions extensions/cli/src/tools/allBuiltIns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { readFileTool } from "./readFile.js";
import { reportFailureTool } from "./reportFailure.js";
import { runTerminalCommandTool } from "./runTerminalCommand.js";
import { searchCodeTool } from "./searchCode.js";
import { subagentTool } from "./subagent.js";
import { uploadArtifactTool } from "./uploadArtifact.js";
import { writeChecklistTool } from "./writeChecklist.js";
import { writeFileTool } from "./writeFile.js";
Expand All @@ -22,6 +23,7 @@ export const ALL_BUILT_IN_TOOLS = [
runTerminalCommandTool,
fetchTool,
writeChecklistTool,
subagentTool,
exitTool,
reportFailureTool,
uploadArtifactTool,
Expand Down
21 changes: 21 additions & 0 deletions extensions/cli/src/tools/builtInToolNames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* List of built-in tool names
* Kept separate from allBuiltIns.ts to avoid circular dependencies
*/
export const BUILT_IN_TOOL_NAMES = [
"Read",
"Edit",
"MultiEdit",
"Write",
"List",
"Search",
"Bash",
"Fetch",
"Checklist",
"Subagent",
"Exit",
"ReportFailure",
"UploadArtifact",
] as const;

export type BuiltInToolName = (typeof BUILT_IN_TOOL_NAMES)[number];
Loading
Loading