feat: add from af cli flag (#195)
This commit is contained in:
55
src/agent/import.ts
Normal file
55
src/agent/import.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Import an agent from an AgentFile (.af) template
|
||||
*/
|
||||
import { createReadStream } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
|
||||
import { getClient } from "./client";
|
||||
import { getModelUpdateArgs } from "./model";
|
||||
import { linkToolsToAgent, updateAgentLLMConfig } from "./modify";
|
||||
|
||||
export interface ImportAgentOptions {
|
||||
filePath: string;
|
||||
modelOverride?: string;
|
||||
stripMessages?: boolean;
|
||||
}
|
||||
|
||||
export interface ImportAgentResult {
|
||||
agent: AgentState;
|
||||
}
|
||||
|
||||
export async function importAgentFromFile(
|
||||
options: ImportAgentOptions,
|
||||
): Promise<ImportAgentResult> {
|
||||
const client = await getClient();
|
||||
const resolvedPath = resolve(options.filePath);
|
||||
|
||||
// Create a file stream for the API (compatible with Node.js and Bun)
|
||||
const file = createReadStream(resolvedPath);
|
||||
|
||||
// Import the agent via API
|
||||
const importResponse = await client.agents.importFile({
|
||||
file: file,
|
||||
strip_messages: options.stripMessages ?? true,
|
||||
override_existing_tools: false,
|
||||
});
|
||||
|
||||
if (!importResponse.agent_ids || importResponse.agent_ids.length === 0) {
|
||||
throw new Error("Import failed: no agent IDs returned");
|
||||
}
|
||||
|
||||
const agentId = importResponse.agent_ids[0] as string;
|
||||
let agent = await client.agents.retrieve(agentId);
|
||||
|
||||
// Override model if specified
|
||||
if (options.modelOverride) {
|
||||
const updateArgs = getModelUpdateArgs(options.modelOverride);
|
||||
await updateAgentLLMConfig(agentId, options.modelOverride, updateArgs);
|
||||
agent = await client.agents.retrieve(agentId);
|
||||
}
|
||||
|
||||
// Link Letta Code tools to the imported agent
|
||||
await linkToolsToAgent(agentId);
|
||||
|
||||
return { agent };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/cli/App.tsx
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { APIUserAbortError } from "@letta-ai/letta-client/core/error";
|
||||
import type {
|
||||
AgentState,
|
||||
@@ -239,6 +239,7 @@ export default function App({
|
||||
| "upserting"
|
||||
| "linking"
|
||||
| "unlinking"
|
||||
| "importing"
|
||||
| "initializing"
|
||||
| "checking"
|
||||
| "ready";
|
||||
@@ -1995,7 +1996,7 @@ export default function App({
|
||||
const client = await getClient();
|
||||
const fileContent = await client.agents.exportFile(agentId);
|
||||
const fileName = `${agentId}.af`;
|
||||
await Bun.write(fileName, JSON.stringify(fileContent, null, 2));
|
||||
writeFileSync(fileName, JSON.stringify(fileContent, null, 2));
|
||||
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
|
||||
@@ -12,6 +12,7 @@ type LoadingState =
|
||||
| "upserting"
|
||||
| "linking"
|
||||
| "unlinking"
|
||||
| "importing"
|
||||
| "initializing"
|
||||
| "checking"
|
||||
| "ready";
|
||||
@@ -189,6 +190,8 @@ function getStatusMessage(
|
||||
return "Attaching Letta Code tools...";
|
||||
case "unlinking":
|
||||
return "Removing Letta Code tools...";
|
||||
case "importing":
|
||||
return "Importing agent from template...";
|
||||
case "checking":
|
||||
return "Checking for pending approvals...";
|
||||
default:
|
||||
|
||||
@@ -66,6 +66,7 @@ export async function handleHeadlessCommand(
|
||||
sleeptime: { type: "boolean" },
|
||||
"init-blocks": { type: "string" },
|
||||
"base-tools": { type: "string" },
|
||||
"from-af": { type: "string" },
|
||||
},
|
||||
strict: false,
|
||||
allowPositionals: true,
|
||||
@@ -108,6 +109,23 @@ export async function handleHeadlessCommand(
|
||||
const initBlocksRaw = values["init-blocks"] as string | undefined;
|
||||
const baseToolsRaw = values["base-tools"] as string | undefined;
|
||||
const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined;
|
||||
const fromAfFile = values["from-af"] as string | undefined;
|
||||
|
||||
// Validate --from-af flag
|
||||
if (fromAfFile) {
|
||||
if (specifiedAgentId) {
|
||||
console.error("Error: --from-af cannot be used with --agent");
|
||||
process.exit(1);
|
||||
}
|
||||
if (shouldContinue) {
|
||||
console.error("Error: --from-af cannot be used with --continue");
|
||||
process.exit(1);
|
||||
}
|
||||
if (forceNew) {
|
||||
console.error("Error: --from-af cannot be used with --new");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (initBlocksRaw && !forceNew) {
|
||||
console.error(
|
||||
@@ -149,8 +167,19 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: Try to use --agent specified ID
|
||||
if (specifiedAgentId) {
|
||||
// Priority 1: Import from AgentFile template
|
||||
if (fromAfFile) {
|
||||
const { importAgentFromFile } = await import("./agent/import");
|
||||
const result = await importAgentFromFile({
|
||||
filePath: fromAfFile,
|
||||
modelOverride: model,
|
||||
stripMessages: true,
|
||||
});
|
||||
agent = result.agent;
|
||||
}
|
||||
|
||||
// Priority 2: Try to use --agent specified ID
|
||||
if (!agent && specifiedAgentId) {
|
||||
try {
|
||||
agent = await client.agents.retrieve(specifiedAgentId);
|
||||
} catch (_error) {
|
||||
@@ -158,7 +187,7 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Check if --new flag was passed (skip all resume logic)
|
||||
// Priority 3: Check if --new flag was passed (skip all resume logic)
|
||||
if (!agent && forceNew) {
|
||||
const updateArgs = getModelUpdateArgs(model);
|
||||
const result = await createAgent(
|
||||
@@ -177,7 +206,7 @@ export async function handleHeadlessCommand(
|
||||
agent = result.agent;
|
||||
}
|
||||
|
||||
// Priority 3: Try to resume from project settings (.letta/settings.local.json)
|
||||
// Priority 4: Try to resume from project settings (.letta/settings.local.json)
|
||||
if (!agent) {
|
||||
await settingsManager.loadLocalProjectSettings();
|
||||
const localProjectSettings = settingsManager.getLocalProjectSettings();
|
||||
@@ -192,7 +221,7 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: Try to reuse global lastAgent if --continue flag is passed
|
||||
// Priority 5: Try to reuse global lastAgent if --continue flag is passed
|
||||
if (!agent && shouldContinue && settings.lastAgent) {
|
||||
try {
|
||||
agent = await client.agents.retrieve(settings.lastAgent);
|
||||
@@ -203,7 +232,7 @@ export async function handleHeadlessCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 5: Create a new agent
|
||||
// Priority 6: Create a new agent
|
||||
if (!agent) {
|
||||
const updateArgs = getModelUpdateArgs(model);
|
||||
const result = await createAgent(
|
||||
|
||||
62
src/index.ts
62
src/index.ts
@@ -46,6 +46,7 @@ OPTIONS
|
||||
Default: text
|
||||
--skills <path> Custom path to skills directory (default: .skills in current directory)
|
||||
--sleeptime Enable sleeptime memory management (only for new agents)
|
||||
--from-af <path> Create agent from an AgentFile (.af) template
|
||||
|
||||
|
||||
BEHAVIOR
|
||||
@@ -144,6 +145,7 @@ async function main() {
|
||||
link: { type: "boolean" },
|
||||
unlink: { type: "boolean" },
|
||||
sleeptime: { type: "boolean" },
|
||||
"from-af": { type: "string" },
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
@@ -200,6 +202,7 @@ async function main() {
|
||||
const specifiedToolset = (values.toolset as string | undefined) ?? undefined;
|
||||
const skillsDirectory = (values.skills as string | undefined) ?? undefined;
|
||||
const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined;
|
||||
const fromAfFile = values["from-af"] as string | undefined;
|
||||
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
|
||||
|
||||
// --init-blocks only makes sense when creating a brand new agent
|
||||
@@ -270,6 +273,30 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate --from-af flag
|
||||
if (fromAfFile) {
|
||||
if (specifiedAgentId) {
|
||||
console.error("Error: --from-af cannot be used with --agent");
|
||||
process.exit(1);
|
||||
}
|
||||
if (shouldContinue) {
|
||||
console.error("Error: --from-af cannot be used with --continue");
|
||||
process.exit(1);
|
||||
}
|
||||
if (forceNew) {
|
||||
console.error("Error: --from-af cannot be used with --new");
|
||||
process.exit(1);
|
||||
}
|
||||
// Verify file exists
|
||||
const { resolve } = await import("node:path");
|
||||
const { existsSync } = await import("node:fs");
|
||||
const resolvedPath = resolve(fromAfFile);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
console.error(`Error: AgentFile not found: ${resolvedPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
|
||||
const baseURL =
|
||||
@@ -434,6 +461,7 @@ async function main() {
|
||||
system,
|
||||
toolset,
|
||||
skillsDirectory,
|
||||
fromAfFile,
|
||||
}: {
|
||||
continueSession: boolean;
|
||||
forceNew: boolean;
|
||||
@@ -445,12 +473,14 @@ async function main() {
|
||||
system?: string;
|
||||
toolset?: "codex" | "default" | "gemini";
|
||||
skillsDirectory?: string;
|
||||
fromAfFile?: string;
|
||||
}) {
|
||||
const [loadingState, setLoadingState] = useState<
|
||||
| "assembling"
|
||||
| "upserting"
|
||||
| "linking"
|
||||
| "unlinking"
|
||||
| "importing"
|
||||
| "initializing"
|
||||
| "checking"
|
||||
| "ready"
|
||||
@@ -569,8 +599,25 @@ async function main() {
|
||||
|
||||
let agent: AgentState | null = null;
|
||||
|
||||
// Priority 1: Try to use --agent specified ID
|
||||
if (agentIdArg) {
|
||||
// Priority 1: Import from AgentFile template
|
||||
if (fromAfFile) {
|
||||
setLoadingState("importing");
|
||||
const { importAgentFromFile } = await import("./agent/import");
|
||||
const result = await importAgentFromFile({
|
||||
filePath: fromAfFile,
|
||||
modelOverride: model,
|
||||
stripMessages: true,
|
||||
});
|
||||
agent = result.agent;
|
||||
setAgentProvenance({
|
||||
isNew: true,
|
||||
freshBlocks: true,
|
||||
blocks: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Priority 2: Try to use --agent specified ID
|
||||
if (!agent && agentIdArg) {
|
||||
try {
|
||||
agent = await client.agents.retrieve(agentIdArg);
|
||||
// console.log(`Using agent ${agentIdArg}...`);
|
||||
@@ -731,7 +778,15 @@ async function main() {
|
||||
}
|
||||
|
||||
init();
|
||||
}, [continueSession, forceNew, freshBlocks, agentIdArg, model, system]);
|
||||
}, [
|
||||
continueSession,
|
||||
forceNew,
|
||||
freshBlocks,
|
||||
agentIdArg,
|
||||
model,
|
||||
system,
|
||||
fromAfFile,
|
||||
]);
|
||||
|
||||
if (!agentId) {
|
||||
return React.createElement(App, {
|
||||
@@ -771,6 +826,7 @@ async function main() {
|
||||
system: specifiedSystem,
|
||||
toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined,
|
||||
skillsDirectory: skillsDirectory,
|
||||
fromAfFile: fromAfFile,
|
||||
}),
|
||||
{
|
||||
exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard
|
||||
|
||||
Reference in New Issue
Block a user