feat(agent): make the assistant agentic - visible tools, LLM voice, full toolset

The agent felt like an artificial idiot because the LLM almost never spoke
for itself: 14+ Go paths injected fmt.Sprintf canned replies, the frontend
filtered out tool-progress events so users saw three dots for 10-20s, the
main prompt told the LLM "be a trading partner" AND "answer only what's
asked", and the planner sliced the toolset by inferred domain so a "BTC
dropped, how much am I losing?" question couldn't see positions and market
at the same time.

- agent/central_brain.go: shouldTrustDeterministicSkillReply now always
  returns false. Successful mutations (trader/strategy/model/exchange
  create/update/start/stop/delete) flow through reviewTaskCompletion so the
  LLM sees the real outcome JSON and writes the user-facing prose. The
  trade-confirmation regex path (handleTradeConfirmation) was already
  outside this code path and is unaffected.

- agent/agent.go: rewrite the Behavior section of the main system prompt.
  Replace the contradictory "answer only what's asked / don't upsell" with
  "lead with the direct answer, then optionally one relevant follow-up
  only when (a) open risk, (b) missing config, or (c) the next step is
  obvious — e.g. created, want me to start it?". Explicitly authorize
  chaining ("if the user says create and start, do both this turn") and
  ban "please wait / I'll get back to you" language because there is no
  background job to come back from.

- agent/tools.go: plannerToolsForText always returns the full 22-tool set
  (new __all__ domain). The old per-domain trimming hid manage_trader from
  market questions and execute_trade from anything that didn't look like
  an explicit trade — cross-domain reasoning was structurally blocked. The
  compact-vs-full strategy schema switch is preserved so mutation intents
  still see the full config schema.

- web/src/components/agent/{AgentStepPanel,ChatMessages}.tsx: stop
  filtering tool: steps. Map raw tool names to friendly labels with emoji
  ("get_positions" → "📊 检查持仓") in zh/en/id. Users now see what the
  agent is doing in real time instead of silence. central_brain routing
  chatter still gets dropped.

- agent/planner_tools_test.go: tests updated to assert the new
  full-toolset behavior and the compact-vs-full strategy schema switch.
This commit is contained in:
tinkle-community
2026-05-29 22:13:05 +08:00
parent fcb73cc195
commit 1851508353
6 changed files with 166 additions and 57 deletions

View File

