fix(ui/desktop): send promoted agent fields at top level to fix summoning stuck

Backend v3 (migration 000037) promoted emoji, agent_description, self_evolve,
thinking_level, reasoning_config from other_config JSONB to top-level columns.
Desktop app was still nesting these inside other_config, so backend saw empty
agent_description and never triggered the summoning goroutine. SummoningModal
then waited forever for agent.summoning WS events that were never emitted.

- types/agent.ts: add promoted fields to AgentData and AgentInput
- services/agent-service.ts: add emoji to AgentListItem
- hooks/use-agent-detail-state.ts: read/write promoted fields at top-level,
  keep fallback to other_config for legacy agents, drop legacy keys on save
- hooks/use-agents.ts: read emoji from top-level with fallback
- components/agents/AgentFormDialog.tsx: send agent_description and emoji at
  top-level; read with fallback
- components/agents/AgentCard.tsx: read self_evolve, emoji, agent_description
  from top-level with fallback to other_config
- components/onboarding/AgentStep.tsx: critical fix - send agent_description
  at top level so backend triggers summoning
This commit is contained in:
viettranx committed 2026-04-11 07:56:42 +07:00
1 parent 1296cdbf02
commit 3105a7c811
7 files changed
+71 -59

No files matched your search

@@ -14,9 +14,8 @@ export function AgentCard({ agent, onEdit, onDelete, onResummon }: AgentCardProp
const { t } = useTranslation('agents')
const displayName = agent.display_name
|| (UUID_RE.test(agent.agent_key) ? 'Unnamed Agent' : agent.agent_key)
const otherCfg = (agent.other_config ?? {}) as Record<string, unknown>
const selfEvolve = agent.agent_type === 'predefined' && Boolean(otherCfg.self_evolve)
const emoji = typeof otherCfg.emoji === 'string' ? otherCfg.emoji : ''
const selfEvolve = agent.agent_type === 'predefined' && Boolean(agent.self_evolve ?? agent.other_config?.self_evolve)
const emoji = agent.emoji ?? (typeof agent.other_config?.emoji === 'string' ? agent.other_config.emoji : '')
const showSubtitle = agent.display_name && !UUID_RE.test(agent.agent_key)
const isSummoning = agent.status === 'summoning'
const isFailed = agent.status === 'summon_failed'
@@ -77,9 +76,9 @@ export function AgentCard({ agent, onEdit, onDelete, onResummon }: AgentCardProp
) : null}
{/* Expertise/frontmatter */}
{(agent.frontmatter || otherCfg.description) ? (
{(agent.frontmatter || agent.agent_description) ? (
<div className="line-clamp-3 text-xs text-text-muted/70">
{String(agent.frontmatter || otherCfg.description || '')}
{String(agent.frontmatter || agent.agent_description || '')}
</div>
) : null}
@@ -41,11 +41,11 @@ export function AgentFormDialog({ open, onOpenChange, agent, onSubmit }: AgentFo
if (!open) return
reset({
displayName: agent?.display_name ?? '',
emoji: (agent?.other_config?.emoji as string) ?? '🦊',
emoji: agent?.emoji ?? (agent?.other_config?.emoji as string) ?? '🦊',
agentKey: agent?.agent_key ?? '',
providerName: agent?.provider ?? '',
model: agent?.model ?? '',
description: (agent?.other_config?.description as string) ?? '',
description: agent?.agent_description ?? (agent?.other_config?.description as string) ?? '',
isDefault: agent?.is_default ?? false,
})
setSubmitError('')
@@ -96,9 +96,6 @@ export function AgentFormDialog({ open, onOpenChange, agent, onSubmit }: AgentFo
const onValid = async (data: AgentFormData) => {
setSubmitError('')
try {
const otherConfig: Record<string, unknown> = {}
if (data.description?.trim()) otherConfig.description = data.description.trim()
if (data.emoji?.trim()) otherConfig.emoji = data.emoji.trim()
await onSubmit({
agent_key: data.agentKey,
display_name: data.displayName.trim() || undefined,
@@ -106,7 +103,9 @@ export function AgentFormDialog({ open, onOpenChange, agent, onSubmit }: AgentFo
model: data.model.trim(),
agent_type: isEditing ? agent!.agent_type : 'predefined',
is_default: data.isDefault || undefined,
other_config: Object.keys(otherConfig).length > 0 ? otherConfig : undefined,
// Promoted fields at top level
emoji: data.emoji?.trim() || null,
agent_description: data.description?.trim() || null,
})
onOpenChange(false)
} catch (err) {
@@ -82,10 +82,6 @@ export function AgentStep({ provider, model, onBack, onComplete }: AgentStepProp
setLoading(true)
setError('')
try {
const otherConfig: Record<string, unknown> = {}
if (description.trim()) otherConfig.description = description.trim()
if (selectedEmoji) otherConfig.emoji = selectedEmoji
const result = await getApiClient().post<{ id: string }>('/v1/agents', {
agent_key: agentKey,
display_name: displayName.trim() || undefined,
@@ -93,7 +89,9 @@ export function AgentStep({ provider, model, onBack, onComplete }: AgentStepProp
model: model || '',
agent_type: 'predefined',
is_default: true,
other_config: Object.keys(otherConfig).length > 0 ? otherConfig : undefined,
// Promoted fields at top level — agent_description triggers summoning on backend
agent_description: description.trim() || null,
emoji: selectedEmoji || null,
})
setCreatedAgent({ id: result.id, name: displayName.trim() || agentKey })
} catch (err) {
@@ -9,10 +9,10 @@ export function useAgentDetailState(
onSave: (id: string, updates: Partial<AgentData>) => Promise<void>,
onClose: () => void,
) {
// --- Identity ---
const [emoji, setEmoji] = useState((agent.other_config?.emoji as string) ?? '🤖')
// --- Identity --- (promoted fields: read top-level, fallback to other_config for legacy data)
const [emoji, setEmoji] = useState(agent.emoji ?? (agent.other_config?.emoji as string) ?? '🤖')
const [displayName, setDisplayName] = useState(agent.display_name ?? '')
const [description, setDescription] = useState((agent.other_config?.description as string) ?? '')
const [description, setDescription] = useState(agent.agent_description ?? (agent.other_config?.description as string) ?? '')
const [status, setStatus] = useState(agent.status ?? 'active')
const [isDefault, setIsDefault] = useState(agent.is_default ?? false)
@@ -22,20 +22,22 @@ export function useAgentDetailState(
const [contextWindow, setContextWindow] = useState(agent.context_window ?? 200000)
const [maxToolIterations, setMaxToolIterations] = useState(agent.max_tool_iterations ?? 25)
// --- Evolution ---
const [selfEvolve, setSelfEvolve] = useState(!!(agent.other_config?.self_evolve))
const [skillLearning, setSkillLearning] = useState(!!(agent.other_config?.skill_learning))
// --- Evolution --- (promoted fields with fallback)
const [selfEvolve, setSelfEvolve] = useState(!!(agent.self_evolve ?? agent.other_config?.self_evolve))
const [skillLearning, setSkillLearning] = useState(!!(agent.skill_evolve ?? agent.other_config?.skill_learning))
const [skillNudgeInterval, setSkillNudgeInterval] = useState(
(agent.other_config?.skill_nudge_interval as number) ?? 15,
agent.skill_nudge_interval ?? (agent.other_config?.skill_nudge_interval as number) ?? 15,
)
// --- Prompt mode ---
const [promptMode, setPromptMode] = useState((agent.other_config?.prompt_mode as string) || 'full')
// --- Thinking ---
const reasoning = (agent.other_config?.reasoning ?? {}) as AgentReasoningConfig
// --- Thinking --- (promoted: reasoning_config + thinking_level, fallback to other_config)
const reasoning = (agent.reasoning_config ?? agent.other_config?.reasoning ?? {}) as AgentReasoningConfig
const [reasoningMode, setReasoningMode] = useState<ReasoningOverrideMode>(reasoning.override_mode ?? 'inherit')
const [thinkingLevel, setThinkingLevel] = useState(reasoning.effort ?? 'off')
const [thinkingLevel, setThinkingLevel] = useState(
agent.thinking_level ?? reasoning.effort ?? 'off',
)
// --- Context pruning ---
const [pruningEnabled, setPruningEnabled] = useState(agent.context_pruning != null)
@@ -70,35 +72,24 @@ export function useAgentDetailState(
setSaving(true)
setSaveError('')
try {
// Build other_config for non-promoted fields only
const otherConfig: Record<string, unknown> = { ...agent.other_config }
if (emoji) otherConfig.emoji = emoji
if (description.trim()) otherConfig.description = description.trim()
else delete otherConfig.description
otherConfig.self_evolve = selfEvolve
// Remove legacy keys that are now promoted to top-level
delete otherConfig.emoji
delete otherConfig.description
delete otherConfig.self_evolve
delete otherConfig.skill_learning
delete otherConfig.skill_nudge_interval
delete otherConfig.reasoning
// Skill learning
otherConfig.skill_learning = skillLearning
if (skillLearning && skillNudgeInterval > 0) {
otherConfig.skill_nudge_interval = skillNudgeInterval
} else {
delete otherConfig.skill_nudge_interval
}
// Prompt mode
// Prompt mode (still in other_config)
if (promptMode && promptMode !== 'full') {
otherConfig.prompt_mode = promptMode
} else {
delete otherConfig.prompt_mode
}
// Thinking / reasoning
if (reasoningMode === 'custom') {
otherConfig.reasoning = { override_mode: 'custom', effort: thinkingLevel }
} else {
delete otherConfig.reasoning
}
// Pinned skills
// Pinned skills (still in other_config)
if (pinnedSkills.length > 0) {
otherConfig.pinned_skills = pinnedSkills
} else {
@@ -107,13 +98,23 @@ export function useAgentDetailState(
await onSave(agent.id, {
display_name: displayName.trim() || undefined,
// Promoted fields at top level
emoji: emoji || null,
agent_description: description.trim() || null,
self_evolve: selfEvolve,
skill_evolve: skillLearning,
skill_nudge_interval: skillLearning && skillNudgeInterval > 0 ? skillNudgeInterval : null,
thinking_level: reasoningMode === 'custom' ? thinkingLevel : null,
reasoning_config: reasoningMode === 'custom'
? { override_mode: 'custom', effort: thinkingLevel } : null,
// Other fields
provider,
model,
context_window: contextWindow,
max_tool_iterations: maxToolIterations,
is_default: isDefault,
status,
other_config: otherConfig,
other_config: Object.keys(otherConfig).length > 0 ? otherConfig : null,
context_pruning: pruningEnabled ? pruningConfig : null,
compaction_config: compactionConfig,
subagents_config: subEnabled ? subConfig : null,
+8 -11
View File
@@ -14,17 +14,14 @@ export function useAgents() {
try {
const result = await agentService.listItems()
const mapped: Agent[] = (result.agents ?? []).map((a) => {
const otherCfg = a.other_config ?? {}
return {
id: a.id,
key: a.agent_key,
name: a.display_name || a.agent_key,
model: a.model ?? 'unknown',
status: 'online' as const,
emoji: typeof otherCfg.emoji === 'string' ? otherCfg.emoji : undefined,
}
})
const mapped: Agent[] = (result.agents ?? []).map((a) => ({
id: a.id,
key: a.agent_key,
name: a.display_name || a.agent_key,
model: a.model ?? 'unknown',
status: 'online' as const,
emoji: a.emoji ?? (typeof a.other_config?.emoji === 'string' ? a.other_config.emoji : undefined),
}))
setAgents(mapped)
return mapped
@@ -9,6 +9,7 @@ export interface AgentListItem {
display_name?: string
model?: string
provider?: string
emoji?: string | null
other_config?: Record<string, unknown> | null
}
+17
View File
@@ -88,6 +88,15 @@ export interface AgentData {
created_at?: string
updated_at?: string
// Promoted fields (formerly in other_config, migration 000037 v3)
emoji?: string | null
agent_description?: string | null
thinking_level?: string | null
self_evolve?: boolean | null
skill_evolve?: boolean | null
skill_nudge_interval?: number | null
reasoning_config?: AgentReasoningConfig | null
// Per-agent JSONB configs (null/undefined = use global defaults)
memory_config?: MemoryConfig | null
compaction_config?: CompactionConfig | null
@@ -108,6 +117,14 @@ export interface AgentInput {
is_default?: boolean
context_window?: number
max_tool_iterations?: number
// Promoted fields
emoji?: string | null
agent_description?: string | null
self_evolve?: boolean
thinking_level?: string | null
reasoning_config?: AgentReasoningConfig | null
skill_evolve?: boolean | null
skill_nudge_interval?: number | null
memory_config?: MemoryConfig | null
other_config?: Record<string, unknown>
}