Merge branch 'main' of github.com:letta-ai/letta-code
This commit is contained in:
47
examples/send-image-simple.ts
Normal file
47
examples/send-image-simple.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Simplest possible example: Send an image to a Letta agent with streaming
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { sendMessageStream } from "../src/agent/message";
|
||||
|
||||
async function main() {
|
||||
const agentId = "agent-YOUR-AGENT-ID"; // Replace with your agent ID
|
||||
const imagePath = "./screenshot.png"; // Replace with your image path
|
||||
|
||||
// 1. Read and encode image
|
||||
const imageData = readFileSync(imagePath).toString("base64");
|
||||
|
||||
// 2. Send message with streaming
|
||||
const stream = await sendMessageStream(agentId, [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What's in this image?",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
mediaType: "image/png",
|
||||
data: imageData,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// 3. Print streaming response
|
||||
console.log("Agent response:");
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.messageType === "assistant_message") {
|
||||
process.stdout.write(chunk.content || "");
|
||||
}
|
||||
}
|
||||
console.log("\n");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
81
examples/send-image.ts
Normal file
81
examples/send-image.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Minimal example: Send an image to a Letta agent
|
||||
*
|
||||
* Usage:
|
||||
* bun examples/send-image.ts <agent-id> <image-path>
|
||||
*
|
||||
* Example:
|
||||
* bun examples/send-image.ts agent-123abc screenshot.png
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { getClient } from "../src/agent/client";
|
||||
import { sendMessageStream } from "../src/agent/message";
|
||||
|
||||
async function main() {
|
||||
const agentId = process.argv[2];
|
||||
const imagePath = process.argv[3];
|
||||
|
||||
if (!agentId || !imagePath) {
|
||||
console.error("Usage: bun send-image.ts <agent-id> <image-path>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 1: Read image file and convert to base64
|
||||
const imageBuffer = readFileSync(imagePath);
|
||||
const base64Data = imageBuffer.toString("base64");
|
||||
|
||||
// Determine media type from file extension
|
||||
const ext = imagePath.toLowerCase();
|
||||
const mediaType = ext.endsWith(".png")
|
||||
? "image/png"
|
||||
: ext.endsWith(".jpg") || ext.endsWith(".jpeg")
|
||||
? "image/jpeg"
|
||||
: ext.endsWith(".gif")
|
||||
? "image/gif"
|
||||
: ext.endsWith(".webp")
|
||||
? "image/webp"
|
||||
: "image/jpeg"; // default
|
||||
|
||||
console.log(`Sending ${imagePath} to agent ${agentId}...`);
|
||||
|
||||
// Step 2: Create message content with text and image
|
||||
const messageContent = [
|
||||
{ type: "text" as const, text: "What do you see in this image?" },
|
||||
{
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
mediaType,
|
||||
data: base64Data,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Step 3: Send message to agent
|
||||
const stream = await sendMessageStream(agentId, [
|
||||
{
|
||||
role: "user",
|
||||
content: messageContent,
|
||||
},
|
||||
]);
|
||||
|
||||
// Step 4: Process the streaming response
|
||||
console.log("\nAgent response:");
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.messageType === "assistant_message") {
|
||||
process.stdout.write(chunk.content || "");
|
||||
} else if (chunk.messageType === "reasoning_message") {
|
||||
// Optionally show internal monologue
|
||||
// console.log(`[thinking] ${chunk.reasoning}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@letta-ai/letta-code",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.19",
|
||||
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Utilities for modifying agent configuration
|
||||
|
||||
import type { LlmConfig } from "@letta-ai/letta-client/resources/models/models";
|
||||
import { getToolNames } from "../tools/manager";
|
||||
import { getClient } from "./client";
|
||||
|
||||
/**
|
||||
@@ -44,3 +45,149 @@ export async function updateAgentLLMConfig(
|
||||
|
||||
return finalConfig;
|
||||
}
|
||||
|
||||
export interface LinkResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
addedCount?: number;
|
||||
}
|
||||
|
||||
export interface UnlinkResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
removedCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach all Letta Code tools to an agent.
|
||||
*
|
||||
* @param agentId - The agent ID
|
||||
* @returns Result with success status and message
|
||||
*/
|
||||
export async function linkToolsToAgent(agentId: string): Promise<LinkResult> {
|
||||
try {
|
||||
const client = await getClient();
|
||||
|
||||
// Get ALL agent tools from agent state
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const currentTools = agent.tools || [];
|
||||
const currentToolIds = currentTools
|
||||
.map((t) => t.id)
|
||||
.filter((id): id is string => typeof id === "string");
|
||||
const currentToolNames = new Set(
|
||||
currentTools
|
||||
.map((t) => t.name)
|
||||
.filter((name): name is string => typeof name === "string"),
|
||||
);
|
||||
|
||||
// Get Letta Code tool names
|
||||
const lettaCodeToolNames = getToolNames();
|
||||
|
||||
// Find tools to add (tools that aren't already attached)
|
||||
const toolsToAdd = lettaCodeToolNames.filter(
|
||||
(name) => !currentToolNames.has(name),
|
||||
);
|
||||
|
||||
if (toolsToAdd.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: "All Letta Code tools already attached",
|
||||
addedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Look up tool IDs from global tool list
|
||||
const toolsToAddIds: string[] = [];
|
||||
for (const toolName of toolsToAdd) {
|
||||
const tools = await client.tools.list({ name: toolName });
|
||||
const tool = tools[0];
|
||||
if (tool?.id) {
|
||||
toolsToAddIds.push(tool.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine current tools with new tools
|
||||
const newToolIds = [...currentToolIds, ...toolsToAddIds];
|
||||
|
||||
// Get current tool_rules and add requires_approval rules for new tools
|
||||
const currentToolRules = agent.tool_rules || [];
|
||||
const newToolRules = [
|
||||
...currentToolRules,
|
||||
...toolsToAdd.map((toolName) => ({
|
||||
tool_name: toolName,
|
||||
type: "requires_approval" as const,
|
||||
prompt_template: null,
|
||||
})),
|
||||
];
|
||||
|
||||
await client.agents.modify(agentId, {
|
||||
tool_ids: newToolIds,
|
||||
tool_rules: newToolRules,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Attached ${toolsToAddIds.length} Letta Code tool(s) to agent`,
|
||||
addedCount: toolsToAddIds.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all Letta Code tools from an agent.
|
||||
*
|
||||
* @param agentId - The agent ID
|
||||
* @returns Result with success status and message
|
||||
*/
|
||||
export async function unlinkToolsFromAgent(
|
||||
agentId: string,
|
||||
): Promise<UnlinkResult> {
|
||||
try {
|
||||
const client = await getClient();
|
||||
|
||||
// Get ALL agent tools from agent state (not tools.list which may be incomplete)
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
const allTools = agent.tools || [];
|
||||
const lettaCodeToolNames = new Set(getToolNames());
|
||||
|
||||
// Filter out Letta Code tools, keep everything else
|
||||
const remainingTools = allTools.filter(
|
||||
(t) => t.name && !lettaCodeToolNames.has(t.name),
|
||||
);
|
||||
const removedCount = allTools.length - remainingTools.length;
|
||||
|
||||
// Extract IDs from remaining tools (filter out any undefined IDs)
|
||||
const remainingToolIds = remainingTools
|
||||
.map((t) => t.id)
|
||||
.filter((id): id is string => typeof id === "string");
|
||||
|
||||
// Remove approval rules for Letta Code tools being unlinked
|
||||
const currentToolRules = agent.tool_rules || [];
|
||||
const remainingToolRules = currentToolRules.filter(
|
||||
(rule) =>
|
||||
rule.type !== "requires_approval" ||
|
||||
!lettaCodeToolNames.has(rule.tool_name),
|
||||
);
|
||||
|
||||
await client.agents.modify(agentId, {
|
||||
tool_ids: remainingToolIds,
|
||||
tool_rules: remainingToolRules,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Removed ${removedCount} Letta Code tool(s) from agent`,
|
||||
removedCount,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ export async function refreshAccessToken(
|
||||
grant_type: "refresh_token",
|
||||
client_id: OAUTH_CONFIG.clientId,
|
||||
refresh_token: refreshToken,
|
||||
refresh_token_mode: "new",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -154,6 +155,39 @@ export async function refreshAccessToken(
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a refresh token (logout)
|
||||
*/
|
||||
export async function revokeToken(refreshToken: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${OAUTH_CONFIG.authBaseUrl}/api/oauth/revoke`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
client_id: OAUTH_CONFIG.clientId,
|
||||
token: refreshToken,
|
||||
token_type_hint: "refresh_token",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
// OAuth 2.0 revoke endpoint should return 200 even if token is already invalid
|
||||
if (!response.ok) {
|
||||
const error = (await response.json()) as OAuthError;
|
||||
console.error(
|
||||
`Warning: Failed to revoke token: ${error.error_description || error.error}`,
|
||||
);
|
||||
// Don't throw - we still want to clear local credentials
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Warning: Failed to revoke token:", error);
|
||||
// Don't throw - we still want to clear local credentials
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate credentials by checking health endpoint
|
||||
* Validate credentials by checking an authenticated endpoint
|
||||
* Uses SDK's agents.list() which requires valid authentication
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { getResumeData } from "../agent/check-approval";
|
||||
import { getClient } from "../agent/client";
|
||||
import { sendMessageStream } from "../agent/message";
|
||||
import { linkToolsToAgent, unlinkToolsFromAgent } from "../agent/modify";
|
||||
import { SessionStats } from "../agent/stats";
|
||||
import type { ApprovalContext } from "../permissions/analyzer";
|
||||
import { permissionMode } from "../permissions/mode";
|
||||
@@ -111,6 +112,8 @@ export default function App({
|
||||
loadingState?:
|
||||
| "assembling"
|
||||
| "upserting"
|
||||
| "linking"
|
||||
| "unlinking"
|
||||
| "initializing"
|
||||
| "checking"
|
||||
| "ready";
|
||||
@@ -669,7 +672,7 @@ export default function App({
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "Clearing credentials...",
|
||||
output: "Logging out...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
@@ -680,6 +683,14 @@ export default function App({
|
||||
try {
|
||||
const { settingsManager } = await import("../settings-manager");
|
||||
const currentSettings = settingsManager.getSettings();
|
||||
|
||||
// Revoke refresh token on server if we have one
|
||||
if (currentSettings.refreshToken) {
|
||||
const { revokeToken } = await import("../auth/oauth");
|
||||
await revokeToken(currentSettings.refreshToken);
|
||||
}
|
||||
|
||||
// Clear local credentials
|
||||
const newEnv = { ...currentSettings.env };
|
||||
delete newEnv.LETTA_API_KEY;
|
||||
// Note: LETTA_BASE_URL is intentionally NOT deleted from settings
|
||||
@@ -829,6 +840,92 @@ export default function App({
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /link command - attach Letta Code tools
|
||||
if (msg.trim() === "/link") {
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "Attaching Letta Code tools to agent...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
setCommandRunning(true);
|
||||
|
||||
try {
|
||||
const result = await linkToolsToAgent(agentId);
|
||||
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: result.message,
|
||||
phase: "finished",
|
||||
success: result.success,
|
||||
});
|
||||
refreshDerived();
|
||||
} catch (error) {
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
}
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Special handling for /unlink command - remove Letta Code tools
|
||||
if (msg.trim() === "/unlink") {
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: "Removing Letta Code tools from agent...",
|
||||
phase: "running",
|
||||
});
|
||||
buffersRef.current.order.push(cmdId);
|
||||
refreshDerived();
|
||||
|
||||
setCommandRunning(true);
|
||||
|
||||
try {
|
||||
const result = await unlinkToolsFromAgent(agentId);
|
||||
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: result.message,
|
||||
phase: "finished",
|
||||
success: result.success,
|
||||
});
|
||||
refreshDerived();
|
||||
} catch (error) {
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: cmdId,
|
||||
input: msg,
|
||||
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
}
|
||||
return { submitted: true };
|
||||
}
|
||||
|
||||
// Immediately add command to transcript with "running" phase
|
||||
const cmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
|
||||
@@ -50,6 +50,20 @@ export const commands: Record<string, Command> = {
|
||||
return "Clearing credentials...";
|
||||
},
|
||||
},
|
||||
"/link": {
|
||||
desc: "Attach Letta Code tools to current agent",
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx to access agent ID and client
|
||||
return "Attaching tools...";
|
||||
},
|
||||
},
|
||||
"/unlink": {
|
||||
desc: "Remove Letta Code tools from current agent",
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx to access agent ID and client
|
||||
return "Removing tools...";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,9 +34,10 @@ export function CommandPreview({
|
||||
paddingX={1}
|
||||
>
|
||||
{commandList.map((item) => (
|
||||
<Box key={item.cmd} justifyContent="space-between" width={40}>
|
||||
<Text>{item.cmd}</Text>
|
||||
<Text dimColor>{item.desc}</Text>
|
||||
<Box key={item.cmd}>
|
||||
<Text>
|
||||
{item.cmd.padEnd(15)} <Text dimColor>{item.desc}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
{showAgentUrl && (
|
||||
|
||||
@@ -498,9 +498,7 @@ export function Input({
|
||||
) : (
|
||||
<Text dimColor>Press / for commands or @ for files</Text>
|
||||
)}
|
||||
<Link url="https://discord.gg/letta">
|
||||
<Text dimColor>Discord</Text>
|
||||
</Link>
|
||||
<Text dimColor>https://discord.gg/letta</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { colors } from "./colors";
|
||||
type LoadingState =
|
||||
| "assembling"
|
||||
| "upserting"
|
||||
| "linking"
|
||||
| "unlinking"
|
||||
| "initializing"
|
||||
| "checking"
|
||||
| "ready";
|
||||
@@ -82,6 +84,12 @@ export function WelcomeScreen({
|
||||
if (loadingState === "upserting") {
|
||||
return "Upserting tools...";
|
||||
}
|
||||
if (loadingState === "linking") {
|
||||
return "Attaching Letta Code tools...";
|
||||
}
|
||||
if (loadingState === "unlinking") {
|
||||
return "Removing Letta Code tools...";
|
||||
}
|
||||
if (loadingState === "checking") {
|
||||
return "Checking for pending approvals...";
|
||||
}
|
||||
|
||||
47
src/index.ts
47
src/index.ts
@@ -91,6 +91,8 @@ async function main() {
|
||||
"permission-mode": { type: "string" },
|
||||
yolo: { type: "boolean" },
|
||||
"output-format": { type: "string" },
|
||||
link: { type: "boolean" },
|
||||
unlink: { type: "boolean" },
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
@@ -233,6 +235,21 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --link and --unlink flags (modify tools before starting session)
|
||||
const shouldLink = values.link as boolean | undefined;
|
||||
const shouldUnlink = values.unlink as boolean | undefined;
|
||||
|
||||
// Validate --link/--unlink flags require --agent
|
||||
if (shouldLink || shouldUnlink) {
|
||||
if (!specifiedAgentId) {
|
||||
console.error(
|
||||
`Error: --${shouldLink ? "link" : "unlink"} requires --agent <id>`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// Implementation is in InteractiveSession init()
|
||||
}
|
||||
|
||||
if (isHeadless) {
|
||||
// For headless mode, load tools synchronously
|
||||
await loadTools();
|
||||
@@ -263,7 +280,13 @@ async function main() {
|
||||
model?: string;
|
||||
}) {
|
||||
const [loadingState, setLoadingState] = useState<
|
||||
"assembling" | "upserting" | "initializing" | "checking" | "ready"
|
||||
| "assembling"
|
||||
| "upserting"
|
||||
| "linking"
|
||||
| "unlinking"
|
||||
| "initializing"
|
||||
| "checking"
|
||||
| "ready"
|
||||
>("assembling");
|
||||
const [agentId, setAgentId] = useState<string | null>(null);
|
||||
const [agentState, setAgentState] = useState<AgentState | null>(null);
|
||||
@@ -279,6 +302,28 @@ async function main() {
|
||||
const client = await getClient();
|
||||
await upsertToolsToServer(client);
|
||||
|
||||
// Handle --link/--unlink after upserting tools
|
||||
if (shouldLink || shouldUnlink) {
|
||||
if (!agentIdArg) {
|
||||
console.error("Error: --link/--unlink requires --agent <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
setLoadingState(shouldLink ? "linking" : "unlinking");
|
||||
const { linkToolsToAgent, unlinkToolsFromAgent } = await import(
|
||||
"./agent/modify"
|
||||
);
|
||||
|
||||
const result = shouldLink
|
||||
? await linkToolsToAgent(agentIdArg)
|
||||
: await unlinkToolsFromAgent(agentIdArg);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(`✗ ${result.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingState("initializing");
|
||||
const { createAgent } = await import("./agent/create");
|
||||
const { getModelUpdateArgs } = await import("./agent/model");
|
||||
|
||||
205
src/tests/agent/link-unlink.test.ts
Normal file
205
src/tests/agent/link-unlink.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { Letta } from "@letta-ai/letta-client";
|
||||
import { linkToolsToAgent, unlinkToolsFromAgent } from "../../agent/modify";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
import { getToolNames, loadTools } from "../../tools/manager";
|
||||
|
||||
// Skip these integration tests if LETTA_API_KEY is not set
|
||||
const shouldSkip = !process.env.LETTA_API_KEY;
|
||||
const describeOrSkip = shouldSkip ? describe.skip : describe;
|
||||
|
||||
describeOrSkip("Link/Unlink Tools", () => {
|
||||
let client: Letta;
|
||||
let testAgentId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Initialize settings and load tools
|
||||
await settingsManager.initialize();
|
||||
await loadTools();
|
||||
|
||||
// Create a test agent
|
||||
const apiKey = process.env.LETTA_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("LETTA_API_KEY required for tests");
|
||||
}
|
||||
|
||||
client = new Letta({ apiKey });
|
||||
|
||||
const agent = await client.agents.create({
|
||||
model: "openai/gpt-4o-mini",
|
||||
embedding: "openai/text-embedding-3-small",
|
||||
memory_blocks: [
|
||||
{ label: "human", value: "Test user" },
|
||||
{ label: "persona", value: "Test agent" },
|
||||
],
|
||||
tools: [],
|
||||
});
|
||||
|
||||
testAgentId = agent.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup: delete test agent
|
||||
if (testAgentId) {
|
||||
try {
|
||||
await client.agents.delete(testAgentId);
|
||||
} catch (_error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("linkToolsToAgent attaches all Letta Code tools", async () => {
|
||||
// Reset: ensure tools are not already attached
|
||||
await unlinkToolsFromAgent(testAgentId);
|
||||
|
||||
const result = await linkToolsToAgent(testAgentId);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.addedCount).toBeGreaterThan(0);
|
||||
|
||||
// Verify tools were attached
|
||||
const agent = await client.agents.retrieve(testAgentId);
|
||||
const toolNames = agent.tools?.map((t) => t.name) || [];
|
||||
const lettaCodeTools = getToolNames();
|
||||
|
||||
for (const toolName of lettaCodeTools) {
|
||||
expect(toolNames).toContain(toolName);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("linkToolsToAgent adds approval rules for all tools", async () => {
|
||||
// First unlink to reset
|
||||
await unlinkToolsFromAgent(testAgentId);
|
||||
|
||||
// Link tools
|
||||
await linkToolsToAgent(testAgentId);
|
||||
|
||||
// Verify approval rules were added
|
||||
const agent = await client.agents.retrieve(testAgentId);
|
||||
const approvalRules = agent.tool_rules?.filter(
|
||||
(rule) => rule.type === "requires_approval",
|
||||
);
|
||||
|
||||
const lettaCodeTools = getToolNames();
|
||||
expect(approvalRules?.length).toBe(lettaCodeTools.length);
|
||||
|
||||
// Check all Letta Code tools have approval rules
|
||||
const rulesToolNames = approvalRules?.map((r) => r.tool_name) || [];
|
||||
for (const toolName of lettaCodeTools) {
|
||||
expect(rulesToolNames).toContain(toolName);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("linkToolsToAgent returns success when tools already attached", async () => {
|
||||
// Reset and link once
|
||||
await unlinkToolsFromAgent(testAgentId);
|
||||
await linkToolsToAgent(testAgentId);
|
||||
|
||||
// Link again
|
||||
const result = await linkToolsToAgent(testAgentId);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.addedCount).toBe(0);
|
||||
expect(result.message).toContain("already attached");
|
||||
}, 30000);
|
||||
|
||||
test("unlinkToolsFromAgent removes all Letta Code tools", async () => {
|
||||
// First link tools
|
||||
await linkToolsToAgent(testAgentId);
|
||||
|
||||
// Then unlink
|
||||
const result = await unlinkToolsFromAgent(testAgentId);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.removedCount).toBeGreaterThan(0);
|
||||
|
||||
// Verify tools were removed
|
||||
const agent = await client.agents.retrieve(testAgentId);
|
||||
const toolNames = agent.tools?.map((t) => t.name) || [];
|
||||
const lettaCodeTools = getToolNames();
|
||||
|
||||
for (const toolName of lettaCodeTools) {
|
||||
expect(toolNames).not.toContain(toolName);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("unlinkToolsFromAgent removes approval rules", async () => {
|
||||
// First link tools
|
||||
await linkToolsToAgent(testAgentId);
|
||||
|
||||
// Then unlink
|
||||
await unlinkToolsFromAgent(testAgentId);
|
||||
|
||||
// Verify approval rules were removed
|
||||
const agent = await client.agents.retrieve(testAgentId);
|
||||
const approvalRules = agent.tool_rules?.filter(
|
||||
(rule) => rule.type === "requires_approval",
|
||||
);
|
||||
|
||||
const lettaCodeTools = new Set(getToolNames());
|
||||
const remainingApprovalRules = approvalRules?.filter((r) =>
|
||||
lettaCodeTools.has(r.tool_name),
|
||||
);
|
||||
|
||||
expect(remainingApprovalRules?.length || 0).toBe(0);
|
||||
}, 30000);
|
||||
|
||||
test("unlinkToolsFromAgent preserves non-Letta-Code tools", async () => {
|
||||
// Link Letta Code tools
|
||||
await linkToolsToAgent(testAgentId);
|
||||
|
||||
// Attach memory tool
|
||||
const memoryTools = await client.tools.list({ name: "memory" });
|
||||
const memoryTool = memoryTools[0];
|
||||
if (memoryTool?.id) {
|
||||
await client.agents.tools.attach(memoryTool.id, {
|
||||
agent_id: testAgentId,
|
||||
});
|
||||
}
|
||||
|
||||
// Unlink Letta Code tools
|
||||
await unlinkToolsFromAgent(testAgentId);
|
||||
|
||||
// Verify memory tool is still there
|
||||
const agent = await client.agents.retrieve(testAgentId);
|
||||
const toolNames = agent.tools?.map((t) => t.name) || [];
|
||||
|
||||
expect(toolNames).toContain("memory");
|
||||
|
||||
// Verify Letta Code tools are gone
|
||||
const lettaCodeTools = getToolNames();
|
||||
for (const toolName of lettaCodeTools) {
|
||||
expect(toolNames).not.toContain(toolName);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test("unlinkToolsFromAgent preserves non-approval tool_rules", async () => {
|
||||
// Link tools
|
||||
await linkToolsToAgent(testAgentId);
|
||||
|
||||
// Add a continue_loop rule manually
|
||||
const agent = await client.agents.retrieve(testAgentId);
|
||||
const newToolRules = [
|
||||
...(agent.tool_rules || []),
|
||||
{
|
||||
tool_name: "memory",
|
||||
type: "continue_loop" as const,
|
||||
prompt_template: "Test rule",
|
||||
},
|
||||
];
|
||||
|
||||
await client.agents.modify(testAgentId, { tool_rules: newToolRules });
|
||||
|
||||
// Unlink Letta Code tools
|
||||
await unlinkToolsFromAgent(testAgentId);
|
||||
|
||||
// Verify continue_loop rule is still there
|
||||
const updatedAgent = await client.agents.retrieve(testAgentId);
|
||||
const continueLoopRules = updatedAgent.tool_rules?.filter(
|
||||
(r) => r.type === "continue_loop" && r.tool_name === "memory",
|
||||
);
|
||||
|
||||
expect(continueLoopRules?.length).toBe(1);
|
||||
}, 30000);
|
||||
});
|
||||
60
test-image-send.ts
Normal file
60
test-image-send.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createAgent } from "./src/agent/create";
|
||||
import { sendMessageStream } from "./src/agent/message";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
async function main() {
|
||||
// Create a simple test image (1x1 red PNG)
|
||||
const testImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
|
||||
const testImagePath = "/tmp/test.png";
|
||||
writeFileSync(testImagePath, Buffer.from(testImageBase64, "base64"));
|
||||
console.log("Created test image at", testImagePath);
|
||||
|
||||
// Create agent
|
||||
console.log("\nCreating test agent...");
|
||||
const agent = await createAgent("image-test-agent");
|
||||
console.log("Agent created:", agent.id);
|
||||
|
||||
// Read image
|
||||
const imageData = readFileSync(testImagePath).toString("base64");
|
||||
|
||||
// Send message with image
|
||||
console.log("\nSending image to agent...");
|
||||
const stream = await sendMessageStream(agent.id, [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What do you see in this image?",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/png",
|
||||
data: imageData,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Print response
|
||||
console.log("\nAgent response:");
|
||||
let fullResponse = "";
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.message_type === "assistant_message" && chunk.content) {
|
||||
fullResponse += chunk.content;
|
||||
process.stdout.write(chunk.content);
|
||||
}
|
||||
}
|
||||
if (!fullResponse) {
|
||||
console.log("(no assistant message received)");
|
||||
}
|
||||
console.log("\n\n✅ Done!");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user