feat: Stateless subagents (#127)
This commit is contained in:
@@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ApprovalResult } from "../agent/approval-execution";
|
||||
import { getResumeData } from "../agent/check-approval";
|
||||
import { getClient } from "../agent/client";
|
||||
import { setCurrentAgentId } from "../agent/context";
|
||||
import type { AgentProvenance } from "../agent/create";
|
||||
import { sendMessageStream } from "../agent/message";
|
||||
import { linkToolsToAgent, unlinkToolsFromAgent } from "../agent/modify";
|
||||
@@ -57,6 +58,7 @@ import { ReasoningMessage } from "./components/ReasoningMessageRich";
|
||||
import { ResumeSelector } from "./components/ResumeSelector";
|
||||
import { SessionStats as SessionStatsComponent } from "./components/SessionStats";
|
||||
import { StatusMessage } from "./components/StatusMessage";
|
||||
import { SubagentManager } from "./components/SubagentManager";
|
||||
import { SystemPromptSelector } from "./components/SystemPromptSelector";
|
||||
import { ToolCallMessage } from "./components/ToolCallMessageRich";
|
||||
import { ToolsetSelector } from "./components/ToolsetSelector";
|
||||
@@ -292,6 +294,13 @@ export default function App({
|
||||
}
|
||||
}, [initialAgentState]);
|
||||
|
||||
// Set agent context for tools (especially Task tool)
|
||||
useEffect(() => {
|
||||
if (agentId) {
|
||||
setCurrentAgentId(agentId);
|
||||
}
|
||||
}, [agentId]);
|
||||
|
||||
// Whether a stream is in flight (disables input)
|
||||
const [streaming, setStreaming] = useState(false);
|
||||
|
||||
@@ -375,6 +384,9 @@ export default function App({
|
||||
const [resumeSelectorOpen, setResumeSelectorOpen] = useState(false);
|
||||
const [messageSearchOpen, setMessageSearchOpen] = useState(false);
|
||||
|
||||
// Subagent manager state (for /subagents command)
|
||||
const [subagentManagerOpen, setSubagentManagerOpen] = useState(false);
|
||||
|
||||
// Profile selector state
|
||||
const [profileSelectorOpen, setProfileSelectorOpen] = useState(false);
|
||||
|
||||
@@ -1494,6 +1506,12 @@ export default function App({
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /subagents command - opens subagent manager
|
||||
if (trimmed === "/subagents") {
|
||||
setSubagentManagerOpen(true);
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /exit command - show stats and exit
|
||||
if (trimmed === "/exit") {
|
||||
handleExit();
|
||||
@@ -3735,6 +3753,11 @@ Plan file path: ${planFilePath}`;
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Subagent Manager - for managing custom subagents */}
|
||||
{subagentManagerOpen && (
|
||||
<SubagentManager onClose={() => setSubagentManagerOpen(false)} />
|
||||
)}
|
||||
|
||||
{/* Resume Selector - conditionally mounted as overlay */}
|
||||
{resumeSelectorOpen && (
|
||||
<ResumeSelector
|
||||
|
||||
@@ -157,6 +157,13 @@ export const commands: Record<string, Command> = {
|
||||
return "Opening pinned agents...";
|
||||
},
|
||||
},
|
||||
"/subagents": {
|
||||
desc: "Manage custom subagents",
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx to open SubagentManager component
|
||||
return "Opening subagent manager...";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
139
src/cli/components/SubagentManager.tsx
Normal file
139
src/cli/components/SubagentManager.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* SubagentManager component - displays available subagents
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
AGENTS_DIR,
|
||||
clearSubagentConfigCache,
|
||||
GLOBAL_AGENTS_DIR,
|
||||
getAllSubagentConfigs,
|
||||
getBuiltinSubagentNames,
|
||||
type SubagentConfig,
|
||||
} from "../../agent/subagents";
|
||||
import { colors } from "./colors";
|
||||
|
||||
interface SubagentManagerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface SubagentItem {
|
||||
name: string;
|
||||
config: SubagentConfig;
|
||||
}
|
||||
|
||||
export function SubagentManager({ onClose }: SubagentManagerProps) {
|
||||
const [builtinSubagents, setBuiltinSubagents] = useState<SubagentItem[]>([]);
|
||||
const [customSubagents, setCustomSubagents] = useState<SubagentItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSubagents() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
clearSubagentConfigCache();
|
||||
const configs = await getAllSubagentConfigs();
|
||||
const builtinNames = getBuiltinSubagentNames();
|
||||
const builtin: SubagentItem[] = [];
|
||||
const custom: SubagentItem[] = [];
|
||||
|
||||
for (const [name, config] of Object.entries(configs)) {
|
||||
const item = { name, config };
|
||||
if (builtinNames.has(name)) {
|
||||
builtin.push(item);
|
||||
} else {
|
||||
custom.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
builtin.sort((a, b) => a.name.localeCompare(b.name));
|
||||
custom.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
setBuiltinSubagents(builtin);
|
||||
setCustomSubagents(custom);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadSubagents();
|
||||
}, []);
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (key.escape || key.return) {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>Loading subagents...</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const renderSubagentList = (items: SubagentItem[]) =>
|
||||
items.map((item, index) => (
|
||||
<Box
|
||||
key={item.name}
|
||||
flexDirection="column"
|
||||
marginBottom={index < items.length - 1 ? 1 : 0}
|
||||
>
|
||||
<Box gap={1}>
|
||||
<Text bold color={colors.selector.itemHighlighted}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text dimColor>({item.config.recommendedModel})</Text>
|
||||
</Box>
|
||||
<Text> {item.config.description}</Text>
|
||||
</Box>
|
||||
));
|
||||
|
||||
const hasNoSubagents =
|
||||
builtinSubagents.length === 0 && customSubagents.length === 0;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text bold color={colors.selector.title}>
|
||||
Available Subagents
|
||||
</Text>
|
||||
|
||||
{error && <Text color={colors.status.error}>Error: {error}</Text>}
|
||||
|
||||
{hasNoSubagents ? (
|
||||
<Text dimColor>No subagents found</Text>
|
||||
) : (
|
||||
<>
|
||||
{builtinSubagents.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold dimColor>
|
||||
Built-in
|
||||
</Text>
|
||||
{renderSubagentList(builtinSubagents)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{customSubagents.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold dimColor>
|
||||
Custom
|
||||
</Text>
|
||||
{renderSubagentList(customSubagents)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text dimColor>
|
||||
To add custom subagents, create .md files in {AGENTS_DIR}/ (project) or{" "}
|
||||
{GLOBAL_AGENTS_DIR}/ (global)
|
||||
</Text>
|
||||
<Text dimColor>Press ESC or Enter to close</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,11 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
|
||||
const rawName = line.name ?? "?";
|
||||
const argsText = line.argsText ?? "...";
|
||||
|
||||
// Task tool handles its own display via console.log - suppress UI rendering entirely
|
||||
if (rawName === "Task" || rawName === "task") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply tool name remapping from old codebase
|
||||
let displayName = rawName;
|
||||
// Anthropic toolset
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Letta } from "@letta-ai/letta-client";
|
||||
import { Box, Text } from "ink";
|
||||
|
||||
import type { AgentProvenance } from "../../agent/create";
|
||||
import { isProjectBlock } from "../../agent/memory";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
import { getVersion } from "../../version";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
@@ -84,10 +85,46 @@ export function getAgentStatusHints(
|
||||
return hints;
|
||||
}
|
||||
|
||||
// For new agents, just show memory block labels
|
||||
if (agentProvenance && agentProvenance.blocks.length > 0) {
|
||||
const blockLabels = agentProvenance.blocks.map((b) => b.label).join(", ");
|
||||
hints.push(`→ Memory blocks: ${blockLabels}`);
|
||||
// For new agents with provenance, show block sources
|
||||
if (agentProvenance) {
|
||||
// Blocks reused from existing storage
|
||||
const reusedGlobalBlocks = agentProvenance.blocks
|
||||
.filter((b) => b.source === "global")
|
||||
.map((b) => b.label);
|
||||
const reusedProjectBlocks = agentProvenance.blocks
|
||||
.filter((b) => b.source === "project")
|
||||
.map((b) => b.label);
|
||||
|
||||
// New blocks - categorize by where they'll be stored
|
||||
// (project blocks → .letta/, others → ~/.letta/)
|
||||
const newBlocks = agentProvenance.blocks.filter((b) => b.source === "new");
|
||||
const newGlobalBlocks = newBlocks
|
||||
.filter((b) => !isProjectBlock(b.label))
|
||||
.map((b) => b.label);
|
||||
const newProjectBlocks = newBlocks
|
||||
.filter((b) => isProjectBlock(b.label))
|
||||
.map((b) => b.label);
|
||||
|
||||
if (reusedGlobalBlocks.length > 0) {
|
||||
hints.push(
|
||||
`→ Reusing from global (~/.letta/): ${reusedGlobalBlocks.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (newGlobalBlocks.length > 0) {
|
||||
hints.push(
|
||||
`→ Created in global (~/.letta/): ${newGlobalBlocks.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (reusedProjectBlocks.length > 0) {
|
||||
hints.push(
|
||||
`→ Reusing from project (.letta/): ${reusedProjectBlocks.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (newProjectBlocks.length > 0) {
|
||||
hints.push(
|
||||
`→ Created in project (.letta/): ${newProjectBlocks.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return hints;
|
||||
|
||||
Reference in New Issue
Block a user