mirror of
https://github.com/block/buzz.git
synced 2026-11-10 11:21:05 +00:00
Sync persona runtime edits to agents
This commit is contained in:
@@ -301,14 +301,7 @@ pub fn parse_md_persona(md_bytes: &[u8]) -> Result<ParsedPersonaPreview, String>
|
||||
fn parsed_preview_from_md_config(
|
||||
config: buzz_persona_pkg::persona::PersonaConfig,
|
||||
) -> ParsedPersonaPreview {
|
||||
// Split "provider:model" into separate fields for the preview.
|
||||
let model = match config.model.as_deref() {
|
||||
Some(s) if !s.is_empty() => {
|
||||
let (_prov, id) = buzz_persona_pkg::persona::split_model(s);
|
||||
Some(id.to_owned())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (provider, model) = split_preview_model(config.model.as_deref());
|
||||
|
||||
ParsedPersonaPreview {
|
||||
display_name: config.display_name,
|
||||
@@ -317,12 +310,22 @@ fn parsed_preview_from_md_config(
|
||||
avatar_ref: config.avatar,
|
||||
runtime: config.runtime,
|
||||
model,
|
||||
provider: None, // Markdown persona format does not carry llmProvider
|
||||
provider,
|
||||
name_pool: Vec::new(),
|
||||
source_file: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn split_preview_model(model: Option<&str>) -> (Option<String>, Option<String>) {
|
||||
match model.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Some(raw_model) => {
|
||||
let (provider, id) = buzz_persona_pkg::persona::split_model(raw_model);
|
||||
(provider.map(str::to_owned), Some(id.to_owned()))
|
||||
}
|
||||
None => (None, None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LenientMdFrontmatter {
|
||||
name: Option<String>,
|
||||
@@ -343,14 +346,7 @@ fn parse_lenient_md_persona(content: &str) -> Result<ParsedPersonaPreview, Strin
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| "Missing display name".to_string())?;
|
||||
// Split "provider:model" into separate fields for the preview.
|
||||
let model = match fields.model.as_deref() {
|
||||
Some(s) if !s.is_empty() => {
|
||||
let (_prov, id) = buzz_persona_pkg::persona::split_model(s);
|
||||
Some(id.to_owned())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (provider, model) = split_preview_model(fields.model.as_deref());
|
||||
|
||||
Ok(ParsedPersonaPreview {
|
||||
display_name,
|
||||
@@ -359,7 +355,7 @@ fn parse_lenient_md_persona(content: &str) -> Result<ParsedPersonaPreview, Strin
|
||||
avatar_ref: fields.avatar,
|
||||
runtime: fields.runtime,
|
||||
model,
|
||||
provider: None, // Markdown persona format does not carry llmProvider
|
||||
provider,
|
||||
name_pool: Vec::new(),
|
||||
source_file: String::new(),
|
||||
})
|
||||
|
||||
@@ -420,6 +420,24 @@ You are Goosey.
|
||||
assert_eq!(result.avatar_data_url, None);
|
||||
assert_eq!(result.avatar_ref.as_deref(), Some("app-avatar:gloopies-19"));
|
||||
assert_eq!(result.model.as_deref(), Some("claude-sonnet-4"));
|
||||
assert_eq!(result.provider.as_deref(), Some("anthropic"));
|
||||
assert_eq!(result.runtime.as_deref(), Some("goose"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_lenient_md_persona_preserves_model_provider_prefix() {
|
||||
let md = r#"---
|
||||
display_name: Lenient Agent
|
||||
model: databricks:gpt-5
|
||||
runtime: goose
|
||||
---
|
||||
You are lenient.
|
||||
"#;
|
||||
|
||||
let result = parse_lenient_md_persona(md).unwrap();
|
||||
assert_eq!(result.display_name, "Lenient Agent");
|
||||
assert_eq!(result.model.as_deref(), Some("gpt-5"));
|
||||
assert_eq!(result.provider.as_deref(), Some("databricks"));
|
||||
assert_eq!(result.runtime.as_deref(), Some("goose"));
|
||||
}
|
||||
|
||||
|
||||
@@ -501,6 +501,8 @@ export function UserProfilePanel({
|
||||
setPersonaDialogState(null);
|
||||
void personasQuery.refetch();
|
||||
},
|
||||
previousPersona: resolvedPersona,
|
||||
runtimes: acpRuntimesQuery.data ?? [],
|
||||
updateManagedAgent: updateManagedAgentMutation.mutateAsync,
|
||||
updatePersona: updatePersonaMutation.mutateAsync,
|
||||
});
|
||||
@@ -510,6 +512,8 @@ export function UserProfilePanel({
|
||||
createManagedAgentForPersona,
|
||||
managedAgent,
|
||||
personasQuery.refetch,
|
||||
resolvedPersona,
|
||||
acpRuntimesQuery.data,
|
||||
updateManagedAgentMutation.mutateAsync,
|
||||
updatePersonaMutation.mutateAsync,
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { toast } from "sonner";
|
||||
|
||||
import { personaManagedAgentUpdate } from "@/features/profile/ui/UserProfilePanelUtils";
|
||||
import type {
|
||||
AcpRuntimeCatalogEntry,
|
||||
AgentPersona,
|
||||
CreateManagedAgentResponse,
|
||||
CreatePersonaInput,
|
||||
@@ -18,6 +19,8 @@ type SubmitProfilePersonaDialogOptions = {
|
||||
input: CreatePersonaInput | UpdatePersonaInput;
|
||||
managedAgent: ManagedAgent | undefined;
|
||||
onDone: () => void;
|
||||
previousPersona?: AgentPersona;
|
||||
runtimes?: readonly AcpRuntimeCatalogEntry[];
|
||||
updateManagedAgent: (
|
||||
input: UpdateManagedAgentInput,
|
||||
) => Promise<{ agent: ManagedAgent; profileSyncError: string | null }>;
|
||||
@@ -30,6 +33,8 @@ export async function submitProfilePersonaDialog({
|
||||
input,
|
||||
managedAgent,
|
||||
onDone,
|
||||
previousPersona,
|
||||
runtimes,
|
||||
updateManagedAgent,
|
||||
updatePersona,
|
||||
}: SubmitProfilePersonaDialogOptions) {
|
||||
@@ -37,7 +42,10 @@ export async function submitProfilePersonaDialog({
|
||||
if ("id" in input) {
|
||||
const persona = await updatePersona(input);
|
||||
const agentUpdate = managedAgent
|
||||
? personaManagedAgentUpdate(managedAgent, persona)
|
||||
? personaManagedAgentUpdate(managedAgent, persona, {
|
||||
previousPersona,
|
||||
runtimes,
|
||||
})
|
||||
: null;
|
||||
const result = agentUpdate ? await updateManagedAgent(agentUpdate) : null;
|
||||
if (result?.profileSyncError) {
|
||||
|
||||
@@ -59,6 +59,24 @@ function persona(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function runtime(overrides = {}) {
|
||||
return {
|
||||
id: "claude",
|
||||
label: "Claude Code",
|
||||
avatarUrl: "app-avatar://claude",
|
||||
availability: "available",
|
||||
command: "claude",
|
||||
binaryPath: "/usr/local/bin/claude",
|
||||
defaultArgs: ["mcp", "serve"],
|
||||
mcpCommand: "claude-mcp",
|
||||
installHint: "",
|
||||
installInstructionsUrl: "",
|
||||
canAutoInstall: false,
|
||||
underlyingCliPath: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("personaManagedAgentUpdate syncs edited persona identity to linked agent", () => {
|
||||
assert.deepEqual(personaManagedAgentUpdate(agent(), persona()), {
|
||||
pubkey: "deadbeef".repeat(8),
|
||||
@@ -89,3 +107,44 @@ test("personaManagedAgentUpdate skips unrelated or unchanged agents", () => {
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("personaManagedAgentUpdate maps changed persona runtime to linked agent commands", () => {
|
||||
assert.deepEqual(
|
||||
personaManagedAgentUpdate(agent(), persona({ runtime: "claude" }), {
|
||||
previousPersona: persona({ runtime: "goose" }),
|
||||
runtimes: [runtime()],
|
||||
}),
|
||||
{
|
||||
pubkey: "deadbeef".repeat(8),
|
||||
name: "Fizz Prime",
|
||||
avatarUrl: null,
|
||||
systemPrompt: "New prompt",
|
||||
model: "new-model",
|
||||
envVars: { NEW_KEY: "2" },
|
||||
agentCommand: "claude",
|
||||
agentArgs: ["mcp", "serve"],
|
||||
mcpCommand: "claude-mcp",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("personaManagedAgentUpdate leaves runtime fields alone when runtime is unchanged", () => {
|
||||
assert.equal(
|
||||
personaManagedAgentUpdate(
|
||||
agent({
|
||||
name: "Fizz Prime",
|
||||
avatarUrl: null,
|
||||
systemPrompt: "New prompt",
|
||||
model: "new-model",
|
||||
envVars: { NEW_KEY: "2" },
|
||||
agentArgs: ["custom"],
|
||||
}),
|
||||
persona({ runtime: "goose" }),
|
||||
{
|
||||
previousPersona: persona({ runtime: "goose" }),
|
||||
runtimes: [runtime({ id: "goose", command: "goose" })],
|
||||
},
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import type {
|
||||
AcpRuntimeCatalogEntry,
|
||||
AgentPersona,
|
||||
Channel,
|
||||
ManagedAgent,
|
||||
@@ -162,6 +163,10 @@ export function resolveAgentInstruction(
|
||||
export function personaManagedAgentUpdate(
|
||||
agent: ManagedAgent,
|
||||
persona: AgentPersona,
|
||||
options: {
|
||||
previousPersona?: AgentPersona;
|
||||
runtimes?: readonly AcpRuntimeCatalogEntry[];
|
||||
} = {},
|
||||
): UpdateManagedAgentInput | null {
|
||||
if (agent.personaId !== persona.id) return null;
|
||||
|
||||
@@ -193,9 +198,39 @@ export function personaManagedAgentUpdate(
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const runtimeChanged =
|
||||
options.previousPersona !== undefined &&
|
||||
options.previousPersona.runtime !== persona.runtime;
|
||||
const runtime = runtimeChanged
|
||||
? options.runtimes?.find((candidate) => candidate.id === persona.runtime)
|
||||
: undefined;
|
||||
if (runtime?.command) {
|
||||
if (runtime.command !== agent.agentCommand) {
|
||||
input.agentCommand = runtime.command;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (!stringArrayEqual(runtime.defaultArgs, agent.agentArgs)) {
|
||||
input.agentArgs = [...runtime.defaultArgs];
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const mcpCommand = runtime.mcpCommand ?? "";
|
||||
if (mcpCommand !== agent.mcpCommand) {
|
||||
input.mcpCommand = mcpCommand;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? input : null;
|
||||
}
|
||||
|
||||
function stringArrayEqual(left: readonly string[], right: readonly string[]) {
|
||||
if (left.length !== right.length) return false;
|
||||
|
||||
return left.every((value, index) => value === right[index]);
|
||||
}
|
||||
|
||||
function stringRecordEqual(
|
||||
left: Record<string, string>,
|
||||
right: Record<string, string>,
|
||||
|
||||
Reference in New Issue
Block a user