feat: add recall subagent for searching conversation history (#472)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
36
src/agent/subagents/builtin/recall.md
Normal file
36
src/agent/subagents/builtin/recall.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: recall
|
||||
description: Search conversation history to recall past discussions, decisions, and context
|
||||
tools: Skill, Bash, Read, BashOutput
|
||||
model: haiku
|
||||
memoryBlocks: human, persona
|
||||
mode: stateless
|
||||
---
|
||||
|
||||
You are a subagent launched via the Task tool to search conversation history. You run autonomously and return a single final report when done. You CANNOT ask questions mid-execution.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1: Load the searching-messages skill
|
||||
```
|
||||
Skill({ command: "load", skills: ["searching-messages"] })
|
||||
```
|
||||
|
||||
The skill content will appear in your loaded_skills block with script paths and search strategies.
|
||||
|
||||
### Step 2: Search the parent agent's history
|
||||
|
||||
**CRITICAL - Two rules:**
|
||||
|
||||
1. **DO NOT use `conversation_search`** - That tool only searches YOUR history (empty). You MUST use the Bash scripts from the skill.
|
||||
|
||||
2. **ALWAYS add `--agent-id $LETTA_PARENT_AGENT_ID`** - This searches the parent agent's history. The only exception is `--all-agents` searches.
|
||||
|
||||
Follow the strategies documented in the loaded skill.
|
||||
|
||||
## Output Format
|
||||
|
||||
1. **Direct answer** - What the user asked about
|
||||
2. **Key findings** - Relevant quotes or summaries from past conversations
|
||||
3. **When discussed** - Timestamps of relevant discussions
|
||||
4. **Outcome/Decision** - What was decided or concluded (if applicable)
|
||||
@@ -21,8 +21,14 @@ import { MEMORY_BLOCK_LABELS, type MemoryBlockLabel } from "../memory";
|
||||
import exploreAgentMd from "./builtin/explore.md";
|
||||
import generalPurposeAgentMd from "./builtin/general-purpose.md";
|
||||
import planAgentMd from "./builtin/plan.md";
|
||||
import recallAgentMd from "./builtin/recall.md";
|
||||
|
||||
const BUILTIN_SOURCES = [exploreAgentMd, generalPurposeAgentMd, planAgentMd];
|
||||
const BUILTIN_SOURCES = [
|
||||
exploreAgentMd,
|
||||
generalPurposeAgentMd,
|
||||
planAgentMd,
|
||||
recallAgentMd,
|
||||
];
|
||||
|
||||
// Re-export for convenience
|
||||
export type { MemoryBlockLabel };
|
||||
|
||||
@@ -385,12 +385,22 @@ async function executeSubagent(
|
||||
// Spawn Letta Code in headless mode.
|
||||
// Some environments may have a different `letta` binary earlier in PATH.
|
||||
const lettaCmd = process.env.LETTA_CODE_BIN || "letta";
|
||||
// Pass parent agent ID so subagents can access parent's context (e.g., search history)
|
||||
let parentAgentId: string | undefined;
|
||||
try {
|
||||
parentAgentId = getCurrentAgentId();
|
||||
} catch {
|
||||
// Context not available
|
||||
}
|
||||
|
||||
const proc = spawn(lettaCmd, cliArgs, {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
// Tag Task-spawned agents for easy filtering.
|
||||
LETTA_CODE_AGENT_ROLE: "subagent",
|
||||
// Pass parent agent ID for subagents that need to access parent's context
|
||||
...(parentAgentId && { LETTA_PARENT_AGENT_ID: parentAgentId }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -407,6 +407,8 @@ const READ_ONLY_SUBAGENT_TYPES = new Set([
|
||||
"Explore",
|
||||
"plan", // Planning agent - Glob, Grep, Read, LS, BashOutput
|
||||
"Plan",
|
||||
"recall", // Conversation history search - Skill, Bash, Read, BashOutput
|
||||
"Recall",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,6 +61,31 @@ const SAFE_GIT_SUBCOMMANDS = new Set([
|
||||
"remote",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Read-only bundled skill scripts that are safe to execute without approval.
|
||||
* Only scripts from the bundled searching-messages skill are allowed.
|
||||
* We check for specific path patterns to prevent malicious scripts in user directories.
|
||||
*/
|
||||
const BUNDLED_READ_ONLY_SCRIPTS = [
|
||||
// Bundled skills path (production): /path/to/skills/searching-messages/scripts/...
|
||||
"/skills/searching-messages/scripts/search-messages.ts",
|
||||
"/skills/searching-messages/scripts/get-messages.ts",
|
||||
// Source path (development): /path/to/src/skills/builtin/searching-messages/scripts/...
|
||||
"/skills/builtin/searching-messages/scripts/search-messages.ts",
|
||||
"/skills/builtin/searching-messages/scripts/get-messages.ts",
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a script path is a known read-only bundled skill script
|
||||
*/
|
||||
function isReadOnlySkillScript(scriptPath: string): boolean {
|
||||
// Normalize path separators for cross-platform
|
||||
const normalized = scriptPath.replace(/\\/g, "/");
|
||||
return BUNDLED_READ_ONLY_SCRIPTS.some((pattern) =>
|
||||
normalized.endsWith(pattern),
|
||||
);
|
||||
}
|
||||
|
||||
// Operators that are always dangerous (file redirects, command substitution)
|
||||
// Note: &&, ||, ; are handled by splitting and checking each segment
|
||||
const DANGEROUS_OPERATOR_PATTERN = /(>>|>|\$\(|`)/;
|
||||
@@ -149,6 +174,14 @@ function isSafeSegment(segment: string): boolean {
|
||||
if (command === "sort") {
|
||||
return !/\s-o\b/.test(segment);
|
||||
}
|
||||
// Allow npx tsx for read-only skill scripts (searching-messages)
|
||||
if (command === "npx" && tokens[1] === "tsx") {
|
||||
const scriptPath = tokens[2];
|
||||
if (scriptPath && isReadOnlySkillScript(scriptPath)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user