mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Avoid noisy stopped states for background agents
Goal sessions were showing stopped background-agent cards because the parent model used TaskStop after reading enough partial review output. That writes a killed task notification, which the desktop then rendered like a failure. TaskStop and async agent launch guidance now make cancellation an exceptional action rather than a normal cleanup step. Stopped background-agent transcript cards also render as neutral interrupted events instead of error-styled failures. Constraint: Background agent task notifications must remain truthful; killed tasks still surface as stopped instead of being hidden. Rejected: Hide stopped notifications in /goal sessions | this would mask real user cancellations and runaway-task stops. Confidence: medium Scope-risk: narrow Directive: Do not encourage parent turns to kill background agents only because partial output was read. Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx -t "renders stopped background agents as neutral transcript events" Tested: cd desktop && bun run lint Tested: bun run check:server
This commit is contained in:
parent
e9ac5739bc
commit
d2d27b3845
@ -251,6 +251,36 @@ describe('MessageList nested tool calls', () => {
|
||||
expect(card.textContent).toContain('45s')
|
||||
})
|
||||
|
||||
it('renders stopped background agents as neutral transcript events', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
messages: [{
|
||||
id: 'background-task-agent-stopped',
|
||||
type: 'background_task',
|
||||
timestamp: 2,
|
||||
task: {
|
||||
taskId: 'agent-task-stopped',
|
||||
toolUseId: 'agent-tool-stopped',
|
||||
status: 'stopped',
|
||||
taskType: 'local_agent',
|
||||
summary: 'Agent "Code review for todo app" was stopped',
|
||||
startedAt: 1,
|
||||
updatedAt: 2,
|
||||
},
|
||||
}],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
render(<MessageList />)
|
||||
|
||||
const card = screen.getByTestId('background-task-event-card')
|
||||
expect(card.getAttribute('data-status')).toBe('stopped')
|
||||
expect(card.textContent).toContain('stopped')
|
||||
expect(card.querySelector('.text-\\[var\\(--color-error\\)\\]')).toBeNull()
|
||||
})
|
||||
|
||||
it('restores the full transcript when scrolling away from latest', async () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useRef, useEffect, useMemo, memo, useState, useCallback, useLayoutEffect, type ReactNode } from 'react'
|
||||
import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, LoaderCircle, Settings, Target, XCircle } from 'lucide-react'
|
||||
import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, CircleStop, LoaderCircle, Settings, Target, XCircle } from 'lucide-react'
|
||||
import { ApiError } from '../../api/client'
|
||||
import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
@ -233,7 +233,8 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })
|
||||
const t = useTranslation()
|
||||
const { task } = message
|
||||
const isRunning = task.status === 'running'
|
||||
const isFailed = task.status === 'failed' || task.status === 'stopped'
|
||||
const isFailed = task.status === 'failed'
|
||||
const isStopped = task.status === 'stopped'
|
||||
const duration = formatBackgroundTaskDuration(task.usage?.durationMs)
|
||||
const detail = task.summary || task.lastToolName || task.description || task.outputFile || task.taskId
|
||||
|
||||
@ -241,6 +242,7 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })
|
||||
<div className="mb-2">
|
||||
<div
|
||||
data-testid="background-task-event-card"
|
||||
data-status={task.status}
|
||||
className="flex min-w-0 items-start gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-2"
|
||||
>
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
@ -248,6 +250,8 @@ function BackgroundTaskEventCard({ message }: { message: BackgroundTaskEvent })
|
||||
<LoaderCircle size={15} strokeWidth={2.25} className="animate-spin text-[var(--color-accent)]" aria-hidden="true" />
|
||||
) : isFailed ? (
|
||||
<XCircle size={15} strokeWidth={2.25} className="text-[var(--color-error)]" aria-hidden="true" />
|
||||
) : isStopped ? (
|
||||
<CircleStop size={15} strokeWidth={2.25} className="text-[var(--color-text-tertiary)]" aria-hidden="true" />
|
||||
) : (
|
||||
<CheckCircle2 size={15} strokeWidth={2.25} className="text-[var(--color-success)]" aria-hidden="true" />
|
||||
)}
|
||||
|
||||
@ -1322,7 +1322,8 @@ The agent is now running and will receive instructions via mailbox.`
|
||||
}
|
||||
if (data.status === 'async_launched') {
|
||||
const prefix = `Async agent launched successfully.\nagentId: ${data.agentId} (internal ID - do not mention to user. Use SendMessage with to: '${data.agentId}' to continue this agent.)\nThe agent is working in the background. You will be notified automatically when it completes.`;
|
||||
const instructions = data.canReadOutputFile ? `Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: ${data.outputFile}\nIf asked, you can check progress before completion by using ${FILE_READ_TOOL_NAME} or ${BASH_TOOL_NAME} tail on the output file.` : `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.`;
|
||||
const stopGuidance = `Do not stop this agent just because you have enough partial output. Stop it only if the user asks to cancel it, or if it is clearly runaway, harmful, duplicative, or no longer useful.`;
|
||||
const instructions = data.canReadOutputFile ? `Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\n${stopGuidance}\noutput_file: ${data.outputFile}\nIf asked, you can check progress before completion by using ${FILE_READ_TOOL_NAME} or ${BASH_TOOL_NAME} tail on the output file.` : `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.\n${stopGuidance}`;
|
||||
const text = `${prefix}\n${instructions}`;
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
|
||||
@ -4,5 +4,9 @@ export const DESCRIPTION = `
|
||||
- Stops a running background task by its ID
|
||||
- Takes a task_id parameter identifying the task to stop
|
||||
- Returns a success or failure status
|
||||
- Use this tool when you need to terminate a long-running task
|
||||
- Use this tool only when the user explicitly asks to cancel/stop a task, or when
|
||||
the task is clearly runaway, harmful, duplicative, or no longer useful.
|
||||
- Do not stop a background agent merely because you have already read enough of
|
||||
its output. Prefer waiting for the automatic completion notification or
|
||||
leaving it running while you summarize available progress.
|
||||
`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user