feat: add /research command with sub-agent delegation
This commit is contained in:
@@ -441,6 +441,7 @@ pnpm tui:fs
|
|||||||
| `/login [provider]` | Authenticate with GitHub (OAuth device flow) |
|
| `/login [provider]` | Authenticate with GitHub (OAuth device flow) |
|
||||||
| `/reset` | Clear history |
|
| `/reset` | Clear history |
|
||||||
| `/status` | Show session info |
|
| `/status` | Show session info |
|
||||||
|
| `/research <task>` | Delegate a task to `agent_configs.research` |
|
||||||
| `/compact` | Compact conversation context |
|
| `/compact` | Compact conversation context |
|
||||||
| `/usage` | Show token usage and cost |
|
| `/usage` | Show token usage and cost |
|
||||||
| `/context` | Show estimated context-window usage |
|
| `/context` | Show estimated context-window usage |
|
||||||
@@ -475,6 +476,29 @@ For cloud Zhipu models, ensure `ZHIPUAI_API_KEY` is set or `api_key` is configur
|
|||||||
|
|
||||||
**Note:** The `/model` command works in the TUI only. WebChat sessions inherit the active tier from the daemon.
|
**Note:** The `/model` command works in the TUI only. WebChat sessions inherit the active tier from the daemon.
|
||||||
|
|
||||||
|
### Research Agent Quickstart
|
||||||
|
|
||||||
|
Add a dedicated research sub-agent and delegate to it with `/research`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
agent_configs:
|
||||||
|
research:
|
||||||
|
model_tier: complex
|
||||||
|
tool_profile: messaging
|
||||||
|
system_prompt: |
|
||||||
|
You are a research agent. Find, verify, and synthesize information.
|
||||||
|
Prefer primary sources. Include links and concrete dates.
|
||||||
|
Keep output structured and concise.
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/research compare k0s vs k3s for a 3-node homelab in 2026
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `research` agent is not configured, Flynn returns a setup hint with available agent names.
|
||||||
|
|
||||||
## Running as Service
|
## Running as Service
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+1
-2
@@ -466,8 +466,7 @@ hooks:
|
|||||||
# You are a research agent. Your job is to find, verify, and synthesize
|
# You are a research agent. Your job is to find, verify, and synthesize
|
||||||
# information from the web. Be thorough but concise. Cite sources when
|
# information from the web. Be thorough but concise. Cite sources when
|
||||||
# possible. Return structured findings — not conversational filler.
|
# possible. Return structured findings — not conversational filler.
|
||||||
# Use web.search to find sources, web.fetch to read them, and file.write
|
# Use web.search to find sources and web.fetch to read them.
|
||||||
# to save findings when asked.
|
|
||||||
#
|
#
|
||||||
# code:
|
# code:
|
||||||
# model_tier: complex
|
# model_tier: complex
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
"updated_at": "2026-02-17",
|
"updated_at": "2026-02-17",
|
||||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"research-agent-command-quickstart": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Implemented a first-class `/research` chat command that delegates to `agent_configs.research` through the command fast-path service layer, with clear fallback messages when the research agent is missing. Added README quickstart guidance and aligned default config examples for a safe research profile.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/commands/types.ts",
|
||||||
|
"src/commands/builtin/index.ts",
|
||||||
|
"src/commands/builtin/index.test.ts",
|
||||||
|
"src/daemon/routing.ts",
|
||||||
|
"README.md",
|
||||||
|
"config/default.yaml",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/commands/builtin/index.test.ts passing"
|
||||||
|
},
|
||||||
"verbose-only-tool-inventory-output": {
|
"verbose-only-tool-inventory-output": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-17",
|
"date": "2026-02-17",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { createContextCommand, createElevateCommand, createModelCommand, createQueueCommand } from './index.js';
|
import { createContextCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand } from './index.js';
|
||||||
|
|
||||||
describe('builtin /model command', () => {
|
describe('builtin /model command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
@@ -36,6 +36,36 @@ describe('builtin /model command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('builtin /research command', () => {
|
||||||
|
it('delegates to the research agent with the full task string', async () => {
|
||||||
|
const cmd = createResearchCommand();
|
||||||
|
const delegateAgent = vi.fn(() => 'research output');
|
||||||
|
|
||||||
|
const result = await cmd.execute(['compare', 'k0s', 'vs', 'k3s'], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/research compare k0s vs k3s',
|
||||||
|
services: { delegateAgent },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(delegateAgent).toHaveBeenCalledWith('research', 'compare k0s vs k3s');
|
||||||
|
expect(result).toEqual({ handled: true, text: 'research output' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns usage when no task is provided', async () => {
|
||||||
|
const cmd = createResearchCommand();
|
||||||
|
const result = await cmd.execute([], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/research',
|
||||||
|
services: {},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Usage: /research <question or task>' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('builtin /elevate command', () => {
|
describe('builtin /elevate command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
const cmd = createElevateCommand();
|
const cmd = createElevateCommand();
|
||||||
|
|||||||
@@ -195,11 +195,35 @@ export function createQueueCommand(): CommandDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createResearchCommand(): CommandDefinition {
|
||||||
|
return {
|
||||||
|
name: 'research',
|
||||||
|
description: 'Delegate a task to the configured research sub-agent',
|
||||||
|
execute: async (args, ctx) => {
|
||||||
|
const task = args.join(' ').trim();
|
||||||
|
if (!task) {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
text: 'Usage: /research <question or task>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!ctx.services?.delegateAgent) {
|
||||||
|
return notAvailable('Research command');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
text: await ctx.services.delegateAgent('research', task),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
||||||
registry.register(createHelpCommand(registry));
|
registry.register(createHelpCommand(registry));
|
||||||
registry.register(createStatusCommand());
|
registry.register(createStatusCommand());
|
||||||
registry.register(createUsageCommand());
|
registry.register(createUsageCommand());
|
||||||
registry.register(createContextCommand());
|
registry.register(createContextCommand());
|
||||||
|
registry.register(createResearchCommand());
|
||||||
registry.register(createModelCommand());
|
registry.register(createModelCommand());
|
||||||
registry.register(createCompactCommand());
|
registry.register(createCompactCommand());
|
||||||
registry.register(createResetCommand());
|
registry.register(createResetCommand());
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface CommandServices {
|
|||||||
setModel?: (tier: string) => Promise<string> | string;
|
setModel?: (tier: string) => Promise<string> | string;
|
||||||
compact?: () => Promise<string> | string;
|
compact?: () => Promise<string> | string;
|
||||||
reset?: () => Promise<string> | string;
|
reset?: () => Promise<string> | string;
|
||||||
|
delegateAgent?: (agentName: string, task: string) => Promise<string> | string;
|
||||||
|
|
||||||
getElevation?: () => Promise<string> | string;
|
getElevation?: () => Promise<string> | string;
|
||||||
setElevation?: (input: string) => Promise<string> | string;
|
setElevation?: (input: string) => Promise<string> | string;
|
||||||
|
|||||||
@@ -590,6 +590,35 @@ export function createMessageRouter(deps: {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
delegateAgent: async (agentName: string, task: string) => {
|
||||||
|
const target = agentName.trim();
|
||||||
|
const message = task.trim();
|
||||||
|
if (!target || !message) {
|
||||||
|
return 'Usage: /research <question or task>';
|
||||||
|
}
|
||||||
|
if (!deps.agentConfigRegistry) {
|
||||||
|
return 'No agent configurations are registered. Add agent_configs.research in config.';
|
||||||
|
}
|
||||||
|
const agentConfig = deps.agentConfigRegistry.get(target);
|
||||||
|
if (!agentConfig) {
|
||||||
|
const available = deps.agentConfigRegistry.list().map((c) => c.name);
|
||||||
|
return `Agent "${target}" not found. Available agents: ${available.length > 0 ? available.join(', ') : 'none'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tier: ModelTier = agentConfig.modelTier ?? 'default';
|
||||||
|
const systemPrompt = agentConfig.systemPrompt
|
||||||
|
?? `You are a sub-agent named "${target}". Complete the assigned task concisely and accurately.`;
|
||||||
|
|
||||||
|
const result = await agent.delegate({
|
||||||
|
tier,
|
||||||
|
systemPrompt,
|
||||||
|
message,
|
||||||
|
maxTokens: 4096,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `[Agent: ${target} | Tier: ${result.tier} | Tokens: ${result.usage.inputTokens}+${result.usage.outputTokens}]\n\n${result.content}`;
|
||||||
|
},
|
||||||
|
|
||||||
getElevation: () => {
|
getElevation: () => {
|
||||||
const untilRaw = session.getConfig('elevation.until_ms');
|
const untilRaw = session.getConfig('elevation.until_ms');
|
||||||
const reason = session.getConfig('elevation.reason') ?? '';
|
const reason = session.getConfig('elevation.reason') ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user