mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(managed-agents): stale harness pin shadows runtime edits; add Restart quick action (#2252)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -478,6 +478,25 @@ pub fn apply_persona_snapshot(record: &mut ManagedAgentRecord, persona: &AgentDe
|
||||
record.model = snapshot.model;
|
||||
record.provider = snapshot.provider;
|
||||
record.runtime = snapshot.runtime;
|
||||
// Drop a stale create-time harness pin when the definition names a
|
||||
// different known runtime; custom commands stay pinned.
|
||||
if let Some(def_runtime) = persona
|
||||
.runtime
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|r| !r.is_empty())
|
||||
.and_then(crate::managed_agents::known_acp_runtime_exact)
|
||||
{
|
||||
if let Some(pin_runtime) = record
|
||||
.agent_command_override
|
||||
.as_deref()
|
||||
.and_then(crate::managed_agents::known_acp_runtime)
|
||||
{
|
||||
if !std::ptr::eq(pin_runtime, def_runtime) {
|
||||
record.agent_command_override = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// env_vars stay overrides-only. Self-heal records written before the env
|
||||
// refresh: persona env used to be baked into `record.env_vars`, turning
|
||||
// inherited values into pseudo-overrides that shadow later persona edits.
|
||||
|
||||
@@ -366,21 +366,39 @@ fn definition_runtime_edit_changes_hash_for_materialized_record() {
|
||||
);
|
||||
}
|
||||
|
||||
/// (c) An explicit agent_command_override (ladder step 1) must beat a
|
||||
/// changed definition runtime — the badge must NOT fire for a pinned instance.
|
||||
/// (c) A pin naming a KNOWN runtime no longer beats a changed definition
|
||||
/// runtime — apply_persona_snapshot clears the stale pin, so the badge fires.
|
||||
#[test]
|
||||
fn agent_command_override_beats_definition_runtime_change() {
|
||||
fn known_runtime_pin_yields_to_definition_runtime_change() {
|
||||
let mut rec = record();
|
||||
rec.persona_id = Some("pers".into());
|
||||
rec.runtime = Some("goose".into()); // materialized runtime
|
||||
rec.agent_command_override = Some("goose".into()); // explicit per-instance pin
|
||||
rec.agent_command_override = Some("goose".into()); // create-time pin
|
||||
|
||||
let before = [persona("pers", Some("goose"), "prompt")];
|
||||
let after = [persona("pers", Some("claude"), "prompt")];
|
||||
assert_ne!(
|
||||
spawn_config_hash(&rec, &before, &[], "wss://ws.example", &Default::default()),
|
||||
spawn_config_hash(&rec, &after, &[], "wss://ws.example", &Default::default()),
|
||||
"stale known-runtime pin must not shadow a definition runtime edit"
|
||||
);
|
||||
}
|
||||
|
||||
/// (c2) A custom-command override (no matching known runtime) still beats a
|
||||
/// changed definition runtime — the badge must NOT fire for such a pin.
|
||||
#[test]
|
||||
fn custom_command_override_beats_definition_runtime_change() {
|
||||
let mut rec = record();
|
||||
rec.persona_id = Some("pers".into());
|
||||
rec.runtime = Some("goose".into()); // materialized runtime
|
||||
rec.agent_command_override = Some("/opt/custom/my-agent".into());
|
||||
|
||||
let before = [persona("pers", Some("goose"), "prompt")];
|
||||
let after = [persona("pers", Some("claude"), "prompt")];
|
||||
assert_eq!(
|
||||
spawn_config_hash(&rec, &before, &[], "wss://ws.example", &Default::default()),
|
||||
spawn_config_hash(&rec, &after, &[], "wss://ws.example", &Default::default()),
|
||||
"explicit override must win regardless of definition runtime change"
|
||||
"custom command override must win regardless of definition runtime change"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,9 @@ import {
|
||||
buildInstanceInputForDefinition,
|
||||
resolveStartRuntimeForDefinition,
|
||||
} from "@/features/agents/lib/instanceInputForDefinition";
|
||||
import {
|
||||
isManagedAgentActive,
|
||||
startManagedAgentWithRules,
|
||||
stopManagedAgentWithRules,
|
||||
} from "@/features/agents/lib/managedAgentControlActions";
|
||||
import { describeLogFile } from "@/features/agents/ui/agentUi";
|
||||
import { AgentDialog } from "@/features/agents/ui/AgentDialog";
|
||||
import { useAgentLifecycleActions } from "@/features/profile/ui/useAgentLifecycleActions";
|
||||
import {
|
||||
consumePendingOpenEditAgent,
|
||||
type EditAgentFocusTarget,
|
||||
@@ -451,42 +447,14 @@ export function UserProfilePanel({
|
||||
],
|
||||
);
|
||||
|
||||
const handleAgentPrimaryAction = React.useCallback(async () => {
|
||||
if (!managedAgent) return;
|
||||
|
||||
try {
|
||||
if (isManagedAgentActive(managedAgent)) {
|
||||
const result = await stopManagedAgentWithRules({
|
||||
agent: managedAgent,
|
||||
channels: channelsQuery.data ?? [],
|
||||
relayAgents: relayAgentsQuery.data ?? [],
|
||||
stopManagedAgent: stopAgentMutation.mutateAsync,
|
||||
});
|
||||
toast.success(result.noticeMessage ?? `Stopped ${managedAgent.name}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await startManagedAgentWithRules({
|
||||
agent: managedAgent,
|
||||
startManagedAgent: startAgentMutation.mutateAsync,
|
||||
});
|
||||
toast.success(
|
||||
managedAgent.backend.type === "provider"
|
||||
? `Deploying ${managedAgent.name}.`
|
||||
: `Started ${managedAgent.name}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Agent action failed.",
|
||||
);
|
||||
}
|
||||
}, [
|
||||
channelsQuery.data,
|
||||
managedAgent,
|
||||
relayAgentsQuery.data,
|
||||
startAgentMutation.mutateAsync,
|
||||
stopAgentMutation.mutateAsync,
|
||||
]);
|
||||
const { handleAgentPrimaryAction, handleAgentRestart } =
|
||||
useAgentLifecycleActions({
|
||||
channels: channelsQuery.data,
|
||||
managedAgent,
|
||||
relayAgents: relayAgentsQuery.data,
|
||||
startManagedAgent: startAgentMutation.mutateAsync,
|
||||
stopManagedAgent: stopAgentMutation.mutateAsync,
|
||||
});
|
||||
|
||||
const handleInstantiateAgent = React.useCallback(async () => {
|
||||
if (!resolvedPersona) return;
|
||||
@@ -828,6 +796,7 @@ export function UserProfilePanel({
|
||||
followMutation={followMutation}
|
||||
agentInstruction={agentInstruction}
|
||||
handleAgentPrimaryAction={handleAgentPrimaryAction}
|
||||
handleAgentRestart={handleAgentRestart}
|
||||
handleEditAgent={handleEditAgent}
|
||||
handleEditPersona={canEditPersona ? handleEditPersona : undefined}
|
||||
handleInstantiateAgent={handleInstantiateAgent}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MessageSquare,
|
||||
Pencil,
|
||||
Play,
|
||||
RefreshCw,
|
||||
Square,
|
||||
UserMinus,
|
||||
UserPlus,
|
||||
@@ -77,6 +78,7 @@ export type ProfileSummaryViewProps = {
|
||||
canInstantiateAgent: boolean;
|
||||
agentInstruction: string | null;
|
||||
handleAgentPrimaryAction: () => void;
|
||||
handleAgentRestart: () => void;
|
||||
handleEditAgent: () => void;
|
||||
handleEditPersona?: () => void;
|
||||
handleInstantiateAgent: () => void;
|
||||
@@ -189,6 +191,7 @@ export function ProfileSummaryView({
|
||||
canInstantiateAgent,
|
||||
agentInstruction,
|
||||
handleAgentPrimaryAction,
|
||||
handleAgentRestart,
|
||||
handleEditAgent,
|
||||
handleEditPersona,
|
||||
handleInstantiateAgent,
|
||||
@@ -369,6 +372,14 @@ export function ProfileSummaryView({
|
||||
? handleAgentPrimaryAction
|
||||
: undefined
|
||||
}
|
||||
onAgentRestart={
|
||||
isOwner === true &&
|
||||
managedAgent?.backend.type === "local" &&
|
||||
(managedAgent.status === "running" ||
|
||||
managedAgent.status === "deployed")
|
||||
? handleAgentRestart
|
||||
: undefined
|
||||
}
|
||||
isFollowing={isFollowing}
|
||||
messagePending={isMessagePending}
|
||||
onMessage={onOpenDm ? handleMessage : undefined}
|
||||
@@ -629,6 +640,7 @@ function ProfilePrimaryActions({
|
||||
isFollowing,
|
||||
messagePending,
|
||||
onAgentPrimaryAction,
|
||||
onAgentRestart,
|
||||
onEditAgent,
|
||||
onMessage,
|
||||
pubkey,
|
||||
@@ -642,6 +654,7 @@ function ProfilePrimaryActions({
|
||||
isFollowing: boolean;
|
||||
messagePending?: boolean;
|
||||
onAgentPrimaryAction?: () => void;
|
||||
onAgentRestart?: () => void;
|
||||
onEditAgent: () => void;
|
||||
onMessage?: () => void;
|
||||
pubkey: string;
|
||||
@@ -698,6 +711,15 @@ function ProfilePrimaryActions({
|
||||
testId="user-profile-agent-primary-action"
|
||||
/>
|
||||
) : null}
|
||||
{onAgentRestart ? (
|
||||
<ProfileQuickAction
|
||||
disabled={agentActionDisabled}
|
||||
icon={RefreshCw}
|
||||
label="Restart"
|
||||
onClick={onAgentRestart}
|
||||
testId="user-profile-agent-restart"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
isManagedAgentActive,
|
||||
respawnManagedAgentWithRules,
|
||||
startManagedAgentWithRules,
|
||||
stopManagedAgentWithRules,
|
||||
} from "@/features/agents/lib/managedAgentControlActions";
|
||||
import type { Channel, ManagedAgent, RelayAgent } from "@/shared/api/types";
|
||||
|
||||
export function useAgentLifecycleActions({
|
||||
channels,
|
||||
managedAgent,
|
||||
relayAgents,
|
||||
startManagedAgent,
|
||||
stopManagedAgent,
|
||||
}: {
|
||||
channels: readonly Channel[] | undefined;
|
||||
managedAgent: ManagedAgent | undefined;
|
||||
relayAgents: readonly RelayAgent[] | undefined;
|
||||
startManagedAgent: (pubkey: string) => Promise<unknown>;
|
||||
stopManagedAgent: (pubkey: string) => Promise<unknown>;
|
||||
}) {
|
||||
const handleAgentPrimaryAction = React.useCallback(async () => {
|
||||
if (!managedAgent) return;
|
||||
|
||||
try {
|
||||
if (isManagedAgentActive(managedAgent)) {
|
||||
const result = await stopManagedAgentWithRules({
|
||||
agent: managedAgent,
|
||||
channels: channels ?? [],
|
||||
relayAgents: relayAgents ?? [],
|
||||
stopManagedAgent,
|
||||
});
|
||||
toast.success(result.noticeMessage ?? `Stopped ${managedAgent.name}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await startManagedAgentWithRules({
|
||||
agent: managedAgent,
|
||||
startManagedAgent,
|
||||
});
|
||||
toast.success(
|
||||
managedAgent.backend.type === "provider"
|
||||
? `Deploying ${managedAgent.name}.`
|
||||
: `Started ${managedAgent.name}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Agent action failed.",
|
||||
);
|
||||
}
|
||||
}, [
|
||||
channels,
|
||||
managedAgent,
|
||||
relayAgents,
|
||||
startManagedAgent,
|
||||
stopManagedAgent,
|
||||
]);
|
||||
|
||||
const handleAgentRestart = React.useCallback(async () => {
|
||||
if (!managedAgent) return;
|
||||
|
||||
try {
|
||||
await respawnManagedAgentWithRules({
|
||||
agent: managedAgent,
|
||||
startManagedAgent,
|
||||
stopManagedAgent,
|
||||
});
|
||||
toast.success(`Restarted ${managedAgent.name}.`);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Agent restart failed.",
|
||||
);
|
||||
}
|
||||
}, [managedAgent, startManagedAgent, stopManagedAgent]);
|
||||
|
||||
return { handleAgentPrimaryAction, handleAgentRestart };
|
||||
}
|
||||
Reference in New Issue
Block a user