feat(FN-1449): complete agent UI field parity with Agent Companies
This change aligns agent editing surfaces across dashboard UI, API payloads, and import/template flows so every first-class editable agent field is round-trippable. Backend changes: - Add memory and bundleConfig fields to POST/PATCH /api/agents routes - Fix agent-companies-parser to use first-class fields (title, icon, role, reportsTo, instructionsText) instead of metadata fallbacks - Update import dry-run preview to show more manifest fields UI changes: - Enable identity field editing (name, title, icon, role, reportsTo) in AgentDetailView ConfigTab - Add instruction bundle configuration (mode, entry file, files, external path) to ConfigTab - Add memory field to NewAgentDialog - Fix AI generation mapping to preserve systemPrompt as instructionsText - Update AgentImportModal preview to show icon, reportsTo, and instructions Test fixes: - Update parser tests for new first-class field behavior - Add MissionExecutionLoop mock for serve tests Documentation: - Add agent field parity matrix to docs/agents.md - Create changeset for @gsxdsm/fusion
This commit is contained in:
@@ -344,6 +344,12 @@ vi.mock("@fusion/engine", () => ({
|
||||
StuckTaskDetector: mocks.stuckDetectorCtor,
|
||||
SelfHealingManager: mocks.selfHealingCtor,
|
||||
MissionAutopilot: mocks.missionAutopilotCtor,
|
||||
MissionExecutionLoop: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
tick: vi.fn().mockResolvedValue(undefined),
|
||||
recoverActiveMissions: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
createAiPromptExecutor: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue("ok")),
|
||||
HeartbeatMonitor: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
|
||||
@@ -357,10 +357,9 @@ name: Zip CEO
|
||||
name: "CEO",
|
||||
role: "custom",
|
||||
title: "Chief Executive Officer",
|
||||
instructionsText: "Lead strategy",
|
||||
metadata: {
|
||||
instructions: "Lead strategy",
|
||||
skills: ["review"],
|
||||
reportsTo: null,
|
||||
sources: [{ kind: "git", repo: "acme/repo" }],
|
||||
},
|
||||
});
|
||||
@@ -391,6 +390,45 @@ name: Zip CEO
|
||||
const input = agentManifestToAgentCreateInput({ name: "Generalist" });
|
||||
expect(input.role).toBe("custom");
|
||||
});
|
||||
|
||||
it("maps manifest icon to first-class field", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Bot",
|
||||
icon: "🤖",
|
||||
role: "executor",
|
||||
});
|
||||
|
||||
expect(input).toEqual({
|
||||
name: "Bot",
|
||||
role: "executor",
|
||||
icon: "🤖",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps manifest reportsTo to first-class field", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Worker",
|
||||
reportsTo: "manager-001",
|
||||
});
|
||||
|
||||
expect(input).toEqual({
|
||||
name: "Worker",
|
||||
role: "custom",
|
||||
reportsTo: "manager-001",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps manifest role to first-class field", () => {
|
||||
const input = agentManifestToAgentCreateInput({
|
||||
name: "Reviewer",
|
||||
role: "reviewer",
|
||||
});
|
||||
|
||||
expect(input).toEqual({
|
||||
name: "Reviewer",
|
||||
role: "reviewer",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapRoleToCapability", () => {
|
||||
|
||||
@@ -284,25 +284,29 @@ export async function parseCompanyArchive(archivePath: string): Promise<AgentCom
|
||||
export function agentManifestToAgentCreateInput(agent: AgentManifest): AgentCreateInput {
|
||||
const metadata: Record<string, unknown> = {};
|
||||
|
||||
if (typeof agent.instructionBody === "string") {
|
||||
metadata.instructions = agent.instructionBody;
|
||||
}
|
||||
// Store skills and metadata sources in metadata (skills is not a first-class field)
|
||||
if (Array.isArray(agent.skills) && agent.skills.length > 0) {
|
||||
metadata.skills = agent.skills;
|
||||
}
|
||||
if (agent.reportsTo !== undefined) {
|
||||
metadata.reportsTo = agent.reportsTo;
|
||||
}
|
||||
if (Array.isArray(agent.metadata?.sources) && agent.metadata.sources.length > 0) {
|
||||
metadata.sources = agent.metadata.sources;
|
||||
}
|
||||
|
||||
return {
|
||||
name: agent.name,
|
||||
role: mapRoleToCapability("custom"),
|
||||
role: agent.role ? mapRoleToCapability(agent.role) : mapRoleToCapability("custom"),
|
||||
...(typeof agent.title === "string" && agent.title.trim().length > 0
|
||||
? { title: agent.title }
|
||||
: {}),
|
||||
...(typeof agent.icon === "string" && agent.icon.trim().length > 0
|
||||
? { icon: agent.icon.trim() }
|
||||
: {}),
|
||||
...(typeof agent.reportsTo === "string" && agent.reportsTo.trim().length > 0
|
||||
? { reportsTo: agent.reportsTo.trim() }
|
||||
: {}),
|
||||
...(typeof agent.instructionBody === "string" && agent.instructionBody.trim().length > 0
|
||||
? { instructionsText: agent.instructionBody.trim() }
|
||||
: {}),
|
||||
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2165,6 +2165,13 @@ function ConfigTab({
|
||||
addToast: (message: string, type?: "success" | "error") => void;
|
||||
onSaved: () => Promise<void>;
|
||||
}) {
|
||||
// Identity field state
|
||||
const [nameValue, setNameValue] = useState(agent.name);
|
||||
const [roleValue, setRoleValue] = useState(agent.role);
|
||||
const [titleValue, setTitleValue] = useState(agent.title ?? "");
|
||||
const [iconValue, setIconValue] = useState(agent.icon ?? "");
|
||||
const [reportsToValue, setReportsToValue] = useState(agent.reportsTo ?? "");
|
||||
|
||||
// Local form state initialised from agent.metadata
|
||||
const [formValues, setFormValues] = useState<Record<string, string>>(() => {
|
||||
const initial: Record<string, string> = {};
|
||||
@@ -2218,6 +2225,12 @@ function ConfigTab({
|
||||
return initial;
|
||||
});
|
||||
|
||||
// Bundle config state
|
||||
const [bundleMode, setBundleMode] = useState<string>(agent.bundleConfig?.mode ?? "");
|
||||
const [bundleEntryFile, setBundleEntryFile] = useState(agent.bundleConfig?.entryFile ?? "AGENTS.md");
|
||||
const [bundleExternalPath, setBundleExternalPath] = useState(agent.bundleConfig?.externalPath ?? "");
|
||||
const [bundleFiles, setBundleFiles] = useState<string[]>(agent.bundleConfig?.files ?? []);
|
||||
|
||||
// Budget status for progress bar display
|
||||
const [budgetStatus, setBudgetStatus] = useState<AgentBudgetStatus | null>(null);
|
||||
const [isResettingBudget, setIsResettingBudget] = useState(false);
|
||||
@@ -2250,6 +2263,19 @@ function ConfigTab({
|
||||
|
||||
/** Detect whether any local value differs from the persisted metadata */
|
||||
const hasChanges = (() => {
|
||||
// Check identity fields
|
||||
if (nameValue !== agent.name) return true;
|
||||
if (roleValue !== agent.role) return true;
|
||||
if (titleValue !== (agent.title ?? "")) return true;
|
||||
if (iconValue !== (agent.icon ?? "")) return true;
|
||||
if (reportsToValue !== (agent.reportsTo ?? "")) return true;
|
||||
|
||||
// Check bundle config
|
||||
if (bundleMode !== (agent.bundleConfig?.mode ?? "")) return true;
|
||||
if (bundleEntryFile !== (agent.bundleConfig?.entryFile ?? "AGENTS.md")) return true;
|
||||
if (bundleExternalPath !== (agent.bundleConfig?.externalPath ?? "")) return true;
|
||||
if (JSON.stringify(bundleFiles) !== JSON.stringify(agent.bundleConfig?.files ?? [])) return true;
|
||||
|
||||
for (const field of ADVANCED_SETTINGS) {
|
||||
const current = formValues[field.key]?.trim() ?? "";
|
||||
const persisted = agent.metadata[field.key] !== undefined && agent.metadata[field.key] !== null
|
||||
@@ -2454,9 +2480,31 @@ function ConfigTab({
|
||||
delete newRuntimeConfig.budgetConfig;
|
||||
}
|
||||
|
||||
// Build bundleConfig payload — only include if mode is set
|
||||
let newBundleConfig: { mode: "managed" | "external"; entryFile: string; files: string[]; externalPath?: string } | undefined;
|
||||
if (bundleMode) {
|
||||
newBundleConfig = {
|
||||
mode: bundleMode as "managed" | "external",
|
||||
entryFile: bundleEntryFile || "AGENTS.md",
|
||||
files: bundleFiles.length > 0 ? bundleFiles : ["AGENTS.md"],
|
||||
};
|
||||
if (bundleMode === "external" && bundleExternalPath.trim()) {
|
||||
newBundleConfig.externalPath = bundleExternalPath.trim();
|
||||
}
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await updateAgent(agent.id, { metadata: newMetadata, runtimeConfig: newRuntimeConfig }, projectId);
|
||||
await updateAgent(agent.id, {
|
||||
name: nameValue.trim() || undefined,
|
||||
role: roleValue as any,
|
||||
title: titleValue.trim() || undefined,
|
||||
icon: iconValue.trim() || undefined,
|
||||
reportsTo: reportsToValue.trim() || undefined,
|
||||
metadata: newMetadata,
|
||||
runtimeConfig: newRuntimeConfig,
|
||||
bundleConfig: newBundleConfig,
|
||||
}, projectId);
|
||||
addToast("Settings saved", "success");
|
||||
setJustSaved(true);
|
||||
// Auto-hide the saved indicator after 3 seconds
|
||||
@@ -2479,19 +2527,24 @@ function ConfigTab({
|
||||
|
||||
<div className="config-fields">
|
||||
<div className="config-field">
|
||||
<label>Name</label>
|
||||
<label htmlFor="agent-name">Name</label>
|
||||
<input
|
||||
id="agent-name"
|
||||
type="text"
|
||||
className="input"
|
||||
defaultValue={agent.name}
|
||||
disabled
|
||||
value={nameValue}
|
||||
onChange={(e) => setNameValue(e.target.value)}
|
||||
/>
|
||||
<span className="config-hint">Name changes coming soon</span>
|
||||
</div>
|
||||
|
||||
<div className="config-field">
|
||||
<label>Role</label>
|
||||
<select className="select" defaultValue={agent.role} disabled>
|
||||
<label htmlFor="agent-role">Role</label>
|
||||
<select
|
||||
id="agent-role"
|
||||
className="select"
|
||||
value={roleValue}
|
||||
onChange={(e) => setRoleValue(e.target.value as any)}
|
||||
>
|
||||
<option value="triage">Triage</option>
|
||||
<option value="executor">Executor</option>
|
||||
<option value="reviewer">Reviewer</option>
|
||||
@@ -2499,7 +2552,42 @@ function ConfigTab({
|
||||
<option value="scheduler">Scheduler</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
<span className="config-hint">Role changes coming soon</span>
|
||||
</div>
|
||||
|
||||
<div className="config-field">
|
||||
<label htmlFor="agent-title">Title</label>
|
||||
<input
|
||||
id="agent-title"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="e.g. Senior Code Reviewer"
|
||||
value={titleValue}
|
||||
onChange={(e) => setTitleValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="config-field">
|
||||
<label htmlFor="agent-icon">Icon</label>
|
||||
<input
|
||||
id="agent-icon"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="e.g. 🤖"
|
||||
value={iconValue}
|
||||
onChange={(e) => setIconValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="config-field">
|
||||
<label htmlFor="agent-reports-to">Reports To</label>
|
||||
<input
|
||||
id="agent-reports-to"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="e.g. agent-001"
|
||||
value={reportsToValue}
|
||||
onChange={(e) => setReportsToValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2723,6 +2811,81 @@ function ConfigTab({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<h3>Instruction Bundle</h3>
|
||||
<p className="config-description">
|
||||
Configure the agent's instruction bundle. Leave empty to use inline instructions only.
|
||||
</p>
|
||||
|
||||
<div className="config-fields">
|
||||
<div className="config-field">
|
||||
<label htmlFor="bundle-mode">Bundle Mode</label>
|
||||
<select
|
||||
id="bundle-mode"
|
||||
className="select"
|
||||
value={bundleMode}
|
||||
onChange={(e) => setBundleMode(e.target.value)}
|
||||
>
|
||||
<option value="">None (use inline instructions)</option>
|
||||
<option value="managed">Managed (system-managed directory)</option>
|
||||
<option value="external">External (user-specified path)</option>
|
||||
</select>
|
||||
<span className="config-hint">
|
||||
{bundleMode === "managed" && "Files will be stored in a system-managed directory within .fusion/agents/"}
|
||||
{bundleMode === "external" && "Specify an external directory path for the instruction files"}
|
||||
{!bundleMode && "Select a mode to enable instruction bundling"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{bundleMode && (
|
||||
<>
|
||||
<div className="config-field">
|
||||
<label htmlFor="bundle-entry-file">Entry File</label>
|
||||
<input
|
||||
id="bundle-entry-file"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="AGENTS.md"
|
||||
value={bundleEntryFile}
|
||||
onChange={(e) => setBundleEntryFile(e.target.value)}
|
||||
/>
|
||||
<span className="config-hint">Primary instructions file name (default: AGENTS.md)</span>
|
||||
</div>
|
||||
|
||||
{bundleMode === "external" && (
|
||||
<div className="config-field">
|
||||
<label htmlFor="bundle-external-path">External Path</label>
|
||||
<input
|
||||
id="bundle-external-path"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="e.g. .fusion/agents/my-agent"
|
||||
value={bundleExternalPath}
|
||||
onChange={(e) => setBundleExternalPath(e.target.value)}
|
||||
/>
|
||||
<span className="config-hint">Absolute or relative path to the external directory</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="config-field">
|
||||
<label htmlFor="bundle-files">Files (comma-separated)</label>
|
||||
<input
|
||||
id="bundle-files"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="AGENTS.md, PROMPTS.md"
|
||||
value={bundleFiles.join(", ")}
|
||||
onChange={(e) => setBundleFiles(
|
||||
e.target.value.split(",").map(f => f.trim()).filter(Boolean)
|
||||
)}
|
||||
/>
|
||||
<span className="config-hint">List of file names in the bundle directory</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<h3>Advanced Settings</h3>
|
||||
<p className="config-description">
|
||||
|
||||
@@ -13,6 +13,9 @@ interface AgentPreview {
|
||||
name: string;
|
||||
role: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
reportsTo?: string;
|
||||
instructionsText?: string;
|
||||
skills?: string[];
|
||||
}
|
||||
|
||||
@@ -28,6 +31,9 @@ interface ImportResult {
|
||||
interface DirectoryAgentInput {
|
||||
name: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
role?: string;
|
||||
reportsTo?: string;
|
||||
skills?: string[];
|
||||
instructionBody?: string;
|
||||
}
|
||||
@@ -75,6 +81,9 @@ function parseDirectoryAgentManifest(content: string): DirectoryAgentInput {
|
||||
|
||||
if (key === "name") result.name = normalizedValue;
|
||||
if (key === "title") result.title = normalizedValue;
|
||||
if (key === "icon") result.icon = normalizedValue;
|
||||
if (key === "role") result.role = normalizedValue;
|
||||
if (key === "reportsTo") result.reportsTo = normalizedValue;
|
||||
}
|
||||
|
||||
if (!result.name) {
|
||||
@@ -391,16 +400,24 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
|
||||
<div className="agent-import-agent-list">
|
||||
{agents.map((agent, idx) => (
|
||||
<div key={idx} className="agent-import-agent-item">
|
||||
<span className="agent-import-agent-icon">🤖</span>
|
||||
<span className="agent-import-agent-icon">{agent.icon || "🤖"}</span>
|
||||
<div className="agent-import-agent-details">
|
||||
<span className="agent-import-agent-name">{agent.name}</span>
|
||||
<span className="agent-import-agent-meta">
|
||||
{agent.title && <span className="agent-import-agent-title">{agent.title} · </span>}
|
||||
<span className="agent-import-agent-role">{agent.role}</span>
|
||||
{agent.reportsTo && (
|
||||
<span className="agent-import-agent-reports"> · reports to {agent.reportsTo}</span>
|
||||
)}
|
||||
{agent.skills && agent.skills.length > 0 && (
|
||||
<span className="agent-import-agent-model"> · {agent.skills.join(", ")}</span>
|
||||
<span className="agent-import-agent-model"> · skills: {agent.skills.join(", ")}</span>
|
||||
)}
|
||||
</span>
|
||||
{agent.instructionsText && (
|
||||
<span className="agent-import-agent-instructions">
|
||||
{agent.instructionsText.slice(0, 100)}{agent.instructionsText.length > 100 ? "..." : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
const [instructionsPath, setInstructionsPath] = useState("");
|
||||
const [instructionsText, setInstructionsText] = useState("");
|
||||
const [soul, setSoul] = useState("");
|
||||
const [memory, setMemory] = useState("");
|
||||
const [runtimeConfig, setRuntimeConfig] = useState<RuntimeConfig>({
|
||||
model: "",
|
||||
thinkingLevel: "off",
|
||||
@@ -90,6 +91,8 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
setTitle(spec.description);
|
||||
setIcon(spec.icon);
|
||||
setRole(mappedRole);
|
||||
// Map generated systemPrompt to instructionsText
|
||||
setInstructionsText(spec.systemPrompt);
|
||||
setRuntimeConfig(c => ({
|
||||
...c,
|
||||
thinkingLevel: spec.thinkingLevel,
|
||||
@@ -147,6 +150,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
setInstructionsPath("");
|
||||
setInstructionsText("");
|
||||
setSoul("");
|
||||
setMemory("");
|
||||
setRuntimeConfig({ model: "", thinkingLevel: "off", maxTurns: 10 });
|
||||
setSelectedPresetId(null);
|
||||
setError(null);
|
||||
@@ -172,6 +176,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
...(instructionsPath.trim() ? { instructionsPath: instructionsPath.trim() } : {}),
|
||||
...(instructionsText.trim() ? { instructionsText: instructionsText.trim() } : {}),
|
||||
...(soul.trim() ? { soul: soul.trim() } : {}),
|
||||
...(memory.trim() ? { memory: memory.trim() } : {}),
|
||||
...(Object.keys(runtimeCfg).length > 0 ? { runtimeConfig: runtimeCfg } : {}),
|
||||
}, projectId);
|
||||
handleClose();
|
||||
@@ -300,6 +305,17 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
onChange={e => setSoul(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="agent-dialog-field">
|
||||
<label htmlFor="agent-memory">Memory <span className="agent-dialog-optional">(optional)</span></label>
|
||||
<textarea
|
||||
id="agent-memory"
|
||||
className="input"
|
||||
rows={2}
|
||||
placeholder="Per-agent memory — stores learnings and context the agent has gathered..."
|
||||
value={memory}
|
||||
onChange={e => setMemory(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="agent-dialog-field">
|
||||
<label htmlFor="agent-instructions-path">Instructions Path <span className="agent-dialog-optional">(optional)</span></label>
|
||||
<input
|
||||
|
||||
@@ -9034,6 +9034,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
instructionsPath,
|
||||
instructionsText,
|
||||
soul,
|
||||
memory,
|
||||
bundleConfig,
|
||||
} = req.body ?? {};
|
||||
|
||||
if (!name || typeof name !== "string") {
|
||||
@@ -9069,6 +9071,29 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
if (typeof soul === "string" && soul.length > 10000) {
|
||||
throw badRequest("soul must be at most 10,000 characters");
|
||||
}
|
||||
if (memory !== undefined && memory !== null && typeof memory !== "string") {
|
||||
throw badRequest("memory must be a string");
|
||||
}
|
||||
if (typeof memory === "string" && memory.length > 50000) {
|
||||
throw badRequest("memory must be at most 50,000 characters");
|
||||
}
|
||||
if (bundleConfig !== undefined && bundleConfig !== null) {
|
||||
if (typeof bundleConfig !== "object" || Array.isArray(bundleConfig)) {
|
||||
throw badRequest("bundleConfig must be an object");
|
||||
}
|
||||
if (typeof bundleConfig.mode !== "string" || !["managed", "external"].includes(bundleConfig.mode)) {
|
||||
throw badRequest("bundleConfig.mode must be 'managed' or 'external'");
|
||||
}
|
||||
if (typeof bundleConfig.entryFile !== "string") {
|
||||
throw badRequest("bundleConfig.entryFile must be a string");
|
||||
}
|
||||
if (!Array.isArray(bundleConfig.files)) {
|
||||
throw badRequest("bundleConfig.files must be an array");
|
||||
}
|
||||
if (bundleConfig.externalPath !== undefined && typeof bundleConfig.externalPath !== "string") {
|
||||
throw badRequest("bundleConfig.externalPath must be a string");
|
||||
}
|
||||
}
|
||||
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { AgentStore } = await import("@fusion/core");
|
||||
@@ -9087,6 +9112,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
instructionsPath: instructionsPath ?? undefined,
|
||||
instructionsText: instructionsText ?? undefined,
|
||||
soul: soul ?? undefined,
|
||||
memory: memory ?? undefined,
|
||||
bundleConfig: bundleConfig ?? undefined,
|
||||
});
|
||||
res.status(201).json(agent);
|
||||
} catch (err: any) {
|
||||
@@ -9258,6 +9285,11 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
name: input.name,
|
||||
role: input.role,
|
||||
title: typeof input.title === "string" ? input.title : undefined,
|
||||
icon: typeof input.icon === "string" ? input.icon : undefined,
|
||||
reportsTo: typeof input.reportsTo === "string" ? input.reportsTo : undefined,
|
||||
instructionsText: typeof input.instructionsText === "string"
|
||||
? input.instructionsText.slice(0, 200) + (input.instructionsText.length > 200 ? "..." : "")
|
||||
: undefined,
|
||||
skills: Array.isArray(input.metadata?.skills)
|
||||
? input.metadata.skills.filter((skill: unknown): skill is string => typeof skill === "string")
|
||||
: undefined,
|
||||
@@ -9526,6 +9558,37 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
updates.soul = body.soul ?? undefined;
|
||||
}
|
||||
|
||||
if ("memory" in body) {
|
||||
if (body.memory !== null && typeof body.memory !== "string") {
|
||||
throw badRequest("memory must be a string");
|
||||
}
|
||||
if (typeof body.memory === "string" && body.memory.length > 50000) {
|
||||
throw badRequest("memory must be at most 50,000 characters");
|
||||
}
|
||||
updates.memory = body.memory ?? undefined;
|
||||
}
|
||||
|
||||
if ("bundleConfig" in body) {
|
||||
if (body.bundleConfig !== null) {
|
||||
if (typeof body.bundleConfig !== "object" || Array.isArray(body.bundleConfig)) {
|
||||
throw badRequest("bundleConfig must be an object");
|
||||
}
|
||||
if (typeof body.bundleConfig.mode !== "string" || !["managed", "external"].includes(body.bundleConfig.mode)) {
|
||||
throw badRequest("bundleConfig.mode must be 'managed' or 'external'");
|
||||
}
|
||||
if (typeof body.bundleConfig.entryFile !== "string") {
|
||||
throw badRequest("bundleConfig.entryFile must be a string");
|
||||
}
|
||||
if (!Array.isArray(body.bundleConfig.files)) {
|
||||
throw badRequest("bundleConfig.files must be an array");
|
||||
}
|
||||
if (body.bundleConfig.externalPath !== undefined && typeof body.bundleConfig.externalPath !== "string") {
|
||||
throw badRequest("bundleConfig.externalPath must be a string");
|
||||
}
|
||||
}
|
||||
updates.bundleConfig = body.bundleConfig ?? undefined;
|
||||
}
|
||||
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { AgentStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
|
||||
Reference in New Issue
Block a user