refactor(agent): route every interaction through one agent constant (AGENT)
Say-hi, the module chat, and Refine each hard-coded the 'cloud' agent while Telegram ran on 'demo' — different agents for web vs Telegram, easy to get wrong. Introduce a single `AGENT = 'default'` in api.ts and use it everywhere. 'default' is the node's fallback agent (the one used when no alias is given), fully loaded with the cloud model + all skills + all tools — so no call site can pick a different or missing agent. Board side (config): 'default' is now the sole enabled agent and owns the telegram.default channel; 'cloud' and 'demo' are disabled and any stray request for them falls back to 'default'. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
06f2342892
commit
5a140ccb17
@@ -8,6 +8,7 @@ import type { WsEvent } from '@/types'
|
|||||||
|
|
||||||
let emit: (e: WsEvent) => void = () => {}
|
let emit: (e: WsEvent) => void = () => {}
|
||||||
vi.mock('@/lib/api', () => ({
|
vi.mock('@/lib/api', () => ({
|
||||||
|
AGENT: 'default',
|
||||||
sendPrompt: vi.fn().mockResolvedValue(undefined),
|
sendPrompt: vi.fn().mockResolvedValue(undefined),
|
||||||
openTeamActivity: (_t: string, on: (e: WsEvent) => void) => {
|
openTeamActivity: (_t: string, on: (e: WsEvent) => void) => {
|
||||||
emit = on
|
emit = on
|
||||||
@@ -28,7 +29,7 @@ describe('AgentChat', () => {
|
|||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<AgentChat />)
|
render(<AgentChat />)
|
||||||
await user.click(screen.getByTestId('prompt-i2c'))
|
await user.click(screen.getByTestId('prompt-i2c'))
|
||||||
expect(mockSend).toHaveBeenCalledWith(expect.any(String), 'List the I2C devices on the bus', 'cloud')
|
expect(mockSend).toHaveBeenCalledWith(expect.any(String), 'List the I2C devices on the bus', 'default')
|
||||||
expect(screen.getByTestId('chat-transcript')).toHaveTextContent(/list the i2c devices/i)
|
expect(screen.getByTestId('chat-transcript')).toHaveTextContent(/list the i2c devices/i)
|
||||||
expect(screen.getByTestId('prompt-i2c')).toHaveAttribute('data-state', 'running')
|
expect(screen.getByTestId('prompt-i2c')).toHaveAttribute('data-state', 'running')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useSession } from '@/store/session'
|
import { useSession } from '@/store/session'
|
||||||
import { sendPrompt, openTeamActivity } from '@/lib/api'
|
import { sendPrompt, openTeamActivity, AGENT } from '@/lib/api'
|
||||||
import type { NodeActivityKind, WsEvent } from '@/types'
|
import type { NodeActivityKind, WsEvent } from '@/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +90,7 @@ export function AgentChat({ onProgress }: AgentChatProps) {
|
|||||||
setStatus((s) => ({ ...s, [id]: 'running' }))
|
setStatus((s) => ({ ...s, [id]: 'running' }))
|
||||||
setLines((prev) => [...prev, { who: 'you', text }])
|
setLines((prev) => [...prev, { who: 'you', text }])
|
||||||
try {
|
try {
|
||||||
await sendPrompt(teamId, text, 'cloud')
|
await sendPrompt(teamId, text, AGENT)
|
||||||
} catch {
|
} catch {
|
||||||
running.current = null
|
running.current = null
|
||||||
setStatus((s) => ({ ...s, [id]: 'idle' }))
|
setStatus((s) => ({ ...s, [id]: 'idle' }))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ let liveOnEvent: ((e: WsEvent) => void) | null = null
|
|||||||
const closeSpy = vi.fn()
|
const closeSpy = vi.fn()
|
||||||
|
|
||||||
vi.mock('@/lib/api', () => ({
|
vi.mock('@/lib/api', () => ({
|
||||||
|
AGENT: 'default',
|
||||||
sendPrompt: (...args: unknown[]) => sendPrompt(...args),
|
sendPrompt: (...args: unknown[]) => sendPrompt(...args),
|
||||||
openTeamActivity: (_teamId: string, onEvent: (e: WsEvent) => void) => {
|
openTeamActivity: (_teamId: string, onEvent: (e: WsEvent) => void) => {
|
||||||
liveOnEvent = onEvent
|
liveOnEvent = onEvent
|
||||||
@@ -39,13 +40,13 @@ describe('BuildFlash', () => {
|
|||||||
expect(screen.getByText(/live board/i)).toBeInTheDocument()
|
expect(screen.getByText(/live board/i)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends the prompt to the single cloud agent and renders streamed board activity', async () => {
|
it('sends the prompt to the single agent and renders streamed board activity', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<MemoryRouter><BuildFlash /></MemoryRouter>)
|
render(<MemoryRouter><BuildFlash /></MemoryRouter>)
|
||||||
|
|
||||||
await user.type(screen.getByLabelText(/prompt your board/i), 'scroll HELLO')
|
await user.type(screen.getByLabelText(/prompt your board/i), 'scroll HELLO')
|
||||||
await user.click(screen.getByRole('button', { name: /working|send/i }))
|
await user.click(screen.getByRole('button', { name: /working|send/i }))
|
||||||
expect(sendPrompt).toHaveBeenCalledWith(useSession.getState().teamId, 'scroll HELLO', 'cloud')
|
expect(sendPrompt).toHaveBeenCalledWith(useSession.getState().teamId, 'scroll HELLO', 'default')
|
||||||
|
|
||||||
// a flash event streams in over the (mocked) SSE feed
|
// a flash event streams in over the (mocked) SSE feed
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { OpenYourNode } from '@/components/OpenYourNode'
|
import { OpenYourNode } from '@/components/OpenYourNode'
|
||||||
import { useSession } from '@/store/session'
|
import { useSession } from '@/store/session'
|
||||||
import { sendPrompt, openTeamActivity } from '@/lib/api'
|
import { sendPrompt, openTeamActivity, AGENT } from '@/lib/api'
|
||||||
import type { NodeActivityKind, WsEvent } from '@/types'
|
import type { NodeActivityKind, WsEvent } from '@/types'
|
||||||
|
|
||||||
interface Entry {
|
interface Entry {
|
||||||
@@ -70,7 +70,7 @@ export function BuildFlash() {
|
|||||||
setBusy(true)
|
setBusy(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendPrompt(teamId, msg, 'cloud')
|
await sendPrompt(teamId, msg, AGENT)
|
||||||
} catch {
|
} catch {
|
||||||
append({ kind: 'error', label: 'Could not reach your board — is it registered and online?' })
|
append({ kind: 'error', label: 'Could not reach your board — is it registered and online?' })
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { useState } from 'react'
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useSession } from '@/store/session'
|
import { useSession } from '@/store/session'
|
||||||
import { sayHi } from '@/lib/api'
|
import { sayHi, AGENT } from '@/lib/api'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type HiState = 'idle' | 'running' | 'ok'
|
type HiState = 'idle' | 'running' | 'ok'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Say hi to your agent" — chats with the cloud agent on the team's own board.
|
* "Say hi to your agent" — chats with the one workshop agent (AGENT) on the
|
||||||
|
* team's own board.
|
||||||
* A reply proves the node is live and listening. Shown once the board is bound.
|
* A reply proves the node is live and listening. Shown once the board is bound.
|
||||||
*/
|
*/
|
||||||
export function SayHiCard() {
|
export function SayHiCard() {
|
||||||
@@ -24,7 +25,7 @@ export function SayHiCard() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
setReply('')
|
setReply('')
|
||||||
try {
|
try {
|
||||||
const r = await sayHi(teamId, 'cloud')
|
const r = await sayHi(teamId, AGENT)
|
||||||
setReply(r || '(your node replied)')
|
setReply(r || '(your node replied)')
|
||||||
setHi('ok')
|
setHi('ok')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+10
-1
@@ -16,6 +16,15 @@ import type {
|
|||||||
const RAW_API_BASE = (import.meta.env.VITE_API_BASE as string | undefined)?.trim()
|
const RAW_API_BASE = (import.meta.env.VITE_API_BASE as string | undefined)?.trim()
|
||||||
export const API_BASE = RAW_API_BASE || 'https://apess-api.redclaw.dev'
|
export const API_BASE = RAW_API_BASE || 'https://apess-api.redclaw.dev'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ONE agent every workshop interaction uses — web say-hi, the module chat,
|
||||||
|
* Refine, and Telegram all route here. It's the node's `default` agent (the one
|
||||||
|
* the daemon falls back to when no alias is given), fully loaded: cloud model +
|
||||||
|
* all skills + all tools. Referencing this single constant everywhere means no
|
||||||
|
* call site can accidentally pick a different (or missing) agent.
|
||||||
|
*/
|
||||||
|
export const AGENT = 'default'
|
||||||
|
|
||||||
/** Build the WS URL. Absolute base → swap http→ws; relative/same-origin base
|
/** Build the WS URL. Absolute base → swap http→ws; relative/same-origin base
|
||||||
* ('' or '/api') → derive scheme+host from the page so it works at any LAN IP. */
|
* ('' or '/api') → derive scheme+host from the page so it works at any LAN IP. */
|
||||||
function wsUrl(code: string): string {
|
function wsUrl(code: string): string {
|
||||||
@@ -184,7 +193,7 @@ export async function sayHi(teamId: string, agent?: string, message?: string): P
|
|||||||
|
|
||||||
/** Ask the team's node a one-off prompt and wait for its reply (reuses the
|
/** Ask the team's node a one-off prompt and wait for its reply (reuses the
|
||||||
* blocking say-hi path). Backs the "Refine" features. Defaults to the cloud agent. */
|
* blocking say-hi path). Backs the "Refine" features. Defaults to the cloud agent. */
|
||||||
export async function askNode(teamId: string, prompt: string, agent = 'cloud'): Promise<string> {
|
export async function askNode(teamId: string, prompt: string, agent = AGENT): Promise<string> {
|
||||||
return sayHi(teamId, agent, prompt)
|
return sayHi(teamId, agent, prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user