@@ -1,4 +1,5 @@
import type { AgentStep } from '../../types/agent'
import { useLanguage } from '../../contexts/LanguageContext'
interface AgentStepPanelProps {
steps?: AgentStep[]
@@ -13,21 +14,80 @@ const statusStyles: Record<AgentStep['status'], { dot: string; text: string }> =
replanned: { dot: '#38bdf8', text: '#9bdcf7' },
}
// Map raw backend tool names to friendly user-facing labels.
// Backend emits `step.label` like `tool:get_positions` and we render that as
// "📊 Checking your positions" instead of hiding it from the user.
const toolLabels: Record<string, { zh: string; en: string; id: string }> = {
// Read-only state
get_positions: { zh: '📊 检查持仓', en: '📊 Checking positions', id: '📊 Memeriksa posisi' },
get_balance: { zh: '💰 查余额', en: '💰 Reading balance', id: '💰 Membaca saldo' },
get_trade_history: { zh: '📜 查交易历史', en: '📜 Reading trade history', id: '📜 Membaca riwayat' },
get_decisions: { zh: '🤖 查 AI 决策记录', en: '🤖 Reading AI decisions', id: '🤖 Membaca keputusan AI' },
get_strategies: { zh: '📋 查策略列表', en: '📋 Listing strategies', id: '📋 Daftar strategi' },
get_candidate_coins: { zh: '🎯 查标的池', en: '🎯 Reading candidate pool', id: '🎯 Kandidat' },
get_exchange_configs: { zh: '🔌 查交易所配置', en: '🔌 Reading exchanges', id: '🔌 Bursa' },
get_model_configs: { zh: '🧠 查 AI 模型', en: '🧠 Reading AI models', id: '🧠 Model AI' },
get_preferences: { zh: '⚙️ 查偏好', en: '⚙️ Reading preferences', id: '⚙️ Preferensi' },
get_backend_logs: { zh: '🪵 查后台日志', en: '🪵 Reading logs', id: '🪵 Membaca log' },
get_watchlist: { zh: '👁 查关注列表', en: '👁 Reading watchlist', id: '👁 Membaca watchlist' },
// Market data
search_stock: { zh: '🔍 搜索股票', en: '🔍 Searching stocks', id: '🔍 Mencari saham' },
get_market_price: { zh: '📈 查实时价格', en: '📈 Fetching price', id: '📈 Mengambil harga' },
get_market_snapshot: { zh: '📈 查市场快照', en: '📈 Reading market snapshot', id: '📈 Snapshot pasar' },
get_kline: { zh: '📈 查 K 线', en: '📈 Reading candlesticks', id: '📈 Membaca candlestick' },
// Mutating
manage_trader: { zh: '🤖 管理 Trader', en: '🤖 Managing trader', id: '🤖 Mengelola trader' },
manage_strategy: { zh: '📋 管理策略', en: '📋 Managing strategy', id: '📋 Mengelola strategi' },
manage_exchange_config: { zh: '🔌 管理交易所', en: '🔌 Managing exchange', id: '🔌 Mengelola bursa' },
manage_model_config: { zh: '🧠 管理 AI 模型', en: '🧠 Managing AI model', id: '🧠 Mengelola model' },
manage_preferences: { zh: '⚙️ 更新偏好', en: '⚙️ Updating preferences', id: '⚙️ Memperbarui preferensi' },
manage_watchlist: { zh: '👁 更新关注列表', en: '👁 Updating watchlist', id: '👁 Memperbarui watchlist' },
execute_trade: { zh: '⚡ 准备下单', en: '⚡ Preparing trade', id: '⚡ Menyiapkan order' },
}
function friendlyStepLabel(rawLabel: string, lang: 'zh' | 'en' | 'id'): string {
const trimmed = rawLabel.trim()
if (trimmed.toLowerCase().startsWith('tool:')) {
const toolName = trimmed.slice(5).trim().toLowerCase()
const entry = toolLabels[toolName]
if (entry) return entry[lang]
// Unknown tool — surface a generic but still informative label
const generic = {
zh: `🔧 调用 ${toolName}`,
en: `🔧 Calling ${toolName}`,
id: `🔧 Memanggil ${toolName}`,
}
return generic[lang]
}
return rawLabel
}
export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
const { language } = useLanguage()
const lang = (language === 'zh' || language === 'id' ? language : 'en') as
| 'zh'
| 'en'
| 'id'
if (!visible || !steps || steps.length === 0) {
return null
}
const sanitizedSteps = steps.filter((step) => {
const label = step.label.trim().toLowerCase()
// Drop only the internal-routing chatter (central_brain); keep tool steps —
// they are exactly what the user wants to see ("agent is actually doing something").
const visibleSteps = steps.filter((step) => {
const detail = (step.detail || '').trim().toLowerCase()
return !(label.startsWith('tool:') || detail === 'central_brain')
return detail !== 'central_brain'
})
if (sanitizedSteps.length === 0) {
if (visibleSteps.length === 0) {
return null
}
const liveRunHeading = lang === 'zh' ? 'AGENT 实时动作' : lang === 'id' ? 'AKSI AGENT' : 'LIVE RUN'
return (
<div
style={{
@@ -48,11 +108,12 @@ export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
marginBottom: 10,
}}
>
Live Run
{liveRunHeading}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{sanitizedSteps.map((step) => {
{visibleSteps.map((step) => {
const style = statusStyles[step.status]
const label = friendlyStepLabel(step.label, lang)
return (
<div
key={step.id}
@@ -85,9 +146,9 @@ export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
fontWeight: step.status === 'running' ? 600 : 500,
}}
>
{step.label}
{label}
</div>
{step.detail && (
{step.detail && step.detail.trim().toLowerCase() !== 'central_brain' && (
<div
style={{
fontSize: 11.5,

View File

@@ -10,12 +10,12 @@ interface ChatMessagesProps {
function hasMeaningfulExecutionSteps(steps?: AgentStep[]) {
if (!steps || steps.length === 0) return false
// Tool steps (label "tool:get_positions" etc.) ARE meaningful — they're the
// visible signal that the agent is actually doing something. Only drop the
// internal routing chatter (central_brain) and pure-planning placeholders.
return steps.some((step) => {
const label = step.label.trim().toLowerCase()
const detail = (step.detail || '').trim().toLowerCase()
if (label.startsWith('tool:') || detail === 'central_brain') {
return false
}
if (detail === 'central_brain') return false
return step.status !== 'planning'
})
}