subagents (written by an agent, for an agent)
so meta.
This commit is contained in:
parent
09445cb565
commit
40cab7ca49
@ -8,8 +8,6 @@ You are spawned as a child process by the Master Control Console (MCC) in Gadget
|
||||
|
||||
You will generally be tasked with exploring a software repository to learn about the software architecture, how things are imlemented, the structure of a class and it's methods, the order of operations as they are performed by the software, or even just read and summarize documentation looking for a specific piece of knowledge for the MCC.
|
||||
|
||||
{{tool_aggression}}
|
||||
|
||||
This is your job.
|
||||
|
||||
### CHAT SESSION
|
||||
@ -8,8 +8,6 @@ You are spawned as a child process by the Master Control Console (MCC) in Gadget
|
||||
|
||||
You will generally be tasked with exploring a software repository to learn about the software architecture, how things are imlemented, the structure of a class and it's methods, or even just read and summarize documentation looking for a specific piece of knowledge for the MCC.
|
||||
|
||||
{{tool_aggression}}
|
||||
|
||||
This is your job.
|
||||
|
||||
### CHAT SESSION
|
||||
@ -2,23 +2,30 @@
|
||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import env from "../config/env.ts";
|
||||
|
||||
import { Socket } from "socket.io-client";
|
||||
import {
|
||||
IAiChatOptions,
|
||||
IAiChatResponse,
|
||||
IAiResponseStreamFn,
|
||||
type IAiChatOptions,
|
||||
type IAiChatResponse,
|
||||
type IAiResponseStreamFn,
|
||||
type IAiTool,
|
||||
type IContextChatMessage,
|
||||
} from "@gadget/ai";
|
||||
import {
|
||||
IChatSession,
|
||||
IChatTurn,
|
||||
IUser,
|
||||
ServerToClientEvents,
|
||||
ClientToServerEvents,
|
||||
type IAiProvider,
|
||||
type IChatSession,
|
||||
type IChatTurn,
|
||||
type IChatSubagentProcess,
|
||||
type IChatToolCall,
|
||||
type IUser,
|
||||
type ServerToClientEvents,
|
||||
type ClientToServerEvents,
|
||||
ChatSessionMode,
|
||||
IProject,
|
||||
type IProject,
|
||||
} from "@gadget/api";
|
||||
|
||||
import AiService from "./ai.ts";
|
||||
@ -40,6 +47,7 @@ import {
|
||||
PlanFileWriteTool,
|
||||
PlanListTool,
|
||||
ShellExecTool,
|
||||
SubagentTool,
|
||||
type DroneToolboxEnvironment,
|
||||
} from "../tools/index.ts";
|
||||
|
||||
@ -65,6 +73,8 @@ const toolboxEnv: DroneToolboxEnvironment = {
|
||||
|
||||
class AgentService extends GadgetService {
|
||||
private toolbox = new AiToolbox(toolboxEnv);
|
||||
private currentWorkOrder: IAgentWorkOrder | null = null;
|
||||
private currentSocket: DroneSocket | null = null;
|
||||
|
||||
get name(): string {
|
||||
return "AgentService";
|
||||
@ -109,10 +119,17 @@ class AgentService extends GadgetService {
|
||||
this.toolbox.register(new PlanFileEditTool(this.toolbox), [ChatSessionMode.Plan]);
|
||||
this.toolbox.register(new PlanListTool(this.toolbox), [ChatSessionMode.Plan]);
|
||||
|
||||
// Chat tools — subagent spawning: available in all modes
|
||||
const subagentTool = new SubagentTool(this.toolbox);
|
||||
subagentTool.setSpawner((agentType, prompt) => this.spawnSubagent(agentType, prompt));
|
||||
this.toolbox.register(subagentTool, readOnlyModes);
|
||||
|
||||
this.log.info("started");
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.currentWorkOrder = null;
|
||||
this.currentSocket = null;
|
||||
this.log.info("stopped");
|
||||
}
|
||||
|
||||
@ -120,6 +137,9 @@ class AgentService extends GadgetService {
|
||||
workOrder: IAgentWorkOrder,
|
||||
socket: DroneSocket,
|
||||
): Promise<void> {
|
||||
this.currentWorkOrder = workOrder;
|
||||
this.currentSocket = socket;
|
||||
|
||||
const { turn } = workOrder;
|
||||
let toolCallCount = 0;
|
||||
let inputTokens = 0;
|
||||
@ -433,6 +453,259 @@ class AgentService extends GadgetService {
|
||||
cacheDir,
|
||||
});
|
||||
}
|
||||
|
||||
async spawnSubagent(
|
||||
agentType: string,
|
||||
prompt: string,
|
||||
): Promise<string> {
|
||||
const workOrder = this.currentWorkOrder;
|
||||
if (!workOrder) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "OPERATION_FAILED",
|
||||
message: "No active work order. Subagent cannot be spawned outside of a work order.",
|
||||
});
|
||||
}
|
||||
|
||||
const turn = workOrder.turn;
|
||||
const session = turn.session as IChatSession;
|
||||
|
||||
if (!session.user) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "OPERATION_FAILED",
|
||||
message: "ChatSession must be populated with user data.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = session.user as IUser;
|
||||
const provider = turn.provider as IAiProvider;
|
||||
|
||||
if (!provider || typeof provider === "string") {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "OPERATION_FAILED",
|
||||
message: "Turn provider must be populated.",
|
||||
});
|
||||
}
|
||||
|
||||
// Build the subagent loop
|
||||
const startTime = Date.now();
|
||||
const systemPrompt = await this.buildSubagentSystemPrompt(agentType);
|
||||
const messages: IContextChatMessage[] = [
|
||||
{ createdAt: new Date(), role: "system", content: systemPrompt },
|
||||
{ createdAt: new Date(), role: "user", content: prompt },
|
||||
];
|
||||
|
||||
const tools = this.getToolsForSubagent(agentType);
|
||||
const streamHandler = this.currentSocket
|
||||
? this.makeStreamHandler(this.currentSocket)
|
||||
: undefined;
|
||||
|
||||
let fullResponse = "";
|
||||
let fullThinking = "";
|
||||
const subagentToolCalls: IChatToolCall[] = [];
|
||||
let toolCallCount = 0;
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let thinkingTokenCount = 0;
|
||||
|
||||
try {
|
||||
let continueLoop = true;
|
||||
while (continueLoop) {
|
||||
continueLoop = false;
|
||||
|
||||
const chatOptions: IAiChatOptions = {
|
||||
context: messages,
|
||||
tools,
|
||||
};
|
||||
|
||||
this.log.info("subagent loop iteration", {
|
||||
agentType,
|
||||
messagesCount: messages.length,
|
||||
toolsAvailable: tools.map((t) => t.name),
|
||||
});
|
||||
|
||||
const response = await AiService.chat(
|
||||
provider,
|
||||
{
|
||||
modelId: turn.llm,
|
||||
params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40 },
|
||||
},
|
||||
chatOptions,
|
||||
streamHandler,
|
||||
);
|
||||
|
||||
if (response.doneReason === "load" && !response.response && !response.thinking) {
|
||||
throw new Error("Subagent model failed to respond (still loading or error)");
|
||||
}
|
||||
|
||||
inputTokens += response.stats?.tokenCounts?.input ?? 0;
|
||||
if (response.thinking) {
|
||||
thinkingTokenCount += Math.ceil(response.thinking.length / 4);
|
||||
}
|
||||
if (response.response) {
|
||||
fullResponse += response.response;
|
||||
outputTokens += Math.ceil(response.response.length / 4);
|
||||
}
|
||||
if (response.thinking) {
|
||||
fullThinking = (fullThinking + response.thinking).trim();
|
||||
}
|
||||
|
||||
// Process tool calls
|
||||
if (response.toolCalls && response.toolCalls.length > 0) {
|
||||
continueLoop = true;
|
||||
toolCallCount += response.toolCalls.length;
|
||||
|
||||
messages.push({
|
||||
createdAt: new Date(),
|
||||
role: "assistant",
|
||||
content: response.response,
|
||||
});
|
||||
|
||||
for (const toolCall of response.toolCalls) {
|
||||
const toolArgsRaw = toolCall.function.arguments;
|
||||
const result = await this.executeTool(
|
||||
toolCall.function.name,
|
||||
toolArgsRaw,
|
||||
);
|
||||
|
||||
subagentToolCalls.push({
|
||||
callId: toolCall.callId,
|
||||
name: toolCall.function.name,
|
||||
parameters: toolArgsRaw,
|
||||
response: result,
|
||||
});
|
||||
|
||||
messages.push({
|
||||
createdAt: new Date(),
|
||||
role: "tool",
|
||||
callId: toolCall.callId,
|
||||
toolName: toolCall.function.name,
|
||||
content: result,
|
||||
});
|
||||
|
||||
inputTokens += Math.ceil(toolArgsRaw.length / 4);
|
||||
outputTokens += Math.ceil(result.length / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have a response if the subagent only made tool calls
|
||||
if (!fullResponse.trim()) {
|
||||
if (toolCallCount > 0) {
|
||||
fullResponse = `Subagent task completed. Executed ${toolCallCount} tool calls to perform the requested actions.`;
|
||||
} else {
|
||||
fullResponse = "Subagent task completed without additional actions.";
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const durationMs = endTime - startTime;
|
||||
const durationSeconds = Math.floor(durationMs / 1000);
|
||||
const minutes = Math.floor(durationSeconds / 60);
|
||||
const seconds = durationSeconds % 60;
|
||||
const durationLabel = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "00")}`;
|
||||
|
||||
const result: IChatSubagentProcess = {
|
||||
prompt,
|
||||
thinking: fullThinking || undefined,
|
||||
response: fullResponse,
|
||||
toolCalls: subagentToolCalls,
|
||||
stats: {
|
||||
toolCallCount,
|
||||
inputTokens,
|
||||
thinkingTokenCount,
|
||||
responseTokens: outputTokens,
|
||||
durationMs,
|
||||
durationLabel,
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify({ success: true, data: result });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.log.error("subagent execution failed", { agentType, error: errorMessage });
|
||||
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "SUBAGENT_FAILED",
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getToolsForSubagent(agentType: string): IAiTool[] {
|
||||
const mode = agentType === "explore"
|
||||
? ChatSessionMode.Plan
|
||||
: ChatSessionMode.Build;
|
||||
|
||||
const tools = Array.from(this.toolbox.getModeSet(mode) || []);
|
||||
|
||||
if (agentType === "explore") {
|
||||
return tools.filter((t) =>
|
||||
!t.name.startsWith("plan_") &&
|
||||
t.name !== "subagent"
|
||||
);
|
||||
}
|
||||
|
||||
return tools.filter((t) => t.name !== "subagent");
|
||||
}
|
||||
|
||||
private async buildSubagentSystemPrompt(agentType: string): Promise<string> {
|
||||
const promptDir = path.resolve(env.installDir, "data", "prompts", "subagent", agentType);
|
||||
const templatePath = path.join(promptDir, "system.md");
|
||||
|
||||
let template: string;
|
||||
try {
|
||||
template = await fs.readFile(templatePath, "utf-8");
|
||||
} catch {
|
||||
this.log.error("subagent prompt template not found", { agentType, templatePath });
|
||||
throw new Error(`Subagent prompt template not found: ${templatePath}`);
|
||||
}
|
||||
|
||||
const workOrder = this.currentWorkOrder;
|
||||
if (!workOrder) return template;
|
||||
|
||||
const session = workOrder.turn.session as IChatSession;
|
||||
const user = session?.user as IUser | undefined;
|
||||
const project = session?.project as IProject | undefined;
|
||||
const provider = workOrder.turn.provider as IAiProvider | undefined;
|
||||
|
||||
// Build session block matching the format used in gadget-code's buildSystemPrompt
|
||||
const sessionBlock = [
|
||||
`Session ID: ${session?._id ?? "unknown"}`,
|
||||
`Session Name: ${session?.name ?? "unknown"}`,
|
||||
`Session Created: ${session?.createdAt?.toISOString() ?? "unknown"}`,
|
||||
`AI Provider: ${provider?.name ?? "unknown"}`,
|
||||
`AI API: ${provider?.apiType ?? "unknown"}`,
|
||||
`Model: ${workOrder.turn.llm}`,
|
||||
`Project: ${project?.name ?? "unknown"}`,
|
||||
].join("\n");
|
||||
|
||||
const personaBlock = [
|
||||
`User ID: ${user?._id ?? "unknown"}`,
|
||||
`Name: ${user?.displayName ?? "unknown"}`,
|
||||
`Is Admin: ${user?.flags?.isAdmin ? "Yes" : "No"}`,
|
||||
].join("\n");
|
||||
|
||||
template = template.replace("{{session_block}}", sessionBlock);
|
||||
template = template.replace("{{persona_block}}", personaBlock);
|
||||
|
||||
// Append AGENTS.md if available in the project root
|
||||
try {
|
||||
const projectRoot = this.toolbox.env.workspace?.projectDir;
|
||||
if (projectRoot) {
|
||||
const agentsMdPath = path.join(projectRoot, "AGENTS.md");
|
||||
const agentsMd = await fs.readFile(agentsMdPath, "utf-8");
|
||||
template += "\n\n## AGENT CONFIGURATION\n\n" + agentsMd;
|
||||
}
|
||||
} catch {
|
||||
// AGENTS.md not found or inaccessible - that's fine
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
export { AgentService };
|
||||
|
||||
4
gadget-drone/src/tools/chat/index.ts
Normal file
4
gadget-drone/src/tools/chat/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
export { SubagentTool } from "./subagent.ts";
|
||||
108
gadget-drone/src/tools/chat/subagent.ts
Normal file
108
gadget-drone/src/tools/chat/subagent.ts
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai";
|
||||
import { formatError } from "@gadget/ai";
|
||||
import { DroneTool } from "../tool.ts";
|
||||
|
||||
const VALID_AGENT_TYPES = ["explore", "general"] as const;
|
||||
type AgentType = (typeof VALID_AGENT_TYPES)[number];
|
||||
|
||||
export class SubagentTool extends DroneTool {
|
||||
private _spawnSubagent: ((agentType: string, prompt: string) => Promise<string>) | null = null;
|
||||
|
||||
setSpawner(fn: typeof this._spawnSubagent): void {
|
||||
this._spawnSubagent = fn;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return "subagent";
|
||||
}
|
||||
|
||||
get category(): string {
|
||||
return "chat";
|
||||
}
|
||||
|
||||
get definition(): IToolDefinition {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name: this.name,
|
||||
description:
|
||||
"Spawn a subagent to perform a specific task. The subagent will execute the task and return its result. Use 'explore' agent type for research and information gathering tasks. Use 'general' agent type for general-purpose task execution.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
agent_type: {
|
||||
type: "string",
|
||||
enum: [...VALID_AGENT_TYPES],
|
||||
description:
|
||||
"The type of subagent to spawn. Use 'explore' for research and information gathering. Use 'general' for general-purpose task execution.",
|
||||
},
|
||||
prompt: {
|
||||
type: "string",
|
||||
description:
|
||||
"The task description and instructions for the subagent. Be specific about what information to find or what task to perform.",
|
||||
},
|
||||
},
|
||||
required: ["agent_type", "prompt"],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
|
||||
const { agent_type, prompt } = args;
|
||||
|
||||
if (!agent_type) {
|
||||
return formatError({
|
||||
code: "MISSING_PARAMETER",
|
||||
message: "The 'agent_type' parameter is required.",
|
||||
parameter: "agent_type",
|
||||
expected: "Either 'explore' or 'general'",
|
||||
recoveryHint: "Specify either 'explore' or 'general' for the agent_type parameter.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!VALID_AGENT_TYPES.includes(agent_type as AgentType)) {
|
||||
return formatError({
|
||||
code: "INVALID_PARAMETER",
|
||||
message: `Invalid agent_type: '${agent_type}'. Must be one of: ${VALID_AGENT_TYPES.join(", ")}`,
|
||||
parameter: "agent_type",
|
||||
expected: `One of: ${VALID_AGENT_TYPES.join(", ")}`,
|
||||
recoveryHint: "Use 'explore' for research tasks or 'general' for general tasks.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!prompt || typeof prompt !== "string") {
|
||||
return formatError({
|
||||
code: "MISSING_PARAMETER",
|
||||
message: "The 'prompt' parameter is required and must be a string.",
|
||||
parameter: "prompt",
|
||||
expected: "A non-empty string describing the task for the subagent.",
|
||||
recoveryHint: "Provide specific instructions for the subagent to execute.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._spawnSubagent) {
|
||||
return formatError({
|
||||
code: "OPERATION_FAILED",
|
||||
message: "SubagentTool has not been initialized with a spawner function.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("spawning subagent", { agentType: agent_type, promptLength: (prompt as string).length });
|
||||
|
||||
try {
|
||||
return await this._spawnSubagent(agent_type as string, prompt as string);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logger.error("subagent execution failed", { agentType: agent_type, error: message });
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: "SUBAGENT_FAILED",
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
export { AiToolbox, type DroneToolboxEnvironment } from "./toolbox.ts";
|
||||
export { DroneTool } from "./tool.ts";
|
||||
export * from "./chat/index.ts";
|
||||
export * from "./system/index.ts";
|
||||
export * from "./network/index.ts";
|
||||
export * from "./plan/index.ts";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user