feat: add messaging-agents bundled skill (#589)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
125
src/skills/builtin/messaging-agents/SKILL.md
Normal file
125
src/skills/builtin/messaging-agents/SKILL.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: messaging-agents
|
||||
description: Send messages to other agents on your server. Use when you need to communicate with, query, or delegate tasks to another agent.
|
||||
---
|
||||
|
||||
# Messaging Agents
|
||||
|
||||
This skill enables you to send messages to other agents on the same Letta server using the thread-safe conversations API.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- You need to ask another agent a question
|
||||
- You want to query an agent that has specialized knowledge
|
||||
- You need information that another agent has in their memory
|
||||
- You want to coordinate with another agent on a task
|
||||
|
||||
## What the Target Agent Can and Cannot Do
|
||||
|
||||
**The target agent CANNOT:**
|
||||
- Access your local environment (read/write files in your codebase)
|
||||
- Execute shell commands on your machine
|
||||
- Use your tools (Bash, Read, Write, Edit, etc.)
|
||||
|
||||
**The target agent CAN:**
|
||||
- Use their own tools (whatever they have configured)
|
||||
- Access their own memory blocks
|
||||
- Make API calls if they have web/API tools
|
||||
- Search the web if they have web search tools
|
||||
- Respond with information from their knowledge/memory
|
||||
|
||||
**Important:** This skill is for *communication* with other agents, not *delegation* of local work. The target agent runs in their own environment and cannot interact with your codebase.
|
||||
|
||||
## Finding an Agent to Message
|
||||
|
||||
If you don't have a specific agent ID, use these skills to find one:
|
||||
|
||||
### By Name or Tags
|
||||
Load the `finding-agents` skill to search for agents:
|
||||
```bash
|
||||
npx tsx <FINDING_AGENTS_SKILL_DIR>/scripts/find-agents.ts --query "agent-name"
|
||||
npx tsx <FINDING_AGENTS_SKILL_DIR>/scripts/find-agents.ts --tags "origin:letta-code"
|
||||
```
|
||||
|
||||
### By Topic They Discussed
|
||||
Load the `searching-messages` skill to find which agent worked on something:
|
||||
```bash
|
||||
npx tsx <SEARCHING_MESSAGES_SKILL_DIR>/scripts/search-messages.ts --query "topic" --all-agents
|
||||
```
|
||||
Results include `agent_id` for each matching message.
|
||||
|
||||
## Script Usage
|
||||
|
||||
### Starting a New Conversation
|
||||
|
||||
```bash
|
||||
npx tsx <SKILL_DIR>/scripts/start-conversation.ts --agent-id <id> --message "<text>"
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
| Arg | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `--agent-id <id>` | Yes | Target agent ID to message |
|
||||
| `--message <text>` | Yes | Message to send |
|
||||
| `--timeout <ms>` | No | Max wait time in ms (default: 120000) |
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx tsx <SKILL_DIR>/scripts/start-conversation.ts \
|
||||
--agent-id agent-abc123 \
|
||||
--message "What do you know about the authentication system?"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"conversation_id": "conversation-xyz789",
|
||||
"response": "The authentication system uses JWT tokens...",
|
||||
"agent_id": "agent-abc123",
|
||||
"agent_name": "BackendExpert"
|
||||
}
|
||||
```
|
||||
|
||||
### Continuing a Conversation
|
||||
|
||||
```bash
|
||||
npx tsx <SKILL_DIR>/scripts/continue-conversation.ts --conversation-id <id> --message "<text>"
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
| Arg | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `--conversation-id <id>` | Yes | Existing conversation ID |
|
||||
| `--message <text>` | Yes | Follow-up message to send |
|
||||
| `--timeout <ms>` | No | Max wait time in ms (default: 120000) |
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
npx tsx <SKILL_DIR>/scripts/continue-conversation.ts \
|
||||
--conversation-id conversation-xyz789 \
|
||||
--message "Can you explain more about the token refresh flow?"
|
||||
```
|
||||
|
||||
## Understanding the Response
|
||||
|
||||
- Scripts return only the **final assistant message** (not tool calls or reasoning)
|
||||
- The target agent may use tools, think, and reason - but you only see their final response
|
||||
- To see the full conversation transcript (including tool calls), use the `searching-messages` skill with `--agent-id` targeting the other agent
|
||||
|
||||
## How It Works
|
||||
|
||||
When you send a message, the target agent receives it with a system reminder:
|
||||
```
|
||||
<system-reminder>
|
||||
This message is from "YourAgentName" (agent ID: agent-xxx), an agent currently running inside the Letta Code CLI (docs.letta.com/letta-code).
|
||||
The sender will only see the final message you generate (not tool calls or reasoning).
|
||||
If you need to share detailed information, include it in your response text.
|
||||
</system-reminder>
|
||||
```
|
||||
|
||||
This helps the target agent understand the context and format their response appropriately.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **finding-agents**: Find agents by name, tags, or fuzzy search
|
||||
- **searching-messages**: Search past messages across agents, or view full conversation transcripts
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Continue Conversation - Send a follow-up message to an existing conversation
|
||||
*
|
||||
* This script is standalone and can be run outside the CLI process.
|
||||
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
||||
* It reads sender agent ID from LETTA_AGENT_ID env var.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx continue-conversation.ts --conversation-id <id> --message "<text>"
|
||||
*
|
||||
* Options:
|
||||
* --conversation-id <id> Existing conversation ID (required)
|
||||
* --message <text> Message to send (required)
|
||||
* --timeout <ms> Max wait time in ms (default: 120000)
|
||||
*
|
||||
* Output:
|
||||
* JSON with conversation_id, response, agent_id, agent_name
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
||||
// (ES module imports don't respect NODE_PATH, but require does)
|
||||
const require = createRequire(import.meta.url);
|
||||
const Letta = require("@letta-ai/letta-client")
|
||||
.default as typeof import("@letta-ai/letta-client").default;
|
||||
type LettaClient = InstanceType<typeof Letta>;
|
||||
|
||||
interface ContinueConversationOptions {
|
||||
conversationId: string;
|
||||
message: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface ContinueConversationResult {
|
||||
conversation_id: string;
|
||||
response: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from env var or settings file
|
||||
*/
|
||||
function getApiKey(): string {
|
||||
if (process.env.LETTA_API_KEY) {
|
||||
return process.env.LETTA_API_KEY;
|
||||
}
|
||||
|
||||
const settingsPath = join(homedir(), ".letta", "settings.json");
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
||||
if (settings.env?.LETTA_API_KEY) {
|
||||
return settings.env.LETTA_API_KEY;
|
||||
}
|
||||
} catch {
|
||||
// Settings file doesn't exist or is invalid
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sender agent ID from env var
|
||||
*/
|
||||
function getSenderAgentId(): string {
|
||||
if (process.env.LETTA_AGENT_ID) {
|
||||
return process.env.LETTA_AGENT_ID;
|
||||
}
|
||||
throw new Error(
|
||||
"No LETTA_AGENT_ID found. This script should be run from within a Letta Code session.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Letta client with auth from env/settings
|
||||
*/
|
||||
function createClient(): LettaClient {
|
||||
return new Letta({ apiKey: getApiKey() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system reminder prefix for the message
|
||||
*/
|
||||
function buildSystemReminder(
|
||||
senderAgentName: string,
|
||||
senderAgentId: string,
|
||||
): string {
|
||||
return `<system-reminder>
|
||||
This message is from "${senderAgentName}" (agent ID: ${senderAgentId}), an agent currently running inside the Letta Code CLI (docs.letta.com/letta-code).
|
||||
The sender will only see the final message you generate (not tool calls or reasoning).
|
||||
If you need to share detailed information, include it in your response text.
|
||||
</system-reminder>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue an existing conversation by sending a follow-up message
|
||||
* @param client - Letta client instance
|
||||
* @param options - Options including conversation ID and message
|
||||
* @returns Conversation result with response and metadata
|
||||
*/
|
||||
export async function continueConversation(
|
||||
client: LettaClient,
|
||||
options: ContinueConversationOptions,
|
||||
): Promise<ContinueConversationResult> {
|
||||
const { conversationId, message } = options;
|
||||
|
||||
// 1. Fetch conversation to get agent_id and validate it exists
|
||||
const conversation = await client.conversations.retrieve(conversationId);
|
||||
|
||||
// 2. Fetch target agent to get name
|
||||
const targetAgent = await client.agents.retrieve(conversation.agent_id);
|
||||
|
||||
// 3. Fetch sender agent to get name for system reminder
|
||||
const senderAgentId = getSenderAgentId();
|
||||
const senderAgent = await client.agents.retrieve(senderAgentId);
|
||||
|
||||
// 4. Build message with system reminder prefix
|
||||
const systemReminder = buildSystemReminder(senderAgent.name, senderAgentId);
|
||||
const fullMessage = systemReminder + message;
|
||||
|
||||
// 5. Send message and consume the stream
|
||||
// Note: conversations.messages.create always returns a Stream
|
||||
const stream = await client.conversations.messages.create(conversationId, {
|
||||
input: fullMessage,
|
||||
});
|
||||
|
||||
// 6. Consume stream and extract final assistant message
|
||||
let finalResponse = "";
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.message_type === "assistant_message") {
|
||||
// Content can be string or array of content parts
|
||||
const content = chunk.content;
|
||||
if (typeof content === "string") {
|
||||
finalResponse += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
if (
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
part.type === "text" &&
|
||||
"text" in part
|
||||
) {
|
||||
finalResponse += (part as { text: string }).text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
conversation_id: conversationId,
|
||||
response: finalResponse,
|
||||
agent_id: targetAgent.id,
|
||||
agent_name: targetAgent.name,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): ContinueConversationOptions {
|
||||
const conversationIdIndex = args.indexOf("--conversation-id");
|
||||
if (conversationIdIndex === -1 || conversationIdIndex + 1 >= args.length) {
|
||||
throw new Error(
|
||||
"Missing required argument: --conversation-id <conversation-id>",
|
||||
);
|
||||
}
|
||||
const conversationId = args[conversationIdIndex + 1] as string;
|
||||
|
||||
const messageIndex = args.indexOf("--message");
|
||||
if (messageIndex === -1 || messageIndex + 1 >= args.length) {
|
||||
throw new Error("Missing required argument: --message <text>");
|
||||
}
|
||||
const message = args[messageIndex + 1] as string;
|
||||
|
||||
const options: ContinueConversationOptions = { conversationId, message };
|
||||
|
||||
const timeoutIndex = args.indexOf("--timeout");
|
||||
if (timeoutIndex !== -1 && timeoutIndex + 1 < args.length) {
|
||||
const timeout = Number.parseInt(args[timeoutIndex + 1] as string, 10);
|
||||
if (!Number.isNaN(timeout)) {
|
||||
options.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// CLI entry point - check if this file is being run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
(async () => {
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const client = createClient();
|
||||
const result = await continueConversation(client, options);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.error(`
|
||||
Usage: npx tsx continue-conversation.ts --conversation-id <id> --message "<text>"
|
||||
|
||||
Options:
|
||||
--conversation-id <id> Existing conversation ID (required)
|
||||
--message <text> Message to send (required)
|
||||
--timeout <ms> Max wait time in ms (default: 120000)
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Start Conversation - Start a new conversation with an agent and send a message
|
||||
*
|
||||
* This script is standalone and can be run outside the CLI process.
|
||||
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
||||
* It reads sender agent ID from LETTA_AGENT_ID env var.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx start-conversation.ts --agent-id <id> --message "<text>"
|
||||
*
|
||||
* Options:
|
||||
* --agent-id <id> Target agent ID to message (required)
|
||||
* --message <text> Message to send (required)
|
||||
* --timeout <ms> Max wait time in ms (default: 120000)
|
||||
*
|
||||
* Output:
|
||||
* JSON with conversation_id, response, agent_id, agent_name
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
||||
// (ES module imports don't respect NODE_PATH, but require does)
|
||||
const require = createRequire(import.meta.url);
|
||||
const Letta = require("@letta-ai/letta-client")
|
||||
.default as typeof import("@letta-ai/letta-client").default;
|
||||
type LettaClient = InstanceType<typeof Letta>;
|
||||
|
||||
interface StartConversationOptions {
|
||||
agentId: string;
|
||||
message: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
interface StartConversationResult {
|
||||
conversation_id: string;
|
||||
response: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from env var or settings file
|
||||
*/
|
||||
function getApiKey(): string {
|
||||
if (process.env.LETTA_API_KEY) {
|
||||
return process.env.LETTA_API_KEY;
|
||||
}
|
||||
|
||||
const settingsPath = join(homedir(), ".letta", "settings.json");
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
||||
if (settings.env?.LETTA_API_KEY) {
|
||||
return settings.env.LETTA_API_KEY;
|
||||
}
|
||||
} catch {
|
||||
// Settings file doesn't exist or is invalid
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sender agent ID from env var
|
||||
*/
|
||||
function getSenderAgentId(): string {
|
||||
if (process.env.LETTA_AGENT_ID) {
|
||||
return process.env.LETTA_AGENT_ID;
|
||||
}
|
||||
throw new Error(
|
||||
"No LETTA_AGENT_ID found. This script should be run from within a Letta Code session.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Letta client with auth from env/settings
|
||||
*/
|
||||
function createClient(): LettaClient {
|
||||
return new Letta({ apiKey: getApiKey() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system reminder prefix for the message
|
||||
*/
|
||||
function buildSystemReminder(
|
||||
senderAgentName: string,
|
||||
senderAgentId: string,
|
||||
): string {
|
||||
return `<system-reminder>
|
||||
This message is from "${senderAgentName}" (agent ID: ${senderAgentId}), an agent currently running inside the Letta Code CLI (docs.letta.com/letta-code).
|
||||
The sender will only see the final message you generate (not tool calls or reasoning).
|
||||
If you need to share detailed information, include it in your response text.
|
||||
</system-reminder>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new conversation with an agent and send a message
|
||||
* @param client - Letta client instance
|
||||
* @param options - Options including target agent ID and message
|
||||
* @returns Conversation result with response and metadata
|
||||
*/
|
||||
export async function startConversation(
|
||||
client: LettaClient,
|
||||
options: StartConversationOptions,
|
||||
): Promise<StartConversationResult> {
|
||||
const { agentId, message } = options;
|
||||
|
||||
// 1. Fetch target agent to validate existence and get name
|
||||
const targetAgent = await client.agents.retrieve(agentId);
|
||||
|
||||
// 2. Fetch sender agent to get name for system reminder
|
||||
const senderAgentId = getSenderAgentId();
|
||||
const senderAgent = await client.agents.retrieve(senderAgentId);
|
||||
|
||||
// 3. Create new conversation
|
||||
const conversation = await client.conversations.create({
|
||||
agent_id: agentId,
|
||||
});
|
||||
|
||||
// 4. Build message with system reminder prefix
|
||||
const systemReminder = buildSystemReminder(senderAgent.name, senderAgentId);
|
||||
const fullMessage = systemReminder + message;
|
||||
|
||||
// 5. Send message and consume the stream
|
||||
// Note: conversations.messages.create always returns a Stream
|
||||
const stream = await client.conversations.messages.create(conversation.id, {
|
||||
input: fullMessage,
|
||||
});
|
||||
|
||||
// 6. Consume stream and extract final assistant message
|
||||
let finalResponse = "";
|
||||
for await (const chunk of stream) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error("Chunk:", JSON.stringify(chunk, null, 2));
|
||||
}
|
||||
if (chunk.message_type === "assistant_message") {
|
||||
// Content can be string or array of content parts
|
||||
const content = chunk.content;
|
||||
if (typeof content === "string") {
|
||||
finalResponse += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
if (
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
part.type === "text" &&
|
||||
"text" in part
|
||||
) {
|
||||
finalResponse += (part as { text: string }).text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
conversation_id: conversation.id,
|
||||
response: finalResponse,
|
||||
agent_id: targetAgent.id,
|
||||
agent_name: targetAgent.name,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): StartConversationOptions {
|
||||
const agentIdIndex = args.indexOf("--agent-id");
|
||||
if (agentIdIndex === -1 || agentIdIndex + 1 >= args.length) {
|
||||
throw new Error("Missing required argument: --agent-id <agent-id>");
|
||||
}
|
||||
const agentId = args[agentIdIndex + 1] as string;
|
||||
|
||||
const messageIndex = args.indexOf("--message");
|
||||
if (messageIndex === -1 || messageIndex + 1 >= args.length) {
|
||||
throw new Error("Missing required argument: --message <text>");
|
||||
}
|
||||
const message = args[messageIndex + 1] as string;
|
||||
|
||||
const options: StartConversationOptions = { agentId, message };
|
||||
|
||||
const timeoutIndex = args.indexOf("--timeout");
|
||||
if (timeoutIndex !== -1 && timeoutIndex + 1 < args.length) {
|
||||
const timeout = Number.parseInt(args[timeoutIndex + 1] as string, 10);
|
||||
if (!Number.isNaN(timeout)) {
|
||||
options.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// CLI entry point - check if this file is being run directly
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMainModule) {
|
||||
(async () => {
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const client = createClient();
|
||||
const result = await startConversation(client, options);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.error(`
|
||||
Usage: npx tsx start-conversation.ts --agent-id <id> --message "<text>"
|
||||
|
||||
Options:
|
||||
--agent-id <id> Target agent ID to message (required)
|
||||
--message <text> Message to send (required)
|
||||
--timeout <ms> Max wait time in ms (default: 120000)
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
}
|
||||
293
src/tests/skills/messaging-agents-scripts.test.ts
Normal file
293
src/tests/skills/messaging-agents-scripts.test.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Tests for the bundled messaging-agents scripts
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import type Letta from "@letta-ai/letta-client";
|
||||
import { continueConversation } from "../../skills/builtin/messaging-agents/scripts/continue-conversation";
|
||||
import { startConversation } from "../../skills/builtin/messaging-agents/scripts/start-conversation";
|
||||
|
||||
// Mock agent data
|
||||
const mockSenderAgent = {
|
||||
id: "agent-sender-123",
|
||||
name: "SenderAgent",
|
||||
};
|
||||
|
||||
const mockTargetAgent = {
|
||||
id: "agent-target-456",
|
||||
name: "TargetAgent",
|
||||
};
|
||||
|
||||
const mockConversation = {
|
||||
id: "conversation-789",
|
||||
agent_id: mockTargetAgent.id,
|
||||
};
|
||||
|
||||
// Helper to create a mock async iterator for streaming
|
||||
function createMockStream(
|
||||
chunks: Array<{ message_type: string; content?: string }>,
|
||||
) {
|
||||
return {
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
for (const chunk of chunks) {
|
||||
yield chunk;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("start-conversation", () => {
|
||||
const originalEnv = process.env.LETTA_AGENT_ID;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.LETTA_AGENT_ID = mockSenderAgent.id;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.LETTA_AGENT_ID = originalEnv;
|
||||
} else {
|
||||
delete process.env.LETTA_AGENT_ID;
|
||||
}
|
||||
});
|
||||
|
||||
test("creates conversation and sends message with system reminder", async () => {
|
||||
const mockRetrieve = mock((id: string) => {
|
||||
if (id === mockSenderAgent.id) return Promise.resolve(mockSenderAgent);
|
||||
if (id === mockTargetAgent.id) return Promise.resolve(mockTargetAgent);
|
||||
throw new Error(`Unknown agent: ${id}`);
|
||||
});
|
||||
|
||||
const mockCreate = mock(() => Promise.resolve(mockConversation));
|
||||
|
||||
const mockMessageCreate = mock(() =>
|
||||
Promise.resolve(
|
||||
createMockStream([
|
||||
{ message_type: "reasoning_message", content: "thinking..." },
|
||||
{ message_type: "assistant_message", content: "Hello there!" },
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const mockClient = {
|
||||
agents: {
|
||||
retrieve: mockRetrieve,
|
||||
},
|
||||
conversations: {
|
||||
create: mockCreate,
|
||||
messages: {
|
||||
create: mockMessageCreate,
|
||||
},
|
||||
},
|
||||
} as unknown as Letta;
|
||||
|
||||
const result = await startConversation(mockClient, {
|
||||
agentId: mockTargetAgent.id,
|
||||
message: "Hello!",
|
||||
});
|
||||
|
||||
// Check that target agent was fetched
|
||||
expect(mockRetrieve).toHaveBeenCalledWith(mockTargetAgent.id);
|
||||
// Check that sender agent was fetched
|
||||
expect(mockRetrieve).toHaveBeenCalledWith(mockSenderAgent.id);
|
||||
|
||||
// Check conversation was created
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
agent_id: mockTargetAgent.id,
|
||||
});
|
||||
|
||||
// Check message was sent with system reminder
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith(mockConversation.id, {
|
||||
input: expect.stringContaining("<system-reminder>"),
|
||||
});
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith(mockConversation.id, {
|
||||
input: expect.stringContaining("Hello!"),
|
||||
});
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith(mockConversation.id, {
|
||||
input: expect.stringContaining(mockSenderAgent.name),
|
||||
});
|
||||
|
||||
// Check result
|
||||
expect(result.conversation_id).toBe(mockConversation.id);
|
||||
expect(result.response).toBe("Hello there!");
|
||||
expect(result.agent_id).toBe(mockTargetAgent.id);
|
||||
expect(result.agent_name).toBe(mockTargetAgent.name);
|
||||
});
|
||||
|
||||
test("throws error when target agent not found", async () => {
|
||||
const mockClient = {
|
||||
agents: {
|
||||
retrieve: mock(() => Promise.reject(new Error("Agent not found"))),
|
||||
},
|
||||
conversations: {
|
||||
create: mock(),
|
||||
messages: { create: mock() },
|
||||
},
|
||||
} as unknown as Letta;
|
||||
|
||||
await expect(
|
||||
startConversation(mockClient, {
|
||||
agentId: "nonexistent",
|
||||
message: "Hello!",
|
||||
}),
|
||||
).rejects.toThrow("Agent not found");
|
||||
});
|
||||
|
||||
test("throws error when LETTA_AGENT_ID not set", async () => {
|
||||
delete process.env.LETTA_AGENT_ID;
|
||||
|
||||
const mockClient = {
|
||||
agents: {
|
||||
retrieve: mock(() => Promise.resolve(mockTargetAgent)),
|
||||
},
|
||||
conversations: {
|
||||
create: mock(),
|
||||
messages: { create: mock() },
|
||||
},
|
||||
} as unknown as Letta;
|
||||
|
||||
await expect(
|
||||
startConversation(mockClient, {
|
||||
agentId: mockTargetAgent.id,
|
||||
message: "Hello!",
|
||||
}),
|
||||
).rejects.toThrow("LETTA_AGENT_ID");
|
||||
});
|
||||
});
|
||||
|
||||
describe("continue-conversation", () => {
|
||||
const originalEnv = process.env.LETTA_AGENT_ID;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.LETTA_AGENT_ID = mockSenderAgent.id;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.LETTA_AGENT_ID = originalEnv;
|
||||
} else {
|
||||
delete process.env.LETTA_AGENT_ID;
|
||||
}
|
||||
});
|
||||
|
||||
test("continues existing conversation with system reminder", async () => {
|
||||
const mockAgentRetrieve = mock((id: string) => {
|
||||
if (id === mockSenderAgent.id) return Promise.resolve(mockSenderAgent);
|
||||
if (id === mockTargetAgent.id) return Promise.resolve(mockTargetAgent);
|
||||
throw new Error(`Unknown agent: ${id}`);
|
||||
});
|
||||
|
||||
const mockConversationRetrieve = mock(() =>
|
||||
Promise.resolve(mockConversation),
|
||||
);
|
||||
|
||||
const mockMessageCreate = mock(() =>
|
||||
Promise.resolve(
|
||||
createMockStream([
|
||||
{ message_type: "assistant_message", content: "Follow-up response!" },
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const mockClient = {
|
||||
agents: {
|
||||
retrieve: mockAgentRetrieve,
|
||||
},
|
||||
conversations: {
|
||||
retrieve: mockConversationRetrieve,
|
||||
messages: {
|
||||
create: mockMessageCreate,
|
||||
},
|
||||
},
|
||||
} as unknown as Letta;
|
||||
|
||||
const result = await continueConversation(mockClient, {
|
||||
conversationId: mockConversation.id,
|
||||
message: "Follow-up question",
|
||||
});
|
||||
|
||||
// Check conversation was fetched
|
||||
expect(mockConversationRetrieve).toHaveBeenCalledWith(mockConversation.id);
|
||||
|
||||
// Check target agent was fetched
|
||||
expect(mockAgentRetrieve).toHaveBeenCalledWith(mockTargetAgent.id);
|
||||
|
||||
// Check sender agent was fetched
|
||||
expect(mockAgentRetrieve).toHaveBeenCalledWith(mockSenderAgent.id);
|
||||
|
||||
// Check message was sent with system reminder
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith(mockConversation.id, {
|
||||
input: expect.stringContaining("<system-reminder>"),
|
||||
});
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith(mockConversation.id, {
|
||||
input: expect.stringContaining("Follow-up question"),
|
||||
});
|
||||
|
||||
// Check result
|
||||
expect(result.conversation_id).toBe(mockConversation.id);
|
||||
expect(result.response).toBe("Follow-up response!");
|
||||
expect(result.agent_id).toBe(mockTargetAgent.id);
|
||||
expect(result.agent_name).toBe(mockTargetAgent.name);
|
||||
});
|
||||
|
||||
test("throws error when conversation not found", async () => {
|
||||
const mockClient = {
|
||||
agents: {
|
||||
retrieve: mock(),
|
||||
},
|
||||
conversations: {
|
||||
retrieve: mock(() =>
|
||||
Promise.reject(new Error("Conversation not found")),
|
||||
),
|
||||
messages: { create: mock() },
|
||||
},
|
||||
} as unknown as Letta;
|
||||
|
||||
await expect(
|
||||
continueConversation(mockClient, {
|
||||
conversationId: "nonexistent",
|
||||
message: "Hello!",
|
||||
}),
|
||||
).rejects.toThrow("Conversation not found");
|
||||
});
|
||||
|
||||
test("handles empty response from agent", async () => {
|
||||
const mockAgentRetrieve = mock((id: string) => {
|
||||
if (id === mockSenderAgent.id) return Promise.resolve(mockSenderAgent);
|
||||
if (id === mockTargetAgent.id) return Promise.resolve(mockTargetAgent);
|
||||
throw new Error(`Unknown agent: ${id}`);
|
||||
});
|
||||
|
||||
const mockConversationRetrieve = mock(() =>
|
||||
Promise.resolve(mockConversation),
|
||||
);
|
||||
|
||||
const mockMessageCreate = mock(() =>
|
||||
Promise.resolve(
|
||||
createMockStream([
|
||||
{ message_type: "reasoning_message", content: "thinking..." },
|
||||
// No assistant message - agent didn't respond with text
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const mockClient = {
|
||||
agents: {
|
||||
retrieve: mockAgentRetrieve,
|
||||
},
|
||||
conversations: {
|
||||
retrieve: mockConversationRetrieve,
|
||||
messages: {
|
||||
create: mockMessageCreate,
|
||||
},
|
||||
},
|
||||
} as unknown as Letta;
|
||||
|
||||
const result = await continueConversation(mockClient, {
|
||||
conversationId: mockConversation.id,
|
||||
message: "Hello?",
|
||||
});
|
||||
|
||||
expect(result.response).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user