diff --git a/.vscode/settings.json b/.vscode/settings.json index a2c1a48..77902a6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "agentic", "contenteditable", "deepseek", "historychange", diff --git a/packages/core/src/PageAgentCore.test.ts b/packages/core/src/PageAgentCore.test.ts index 1739026..5dc8010 100644 --- a/packages/core/src/PageAgentCore.test.ts +++ b/packages/core/src/PageAgentCore.test.ts @@ -130,6 +130,16 @@ describe.concurrent('PageAgentCore lifecycle', () => { expect(fetchMock).toHaveBeenCalledTimes(1) }) + it('completes (not errors) when the LLM reports task failure', async () => { + const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('gave up', false)) + const agent = createAgent(fetchMock) + + const result = await agent.execute('do something') + + expect(result).toMatchObject({ success: false, data: 'gave up' }) + expect(agent.status).toBe('completed') + }) + it('throws when a task is already running', async () => { const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse()) const agent = createAgent(fetchMock) @@ -137,7 +147,7 @@ describe.concurrent('PageAgentCore lifecycle', () => { await expect(agent.execute('second')).rejects.toThrow('A task is already running.') - agent.stop() + await agent.stop() await result }) }) @@ -150,20 +160,30 @@ describe.concurrent('PageAgentCore lifecycle', () => { const agent = createAgent(fetchMock) const { result: firstTask } = await startBlockedTask(agent) - agent.stop() + await agent.stop() + expect(agent.status).toBe('stopped') await expect(firstTask).resolves.toMatchObject({ success: false, data: 'Task aborted' }) const secondTask = await agent.execute('second') expect(secondTask).toMatchObject({ success: true, data: 'second task' }) + expect(agent.status).toBe('completed') }) - it('is a no-op when no task is running', () => { + it('resolves only after the run has fully settled', async () => { + const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse()) + const agent = createAgent(fetchMock) + const { result } = await startBlockedTask(agent) + + await agent.stop() + expect(agent.status).toBe('stopped') + await expect(result).resolves.toMatchObject({ success: false }) + }) + + it('is a no-op when no task is running', async () => { const agent = createAgent(createFetchMock()) - expect(() => { - agent.stop() - agent.stop() - }).not.toThrow() + await expect(agent.stop()).resolves.toBeUndefined() + await expect(agent.stop()).resolves.toBeUndefined() expect(agent.status).toBe('idle') }) }) @@ -222,17 +242,85 @@ describe.concurrent('PageAgentCore lifecycle', () => { expect(result.success).toBe(false) expect(agent.status).toBe('error') }) + + it('re-throws and sets error status when onBeforeTask throws', async () => { + const agent = createAgent(createFetchMock(), { + onBeforeTask: async () => { + throw new Error('setup failed') + }, + }) + + await expect(agent.execute('do something')).rejects.toThrow('setup failed') + expect(agent.status).toBe('error') + expect(agent.history.some((e) => e.type === 'error')).toBe(false) + }) + + it('re-throws and sets error status when onAfterTask throws', async () => { + const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('all done')) + const agent = createAgent(fetchMock, { + onAfterTask: async () => { + throw new Error('teardown failed') + }, + }) + + await expect(agent.execute('do something')).rejects.toThrow('teardown failed') + expect(agent.status).toBe('error') + }) + + it('stays reusable after onBeforeTask throws', async () => { + const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('second')) + let failOnce = true + const agent = createAgent(fetchMock, { + onBeforeTask: async () => { + if (failOnce) { + failOnce = false + throw new Error('setup failed') + } + }, + }) + + await expect(agent.execute('first')).rejects.toThrow('setup failed') + const result = await agent.execute('second') + expect(result).toMatchObject({ success: true, data: 'second' }) + }) + + it('re-throws and sets error status when onBeforeStep throws', async () => { + const agent = createAgent(createFetchMock(), { + onBeforeStep: async () => { + throw new Error('before step failed') + }, + }) + + await expect(agent.execute('do something')).rejects.toThrow('before step failed') + expect(agent.status).toBe('error') + expect(agent.history.some((e) => e.type === 'error')).toBe(false) + }) + + it('re-throws and sets error status when onAfterStep throws', async () => { + const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('all done')) + const agent = createAgent(fetchMock, { + onAfterStep: async () => { + throw new Error('after step failed') + }, + }) + + await expect(agent.execute('do something')).rejects.toThrow('after step failed') + expect(agent.status).toBe('error') + expect(agent.history.some((e) => e.type === 'error')).toBe(false) + }) }) describe('cancellation edge cases', () => { - it('rejects a new task while a stopped task is settling', async () => { + it('rejects a new task while a stop is still settling', async () => { const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse()) const agent = createAgent(fetchMock) const { result: firstTask } = await startBlockedTask(agent) - agent.stop() + const stopped = agent.stop() await expect(agent.execute('too early')).rejects.toThrow('A task is already running.') + + await stopped await expect(firstTask).resolves.toMatchObject({ success: false, data: 'Task aborted' }) expect(fetchMock).toHaveBeenCalledTimes(1) }) @@ -266,10 +354,12 @@ describe.concurrent('PageAgentCore lifecycle', () => { const task = agent.execute('run slow tool') await toolStarted - agent.stop() + const stopped = agent.stop() resolveTool() + await stopped await expect(task).resolves.toMatchObject({ success: false, data: 'Task aborted' }) + expect(agent.status).toBe('stopped') }) }) }) diff --git a/packages/core/src/PageAgentCore.ts b/packages/core/src/PageAgentCore.ts index 9189389..86af83f 100644 --- a/packages/core/src/PageAgentCore.ts +++ b/packages/core/src/PageAgentCore.ts @@ -21,7 +21,7 @@ import type { MacroToolInput, MacroToolResult, } from './types' -import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils' +import { assert, fetchLlmsTxt, normalizeResponse, suppress, uid, waitFor } from './utils' export { tool, type PageAgentTool } from './tools' export type * from './types' @@ -42,7 +42,7 @@ export type PageAgentCoreConfig = AgentConfig & { pageController: PageController * - loop * * ## Event System - * - `statuschange` - Agent status transitions (idle → running → completed/error) + * - `statuschange` - Agent status transitions (idle → running → completed/error/stopped) * - `historychange` - History events updated (persistent, part of agent memory) * - `activity` - Real-time activity feedback (transient, for UI only) * - `dispose` - Agent cleanup triggered @@ -91,6 +91,10 @@ export class PageAgentCore extends EventTarget { #abortController = new AbortController() #observations: string[] = [] + /** Resolves when the current run has fully settled. Awaited by `stop()`. */ + #running: Promise = Promise.resolve() + #lastResult: ExecutionResult | null = null + /** internal states during a single task execution */ #states = { /** Accumulated wait time in seconds */ @@ -147,13 +151,19 @@ export class PageAgentCore extends EventTarget { return this.#status } + /** Result of the most recent run, or `null` before the first run completes. */ + get lastResult(): ExecutionResult | null { + return this.#lastResult + } + /** Emit statuschange event */ #emitStatusChange(): void { this.dispatchEvent(new Event('statuschange')) } /** Emit historychange event */ - #emitHistoryChange(): void { + #emitHistoryChange(pushHistoricalEvent?: HistoricalEvent): void { + if (pushHistoricalEvent) this.history.push(pushHistoricalEvent) this.dispatchEvent(new Event('historychange')) } @@ -183,14 +193,22 @@ export class PageAgentCore extends EventTarget { this.#observations.push(content) } - /** Stop the current task. Agent remains reusable. */ - stop() { - this.pageController.cleanUpHighlights() - this.pageController.hideMask() + /** + * Stop the current task and wait until the run has fully settled (including lifecycle hooks). + * @note never await .stop() in a lifecycle hook. + */ + async stop(): Promise { + if (this.#status !== 'running') return this.#abortController.abort() + await this.#running } + /** + * external errors (pre-checks/config/hooks) will threw; + * agent errors will be caught and added to history, and return a failed result + */ async execute(task: string): Promise { + // pre-checks if (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.') if (this.#status === 'running') throw new Error('A task is already running.') if (!task) throw new Error('Task is required') @@ -202,135 +220,158 @@ export class PageAgentCore extends EventTarget { this.#observations = [] this.#states = { totalWaitTime: 0, lastURL: '', browserState: null } this.#abortController = new AbortController() + const signal = this.#abortController.signal + + let resolveRunning!: () => void + this.#running = new Promise((r) => (resolveRunning = r)) this.#setStatus('running') this.#emitHistoryChange() // Disable ask_user tool if onAskUser is not set - if (!this.onAskUser) { - this.tools.delete('ask_user') - } + if (!this.onAskUser) this.tools.delete('ask_user') const onBeforeStep = this.config.onBeforeStep const onAfterStep = this.config.onAfterStep const onBeforeTask = this.config.onBeforeTask const onAfterTask = this.config.onAfterTask - - try { - await onBeforeTask?.(this) - await this.pageController.showMask() - } catch (error) { - this.#setStatus('error') - throw error - } + const stepDelay = this.config.stepDelay ?? 0.4 + const maxSteps = this.config.maxSteps let step = 0 - let taskSuccess: boolean - let taskResult: string + let taskResult: ExecutionResult + let finalStatus: AgentStatus = 'error' - while (true) { - try { - console.group(`step: ${step}`) + await suppress(() => this.pageController.showMask()) + // graceful exit + try { + await onBeforeTask?.(this) + + while (true) { await onBeforeStep?.(this, step) - // observe + // handle internal agent errors + try { + console.group(`step: ${step}`) - console.log(chalk.blue.bold('👀 Observing...')) + // @note It's convenient to treat stepDelay as part of the next step. + // Maybe move it to a dedicated try block for better semantics? + if (step > 0) await waitFor(stepDelay, signal) - this.#states.browserState = await this.pageController.getBrowserState() - await this.#handleObservations(step) + signal.throwIfAborted() - // assemble prompts + // observe - const messages = [ - { role: 'system' as const, content: this.#getSystemPrompt() }, - { role: 'user' as const, content: await this.#assembleUserPrompt() }, - ] + console.log(chalk.blue.bold('👀 Observing...')) - const macroTool = { AgentOutput: this.#packMacroTool() } + this.#states.browserState = await this.pageController.getBrowserState() + await this.#handleObservations(step) - // invoke LLM + // assemble prompts - console.log(chalk.blue.bold('🧠 Thinking...')) - this.#emitActivity({ type: 'thinking' }) + const messages = [ + { role: 'system' as const, content: this.#getSystemPrompt() }, + { role: 'user' as const, content: await this.#assembleUserPrompt() }, + ] - const result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, { - toolChoiceName: 'AgentOutput', - normalizeResponse: (res) => normalizeResponse(res, this.tools), - }) + const macroTool = { AgentOutput: this.#packMacroTool() } - // assemble history + // invoke LLM - const macroResult = result.toolResult as MacroToolResult - const input = macroResult.input - const output = macroResult.output - const reflection: Partial = { - evaluation_previous_goal: input.evaluation_previous_goal, - memory: input.memory, - next_goal: input.next_goal, - } - const actionName = Object.keys(input.action)[0] - const action: AgentStepEvent['action'] = { - name: actionName, - input: input.action[actionName], - output: output, + console.log(chalk.blue.bold('🧠 Thinking...')) + this.#emitActivity({ type: 'thinking' }) + + const result = await this.#llm.invoke(messages, macroTool, signal, { + toolChoiceName: 'AgentOutput', + normalizeResponse: (res) => normalizeResponse(res, this.tools), + }) + + // assemble history + + const macroResult = result.toolResult as MacroToolResult + const input = macroResult.input + const output = macroResult.output + const reflection: Partial = { + evaluation_previous_goal: input.evaluation_previous_goal, + memory: input.memory, + next_goal: input.next_goal, + } + const actionName = Object.keys(input.action)[0] + const action: AgentStepEvent['action'] = { + name: actionName, + input: input.action[actionName], + output: output, + } + + this.#emitHistoryChange({ + type: 'step', + stepIndex: step, + reflection, + action, + usage: result.usage, + rawResponse: result.rawResponse, + rawRequest: result.rawRequest, + }) + + if (actionName === 'done') { + const success = action.input?.success ?? false + const data = action.input?.text || 'no text provided' + console.log(chalk.green.bold('Task completed'), success, data) + taskResult = { success, data, history: this.history } + this.#lastResult = taskResult + finalStatus = 'completed' + break + } + } catch (error: unknown) { + // catch block must not throw error. otherwise the error may be overridden if finally block also throws error. + + const isAbortError = (error as any)?.name === 'AbortError' + if (!isAbortError) console.error('Task failed', error) + const message = isAbortError ? 'Task aborted' : String(error) + this.#emitActivity({ type: 'error', message: message }) + this.#emitHistoryChange({ type: 'error', message: message, rawResponse: error }) + taskResult = { success: false, data: message, history: this.history } + this.#lastResult = taskResult + finalStatus = isAbortError ? 'stopped' : 'error' + break + } finally { + // finally block runs before the break above. + + console.groupEnd() + // @note hook may throw error. + // which will override the `break` above and be handled as an external error. + // as expected. + await onAfterStep?.(this, this.history) } - this.history.push({ - type: 'step', - stepIndex: step, - reflection, - action, - usage: result.usage, - rawResponse: result.rawResponse, - rawRequest: result.rawRequest, - } as AgentStepEvent) - this.#emitHistoryChange() - - await onAfterStep?.(this, this.history) - - console.groupEnd() - - if (actionName === 'done') { - taskSuccess = action.input?.success ?? false - taskResult = action.input?.text || 'no text provided' - console.log(chalk.green.bold('Task completed'), taskSuccess, taskResult) + step++ + if (step > maxSteps) { + const message = 'Step count exceeded maximum limit' + console.error(message) + this.#emitActivity({ type: 'error', message: message }) + this.#emitHistoryChange({ type: 'error', message: message }) + taskResult = { success: false, data: message, history: this.history } + this.#lastResult = taskResult + finalStatus = 'error' break } - } catch (error: unknown) { - console.groupEnd() - const isAbortError = (error as any)?.name === 'AbortError' - if (!isAbortError) console.error('Task failed', error) - taskResult = isAbortError ? 'Task aborted' : String(error) - taskSuccess = false - this.#emitActivity({ type: 'error', message: taskResult }) - this.history.push({ type: 'error', message: taskResult, rawResponse: error }) - this.#emitHistoryChange() - break - } + } // while - step++ - if (step > this.config.maxSteps) { - taskResult = 'Step count exceeded maximum limit' - taskSuccess = false - this.#emitActivity({ type: 'error', message: taskResult }) - this.history.push({ type: 'error', message: taskResult }) - this.#emitHistoryChange() - break - } + await onAfterTask?.(this, taskResult) - await waitFor(this.config.stepDelay ?? 0.4) + return taskResult + } catch (error) { + this.#emitActivity({ type: 'error', message: String(error) }) + finalStatus = 'error' + throw error + } finally { + await suppress(() => this.pageController.cleanUpHighlights()) + await suppress(() => this.pageController.hideMask()) + this.#abortController.abort() + resolveRunning() + this.#setStatus(finalStatus) } - - this.#onDone(taskSuccess) - const result: ExecutionResult = { - success: taskSuccess, - data: taskResult, - history: this.history, - } - await onAfterTask?.(this, result) - return result } /** @@ -605,13 +646,6 @@ export class PageAgentCore extends EventTarget { return prompt } - #onDone(success = true) { - this.pageController.cleanUpHighlights() - this.pageController.hideMask() // No await - fire and forget - this.#setStatus(success ? 'completed' : 'error') - this.#abortController.abort() - } - dispose() { console.log('Disposing PageAgent...') this.disposed = true diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9daff76..cdce4cb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -262,9 +262,9 @@ export type HistoricalEvent = | AgentErrorEvent /** - * Agent execution status + * Agent lifecycle status. */ -export type AgentStatus = 'idle' | 'running' | 'completed' | 'error' +export type AgentStatus = 'idle' | 'running' | 'completed' | 'error' | 'stopped' /** * Agent activity - transient state for immediate UI feedback. diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index f030864..fe74fb1 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -129,3 +129,15 @@ export function assert(condition: unknown, message?: string, silent?: boolean): throw new Error(errorMessage) } } + +/** + * Suppress errors from a function. + */ +export async function suppress(fn: () => T | Promise): Promise | undefined> { + try { + return await fn() + } catch (error) { + console.error(error) + return undefined + } +} diff --git a/packages/extension/docs/extension_api.md b/packages/extension/docs/extension_api.md index ab661d2..bb48461 100644 --- a/packages/extension/docs/extension_api.md +++ b/packages/extension/docs/extension_api.md @@ -131,7 +131,7 @@ export type Execute = (task: string, config: ExecuteConfig) => Promise { await tabsController.init(agent.task, { includeInitialTab, experimentalIncludeAllTabs }) - - heartBeatInterval = window.setInterval(() => { - chrome.storage.local.set({ - agentHeartbeat: Date.now(), - }) - }, 1_000) - - await chrome.storage.local.set({ - isAgentRunning: true, - }) - }, - - onAfterTask: async () => { - if (heartBeatInterval) { - window.clearInterval(heartBeatInterval) - heartBeatInterval = null - } - - await chrome.storage.local.set({ - isAgentRunning: false, - }) }, onBeforeStep: async (agent) => { @@ -87,16 +69,28 @@ export class MultiPageAgent extends PageAgentCore { onDispose: () => { if (heartBeatInterval) { - window.clearInterval(heartBeatInterval) + clearInterval(heartBeatInterval) heartBeatInterval = null } - - chrome.storage.local.set({ - isAgentRunning: false, - }) + chrome.storage.local.set({ isAgentRunning: false }).catch(console.error) tabsController.dispose() }, }) + + this.addEventListener('statuschange', () => { + const running = this.status === 'running' + + if (running && !heartBeatInterval) { + heartBeatInterval = window.setInterval(() => { + void chrome.storage.local.set({ agentHeartbeat: Date.now() }) + }, 1_000) + } else if (!running && heartBeatInterval) { + clearInterval(heartBeatInterval) + heartBeatInterval = null + } + + chrome.storage.local.set({ isAgentRunning: running }).catch(console.error) + }) } } diff --git a/packages/extension/src/agent/useAgent.ts b/packages/extension/src/agent/useAgent.ts index cfc7702..23c8ba3 100644 --- a/packages/extension/src/agent/useAgent.ts +++ b/packages/extension/src/agent/useAgent.ts @@ -80,7 +80,7 @@ export function useAgent(): UseAgentResult { const handleStatusChange = (e: Event) => { const newStatus = agent.status as AgentStatus setStatus(newStatus) - if (newStatus === 'idle' || newStatus === 'completed' || newStatus === 'error') { + if (newStatus !== 'running') { setActivity(null) } } diff --git a/packages/extension/src/components/misc.tsx b/packages/extension/src/components/misc.tsx index 66bfa48..1779e02 100644 --- a/packages/extension/src/components/misc.tsx +++ b/packages/extension/src/components/misc.tsx @@ -14,6 +14,7 @@ export function StatusDot({ status }: { status: AgentStatus }) { running: 'bg-blue-500', completed: 'bg-green-500', error: 'bg-destructive', + stopped: 'bg-muted-foreground', }[status] const label = { @@ -21,6 +22,7 @@ export function StatusDot({ status }: { status: AgentStatus }) { running: 'Running', completed: 'Done', error: 'Error', + stopped: 'Stopped', }[status] return ( diff --git a/packages/extension/src/entrypoints/sidepanel/App.tsx b/packages/extension/src/entrypoints/sidepanel/App.tsx index 8fe5c17..e54948f 100644 --- a/packages/extension/src/entrypoints/sidepanel/App.tsx +++ b/packages/extension/src/entrypoints/sidepanel/App.tsx @@ -39,7 +39,7 @@ export default function App() { if ( prev === 'running' && - (status === 'completed' || status === 'error') && + (status === 'completed' || status === 'error' || status === 'stopped') && history.length > 0 && currentTask ) { diff --git a/packages/extension/src/lib/db.ts b/packages/extension/src/lib/db.ts index 9e520a8..e0db44c 100644 --- a/packages/extension/src/lib/db.ts +++ b/packages/extension/src/lib/db.ts @@ -8,7 +8,7 @@ export interface SessionRecord { id: string task: string history: HistoricalEvent[] - status: 'completed' | 'error' + status: 'completed' | 'error' | 'stopped' createdAt: number } diff --git a/packages/ui/src/panel/Panel.ts b/packages/ui/src/panel/Panel.ts index 3972839..a65d934 100644 --- a/packages/ui/src/panel/Panel.ts +++ b/packages/ui/src/panel/Panel.ts @@ -100,10 +100,10 @@ export class Panel { #handleStatusChange(): void { const status = this.#agent.status - // Map agent status to UI indicator type - const indicatorType = - status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status - this.#updateStatusIndicator(indicatorType) + // Map agent status to UI indicator. A `completed` run whose result reports + // failure shows as error; other statuses map to their own indicator. + const failed = status === 'completed' && this.#agent.lastResult?.success === false + this.#updateStatusIndicator(failed ? 'error' : status) // Morph action button: running = stop (■), not running = close (X) if (status === 'running') { @@ -121,7 +121,7 @@ export class Panel { } // Handle completion - if (status === 'completed' || status === 'error') { + if (status === 'completed' || status === 'error' || status === 'stopped') { if (!this.#isExpanded) { this.#expand() } @@ -376,7 +376,7 @@ export class Panel { } const status = this.#agent.status - const isTaskEnded = status === 'completed' || status === 'error' + const isTaskEnded = status === 'completed' || status === 'error' || status === 'stopped' // Only show input area after task completion if configured to do so if (isTaskEnded) { @@ -559,13 +559,23 @@ export class Panel { } #updateStatusIndicator( - type: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error' + type: + | 'idle' + | 'running' + | 'thinking' + | 'executing' + | 'executed' + | 'retrying' + | 'completed' + | 'error' + | 'stopped' ): void { - // Clear all status classes + // `running` animates like thinking; `idle`/`stopped` use the neutral base. + const variant = type === 'running' ? 'thinking' : type this.#indicator.className = styles.indicator - - // Add corresponding status class - this.#indicator.classList.add(styles[type]) + if (variant !== 'idle' && variant !== 'stopped') { + this.#indicator.classList.add(styles[variant]) + } } #scrollToBottom(): void { diff --git a/packages/ui/src/panel/types.ts b/packages/ui/src/panel/types.ts index 9e65b8a..d3ac663 100644 --- a/packages/ui/src/panel/types.ts +++ b/packages/ui/src/panel/types.ts @@ -22,14 +22,17 @@ export type AgentActivity = * This enables decoupling and allows any agent implementation to work with Panel. * * Events: - * - 'statuschange': Agent status changed (idle/running/completed/error) + * - 'statuschange': Agent status changed * - 'historychange': Historical events updated (persisted) * - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc) * - 'dispose': Agent is being disposed */ export interface PanelAgentAdapter extends EventTarget { /** Current agent status */ - readonly status: 'idle' | 'running' | 'completed' | 'error' + readonly status: 'idle' | 'running' | 'completed' | 'error' | 'stopped' + + /** Result of the most recent run, or `null` before the first run completes */ + readonly lastResult: { success: boolean } | null /** History of agent events */ readonly history: readonly { @@ -71,7 +74,7 @@ export interface PanelAgentAdapter extends EventTarget { execute(task: string): Promise /** Stop the current task (agent remains reusable) */ - stop(): void + stop(): Promise /** Dispose the agent (terminal, cannot be reused) */ dispose(): void diff --git a/packages/website/src/pages/docs/advanced/custom-ui/page.tsx b/packages/website/src/pages/docs/advanced/custom-ui/page.tsx index 3ebc83c..788a32d 100644 --- a/packages/website/src/pages/docs/advanced/custom-ui/page.tsx +++ b/packages/website/src/pages/docs/advanced/custom-ui/page.tsx @@ -128,8 +128,8 @@ export default function CustomUIDocs() { name: 'statuschange', type: 'Event', description: isZh - ? 'Agent 状态变化 (idle → running → completed/error)' - : 'Agent status changes (idle → running → completed/error)', + ? 'Agent 状态变化 (idle → running → completed/error/stopped)' + : 'Agent status changes (idle → running → completed/error/stopped)', }, { name: 'historychange', diff --git a/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx b/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx index 4a08017..40f5cf1 100644 --- a/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx +++ b/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx @@ -281,8 +281,8 @@ const result = await agent.execute('Fill in the form with test data')`}

{isZh - ? '这些接口高度实验性,可能在未来版本中发生变化。' - : 'These APIs are highly experimental and may change in future versions. '} + ? '这些接口高度实验性,可能在未来版本中发生变化。钩子中抛出的错误会使任务失败并从 execute() 抛出;如不希望影响任务,请在钩子内部自行捕获。' + : 'These APIs are highly experimental and may change in future versions. Errors thrown from hooks fail the run and propagate from execute(); catch errors inside the hook if the task should not be affected.'}

', description: isZh - ? '停止当前任务。Agent 仍可复用。' - : 'Stop the current task. Agent remains reusable.', + ? '停止当前任务,并在任务完全结束后 resolve。Agent 仍可复用。' + : 'Stop the current task; resolves once the run has fully settled. Agent remains reusable.', }, { name: 'dispose()', @@ -415,8 +415,8 @@ const result = await agent.execute('Fill in the form with test data')`} name: 'statuschange', type: 'Event', description: isZh - ? 'Agent 状态变化时触发 (idle → running → completed/error)' - : 'Fired when agent status changes (idle → running → completed/error)', + ? 'Agent 状态变化时触发 (idle → running → completed/error/stopped)' + : 'Fired when agent status changes (idle → running → completed/error/stopped)', }, { name: 'historychange',