feat(autofix-pr): 内容回流让本地模型读到 PR 修复结果
Phase 3 of remote-agent completion loop。Phase 2 注册了 completionChecker
让框架能在 PR 合并/关闭/有 push+CI 绿时主动完成 task,但 task-notification
仍然只携带 generic 文本(""${owner}/${repo}#42 merged"")。Phase 3 让本地
模型读到远端 agent 自己产出的结构化结果(commits 列表、files 列表、CI
状态、人类可读 summary)。
实现:
- 新增 extractAutofixResultFromLog (src/commands/autofix-pr/
extractAutofixResult.ts):从 SDKMessage[] 中扫 <autofix-result> tag,
优先 hook stdout 后 fallback assistant text,latest-wins。10 个单测。
- RemoteAgentTask 新增 registerContentExtractor 注册式 API + 私有
enqueueRichRemoteNotification(参考 enqueueRemoteReviewNotification),
在 3 个 generic 完成路径(archived / completionChecker / result-driven)
先尝试 tryExtractRichContent,有内容用 rich 变体,没有走 generic。
isRemoteReview 路径不变(它走自己的 enqueueRemoteReviewNotification)。
- launchAutofixPr.ts 模块顶部 registerContentExtractor('autofix-pr',
extractAutofixResultFromLog)。initialMessage 加 <autofix-result> 输出
指令(pr-number / commits-pushed / files-changed / ci-status / summary)。
设计:
- 注册式 API(同 Phase 1 hook + Phase 2 checker):framework 不反向依赖
命令模块,所有 PR-specific 逻辑在 autofix-pr/
- latest-wins:agent 重试时只取最新 tag,旧 tag 不会污染
- truncated tag → null:开 tag 无对应闭 tag 视为不完整,走 generic
fallback
- 跨 message 不拼接:开 tag 和闭 tag 在不同 message 视为不完整(避免
误拼字符串)
- 字符串 content 不解析:assistant.message.content 为 string(非 block
array)的少见路径直接 skip,不 crash
测试:
- extractAutofixResultFromLog 10 个单测(空 log / 无 tag / hook stdout /
assistant text / hook_response subtype / 多 tag latest-wins / 截断 /
hook 后于 assistant 的优先级 / 跨 message 不拼接 / 字符串 content
graceful)
- launchAutofixPr 3 个新测试(extractor 注册 / initialMessage 含 tag
schema / extractor 真实行为)
完整方案见 docs/features/remote-agent-completion-analysis.md 第 5.3 节。
This commit is contained in:
parent
31433ac958
commit
873f0a03ac
123
src/commands/autofix-pr/__tests__/extractAutofixResult.test.ts
Normal file
123
src/commands/autofix-pr/__tests__/extractAutofixResult.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
import type { SDKMessage } from '../../../entrypoints/agentSdkTypes.js'
|
||||
import {
|
||||
AUTOFIX_RESULT_TAG,
|
||||
extractAutofixResultFromLog,
|
||||
} from '../extractAutofixResult.js'
|
||||
|
||||
function hookProgressMessage(stdout: string): SDKMessage {
|
||||
return {
|
||||
type: 'system',
|
||||
subtype: 'hook_progress',
|
||||
stdout,
|
||||
} as unknown as SDKMessage
|
||||
}
|
||||
|
||||
function assistantTextMessage(text: string): SDKMessage {
|
||||
return {
|
||||
type: 'assistant',
|
||||
message: {
|
||||
content: [{ type: 'text', text }],
|
||||
},
|
||||
} as unknown as SDKMessage
|
||||
}
|
||||
|
||||
const sampleTag = (summary: string): string =>
|
||||
`<${AUTOFIX_RESULT_TAG}>
|
||||
<pr-number>42</pr-number>
|
||||
<commits-pushed>
|
||||
<commit sha="abc123">${summary}</commit>
|
||||
</commits-pushed>
|
||||
<ci-status>green</ci-status>
|
||||
<summary>${summary}</summary>
|
||||
</${AUTOFIX_RESULT_TAG}>`
|
||||
|
||||
describe('extractAutofixResultFromLog', () => {
|
||||
test('returns null on empty log', () => {
|
||||
expect(extractAutofixResultFromLog([])).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null when no tag present', () => {
|
||||
const log = [
|
||||
assistantTextMessage('just some normal text without the tag'),
|
||||
hookProgressMessage('hook output without tag'),
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBeNull()
|
||||
})
|
||||
|
||||
test('extracts from hook stdout', () => {
|
||||
const tag = sampleTag('fixed lint error')
|
||||
const log = [hookProgressMessage(`prefix\n${tag}\nsuffix`)]
|
||||
const result = extractAutofixResultFromLog(log)
|
||||
expect(result).toBe(tag)
|
||||
})
|
||||
|
||||
test('extracts from assistant text', () => {
|
||||
const tag = sampleTag('typecheck fixed')
|
||||
const log = [assistantTextMessage(`Done!\n${tag}`)]
|
||||
expect(extractAutofixResultFromLog(log)).toBe(tag)
|
||||
})
|
||||
|
||||
test('extracts from hook_response subtype too', () => {
|
||||
const tag = sampleTag('via hook_response')
|
||||
const log = [
|
||||
{
|
||||
type: 'system',
|
||||
subtype: 'hook_response',
|
||||
stdout: tag,
|
||||
} as unknown as SDKMessage,
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBe(tag)
|
||||
})
|
||||
|
||||
test('returns the latest tag when multiple appear in different messages', () => {
|
||||
const older = sampleTag('older attempt')
|
||||
const newer = sampleTag('newer attempt')
|
||||
const log = [
|
||||
assistantTextMessage(`first try\n${older}`),
|
||||
assistantTextMessage(`retry\n${newer}`),
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBe(newer)
|
||||
})
|
||||
|
||||
test('returns null when open tag exists but close tag is missing (truncated)', () => {
|
||||
const log = [
|
||||
assistantTextMessage(
|
||||
`<${AUTOFIX_RESULT_TAG}>\n<summary>got cut off mid-write...`,
|
||||
),
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBeNull()
|
||||
})
|
||||
|
||||
test('walks backwards so hook stdout from later in log wins over earlier assistant text', () => {
|
||||
const earlier = sampleTag('via assistant first')
|
||||
const later = sampleTag('via hook later')
|
||||
const log = [
|
||||
assistantTextMessage(`some output\n${earlier}`),
|
||||
hookProgressMessage(later),
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBe(later)
|
||||
})
|
||||
|
||||
test('ignores tag-shaped strings that span across messages (no concatenation)', () => {
|
||||
// Open tag in one message, close tag in another — should NOT be stitched.
|
||||
const log = [
|
||||
assistantTextMessage(`<${AUTOFIX_RESULT_TAG}>\n<summary>part 1`),
|
||||
assistantTextMessage(`part 2</summary>\n</${AUTOFIX_RESULT_TAG}>`),
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBeNull()
|
||||
})
|
||||
|
||||
test('extracts when assistant content is a string (not block array)', () => {
|
||||
// Some SDK paths emit assistant content as a raw string instead of
|
||||
// a content-block array. Current implementation skips those — verify
|
||||
// graceful no-op rather than crash.
|
||||
const log = [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: { content: sampleTag('string content') },
|
||||
} as unknown as SDKMessage,
|
||||
]
|
||||
expect(extractAutofixResultFromLog(log)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -65,12 +65,16 @@ const registerCompletionCheckerMock = mock<
|
|||
checker: (metadata?: unknown) => Promise<string | null>,
|
||||
) => void
|
||||
>(() => {})
|
||||
const registerContentExtractorMock = mock<
|
||||
(taskType: string, extractor: (log: unknown[]) => string | null) => void
|
||||
>(() => {})
|
||||
|
||||
mock.module('src/tasks/RemoteAgentTask/RemoteAgentTask.js', () => ({
|
||||
checkRemoteAgentEligibility: checkEligibilityMock,
|
||||
registerRemoteAgentTask: registerMock,
|
||||
registerCompletionHook: registerCompletionHookMock,
|
||||
registerCompletionChecker: registerCompletionCheckerMock,
|
||||
registerContentExtractor: registerContentExtractorMock,
|
||||
getRemoteTaskSessionUrl: getSessionUrlMock,
|
||||
formatPreconditionError: (e: { type: string }) => e.type,
|
||||
}))
|
||||
|
|
@ -525,6 +529,60 @@ describe('callAutofixPr · Phase 2 completionChecker integration', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// Phase 3: content extractor wiring + initialMessage tag instruction
|
||||
describe('callAutofixPr · Phase 3 content extractor integration', () => {
|
||||
test('registerContentExtractor is called at module load with autofix-pr type', () => {
|
||||
const calls = registerContentExtractorMock.mock.calls.filter(
|
||||
c => c[0] === 'autofix-pr',
|
||||
)
|
||||
expect(calls.length).toBeGreaterThan(0)
|
||||
const extractor = calls[calls.length - 1]?.[1]
|
||||
expect(typeof extractor).toBe('function')
|
||||
})
|
||||
|
||||
test('initialMessage instructs the remote agent to emit an <autofix-result> tag', async () => {
|
||||
await callAutofixPr(onDone, makeContext(), '42')
|
||||
// teleportMock's typed signature has no args, so calls[0] is a
|
||||
// zero-length tuple. We know teleportToRemote is invoked with one
|
||||
// options object, so double-cast through unknown to read the args.
|
||||
const calls = teleportMock.mock.calls as unknown as Array<
|
||||
[{ initialMessage?: string }]
|
||||
>
|
||||
const teleportArgs = calls[0]?.[0]
|
||||
expect(teleportArgs?.initialMessage).toContain('<autofix-result>')
|
||||
expect(teleportArgs?.initialMessage).toContain('</autofix-result>')
|
||||
expect(teleportArgs?.initialMessage).toContain('<ci-status>')
|
||||
expect(teleportArgs?.initialMessage).toContain('<summary>')
|
||||
})
|
||||
|
||||
test('registered extractor returns string for valid log and null for empty', () => {
|
||||
const calls = registerContentExtractorMock.mock.calls.filter(
|
||||
c => c[0] === 'autofix-pr',
|
||||
)
|
||||
const extractor = calls[calls.length - 1]?.[1] as
|
||||
| ((log: unknown[]) => string | null)
|
||||
| undefined
|
||||
expect(extractor).toBeDefined()
|
||||
// Empty log → null
|
||||
expect(extractor?.([])).toBeNull()
|
||||
// Log with assistant text containing tag → returns it
|
||||
const logWithTag = [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'done\n<autofix-result><summary>x</summary></autofix-result>',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
expect(extractor?.(logWithTag)).toContain('<autofix-result>')
|
||||
})
|
||||
})
|
||||
|
||||
// Cover ../index.ts load() — placed in this test file so all the heavy mocks
|
||||
// (teleport / detectRepository / RemoteAgentTask / bootstrap-state / analytics /
|
||||
// skillDetect) are already registered when load() dynamically imports
|
||||
|
|
|
|||
84
src/commands/autofix-pr/extractAutofixResult.ts
Normal file
84
src/commands/autofix-pr/extractAutofixResult.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Extract the <autofix-result> tag from a remote autofix-pr session log.
|
||||
//
|
||||
// The remote agent emits a structured XML block as its final message
|
||||
// (initialMessage in launchAutofixPr.ts instructs it to). The tag carries
|
||||
// PR-specific outcome data — commits pushed, files changed, CI status,
|
||||
// summary — that the framework's generic "task completed" notification
|
||||
// can't convey. We surface it to the local model by injecting the tag
|
||||
// verbatim into the message queue (analogous to <remote-review> handling).
|
||||
//
|
||||
// Resilient to two production realities:
|
||||
// 1. The tag may appear in either an assistant text block or a hook
|
||||
// stdout (some autofix skills wrap the final report in a hook).
|
||||
// 2. The tag may not appear at all (older agents, truncated runs) —
|
||||
// caller falls back to generic completion notification.
|
||||
|
||||
import type {
|
||||
SDKAssistantMessage,
|
||||
SDKMessage,
|
||||
} from '../../entrypoints/agentSdkTypes.js'
|
||||
|
||||
export const AUTOFIX_RESULT_TAG = 'autofix-result'
|
||||
|
||||
const TAG_OPEN = `<${AUTOFIX_RESULT_TAG}>`
|
||||
const TAG_CLOSE = `</${AUTOFIX_RESULT_TAG}>`
|
||||
|
||||
/**
|
||||
* Walk the session log for an <autofix-result> tag. Returns the full tag
|
||||
* (including delimiters) so the caller can inject it as-is into the
|
||||
* notification; returns null if no tag is present.
|
||||
*
|
||||
* Search order:
|
||||
* 1. Latest hook_progress / hook_response stdout (autofix skills that
|
||||
* use hooks to format the report write here first).
|
||||
* 2. Latest assistant text block (agents that don't use hooks write the
|
||||
* tag inline in their final message).
|
||||
*
|
||||
* Latest-wins so re-tries within the same session don't surface stale
|
||||
* earlier results.
|
||||
*/
|
||||
export function extractAutofixResultFromLog(log: SDKMessage[]): string | null {
|
||||
// Walk backwards so we hit the most recent tag first.
|
||||
for (let i = log.length - 1; i >= 0; i--) {
|
||||
const msg = log[i]
|
||||
if (!msg) continue
|
||||
|
||||
// Hook stdout (system messages of subtype hook_progress / hook_response).
|
||||
if (
|
||||
msg.type === 'system' &&
|
||||
(msg.subtype === 'hook_progress' || msg.subtype === 'hook_response')
|
||||
) {
|
||||
const stdout = (msg as { stdout?: unknown }).stdout
|
||||
if (typeof stdout === 'string') {
|
||||
const extracted = extractBetween(stdout, TAG_OPEN, TAG_CLOSE)
|
||||
if (extracted) return extracted
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Assistant text blocks.
|
||||
if (msg.type === 'assistant') {
|
||||
const content = (msg as SDKAssistantMessage).message?.content
|
||||
if (!content || typeof content === 'string') continue
|
||||
for (const block of content as Array<{ type: string; text?: string }>) {
|
||||
if (block.type !== 'text' || typeof block.text !== 'string') continue
|
||||
if (!block.text.includes(TAG_OPEN)) continue
|
||||
const extracted = extractBetween(block.text, TAG_OPEN, TAG_CLOSE)
|
||||
if (extracted) return extracted
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function extractBetween(
|
||||
text: string,
|
||||
open: string,
|
||||
close: string,
|
||||
): string | null {
|
||||
const start = text.lastIndexOf(open)
|
||||
if (start === -1) return null
|
||||
const end = text.indexOf(close, start + open.length)
|
||||
if (end === -1) return null
|
||||
return text.slice(start, end + close.length)
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
getRemoteTaskSessionUrl,
|
||||
registerCompletionChecker,
|
||||
registerCompletionHook,
|
||||
registerContentExtractor,
|
||||
registerRemoteAgentTask,
|
||||
type AutofixPrRemoteTaskMetadata,
|
||||
type BackgroundRemoteSessionPrecondition,
|
||||
|
|
@ -31,6 +32,7 @@ import {
|
|||
trySetActiveMonitor,
|
||||
updateActiveMonitor,
|
||||
} from './monitorState.js'
|
||||
import { extractAutofixResultFromLog } from './extractAutofixResult.js'
|
||||
import { parseAutofixArgs } from './parseArgs.js'
|
||||
import { checkPrAutofixOutcome, fetchPrHeadSha } from './prFetch.js'
|
||||
import { detectAutofixSkills, formatSkillsHint } from './skillDetect.js'
|
||||
|
|
@ -81,6 +83,13 @@ registerCompletionHook('autofix-pr', (taskId, metadata) => {
|
|||
if (meta) lastCheckAt.delete(throttleKey(meta))
|
||||
})
|
||||
|
||||
// Phase 3 content return: extract the <autofix-result> tag from the session
|
||||
// log so the local model sees the agent's structured outcome (commits
|
||||
// pushed, files changed, CI status) inline in the completion task-
|
||||
// notification — instead of just a file-path pointer. The framework falls
|
||||
// back to the generic notification if extraction returns null.
|
||||
registerContentExtractor('autofix-pr', log => extractAutofixResultFromLog(log))
|
||||
|
||||
function makeErrorText(message: string, code: string): string {
|
||||
logEvent('tengu_autofix_pr_result', {
|
||||
result:
|
||||
|
|
@ -249,7 +258,23 @@ export const callAutofixPr: LocalJSXCommandCall = async (
|
|||
// 4.5 compose message
|
||||
const target = `${owner}/${repo}#${prNumber}`
|
||||
const branchName = `refs/pull/${prNumber}/head`
|
||||
const initialMessage = `Auto-fix failing CI checks on PR #${prNumber} in ${owner}/${repo}.${skillsHint}`
|
||||
const initialMessage = `Auto-fix failing CI checks on PR #${prNumber} in ${owner}/${repo}.${skillsHint}
|
||||
|
||||
When you finish (or hit a blocker you can't recover from), output the following XML tag as your final message so the local user gets a structured summary:
|
||||
|
||||
<autofix-result>
|
||||
<pr-number>${prNumber}</pr-number>
|
||||
<commits-pushed>
|
||||
<commit sha="...">commit message</commit>
|
||||
</commits-pushed>
|
||||
<files-changed>
|
||||
<file path="...">N changes</file>
|
||||
</files-changed>
|
||||
<ci-status>green | red | pending | unknown</ci-status>
|
||||
<summary>One-sentence summary of what was fixed or why it could not be fixed.</summary>
|
||||
</autofix-result>
|
||||
|
||||
If no fix was needed, omit <commits-pushed> and <files-changed> and explain in <summary>. If you only attempted partial work, list the commits you did push and explain the remainder in <summary>.`
|
||||
|
||||
// 4.6 in-process teammate
|
||||
const teammate = createAutofixTeammate(initialMessage, target)
|
||||
|
|
|
|||
|
|
@ -133,6 +133,39 @@ export type RemoteTaskCompletionHook = (taskId: string, remoteTaskMetadata: Remo
|
|||
|
||||
const completionHooks = new Map<RemoteTaskType, RemoteTaskCompletionHook>();
|
||||
|
||||
/**
|
||||
* Inspect a completed remote task's accumulated log and return an XML fragment
|
||||
* to inject inline into the completion task-notification. Returning null falls
|
||||
* back to the framework's generic "task completed" notification (file-path
|
||||
* pointer only). Used by command modules whose remote agents emit structured
|
||||
* outcome tags the local model should read directly.
|
||||
*/
|
||||
export type RemoteTaskContentExtractor = (log: SDKMessage[]) => string | null;
|
||||
|
||||
const contentExtractors = new Map<RemoteTaskType, RemoteTaskContentExtractor>();
|
||||
|
||||
/**
|
||||
* Register a content extractor for a remote task type. Called once per
|
||||
* completion in the generic completion branches (archived, completionChecker,
|
||||
* result-driven). isRemoteReview tasks have their own bespoke path and skip
|
||||
* extractors entirely. Errors propagate to the framework which logs and falls
|
||||
* back to generic notification.
|
||||
*/
|
||||
export function registerContentExtractor(remoteTaskType: RemoteTaskType, extractor: RemoteTaskContentExtractor): void {
|
||||
contentExtractors.set(remoteTaskType, extractor);
|
||||
}
|
||||
|
||||
function tryExtractRichContent(task: RemoteAgentTaskState, log: SDKMessage[]): string | null {
|
||||
const extractor = contentExtractors.get(task.remoteTaskType);
|
||||
if (!extractor) return null;
|
||||
try {
|
||||
return extractor(log);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a completion hook for a remote task type. Invoked once after the
|
||||
* task reaches a terminal state in any of the framework's completion branches
|
||||
|
|
@ -253,6 +286,41 @@ function enqueueRemoteNotification(
|
|||
enqueuePendingNotification({ value: message, mode: 'task-notification' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as enqueueRemoteNotification but inlines a structured XML fragment
|
||||
* (returned by a registered RemoteTaskContentExtractor) so the local model
|
||||
* reads the remote agent's outcome directly instead of having to follow a
|
||||
* file-path pointer. Mode is still 'task-notification' — the framing XML is
|
||||
* the same, only the body differs.
|
||||
*/
|
||||
function enqueueRichRemoteNotification(
|
||||
taskId: string,
|
||||
title: string,
|
||||
status: 'completed' | 'failed' | 'killed',
|
||||
richContent: string,
|
||||
setAppState: SetAppState,
|
||||
toolUseId?: string,
|
||||
): void {
|
||||
if (!markTaskNotified(taskId, setAppState)) return;
|
||||
|
||||
const statusText = status === 'completed' ? 'completed successfully' : status === 'failed' ? 'failed' : 'was stopped';
|
||||
const toolUseIdLine = toolUseId ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>` : '';
|
||||
const outputPath = getTaskOutputPath(taskId);
|
||||
|
||||
const message = `<${TASK_NOTIFICATION_TAG}>
|
||||
<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine}
|
||||
<${TASK_TYPE_TAG}>remote_agent</${TASK_TYPE_TAG}>
|
||||
<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
|
||||
<${STATUS_TAG}>${status}</${STATUS_TAG}>
|
||||
<${SUMMARY_TAG}>Remote task "${title}" ${statusText}</${SUMMARY_TAG}>
|
||||
</${TASK_NOTIFICATION_TAG}>
|
||||
The remote agent produced the following structured outcome. Summarize the key changes for the user:
|
||||
|
||||
${richContent}`;
|
||||
|
||||
enqueuePendingNotification({ value: message, mode: 'task-notification' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically mark a task as notified. Returns true if this call flipped the
|
||||
* flag (caller should enqueue), false if already notified (caller should skip).
|
||||
|
|
@ -718,7 +786,19 @@ function startRemoteSessionPolling(taskId: string, context: TaskContext): () =>
|
|||
updateTaskState<RemoteAgentTaskState>(taskId, context.setAppState, t =>
|
||||
t.status === 'running' ? { ...t, status: 'completed', endTime: Date.now() } : t,
|
||||
);
|
||||
enqueueRemoteNotification(taskId, task.title, 'completed', context.setAppState, task.toolUseId);
|
||||
const richContent = tryExtractRichContent(task, accumulatedLog);
|
||||
if (richContent) {
|
||||
enqueueRichRemoteNotification(
|
||||
taskId,
|
||||
task.title,
|
||||
'completed',
|
||||
richContent,
|
||||
context.setAppState,
|
||||
task.toolUseId,
|
||||
);
|
||||
} else {
|
||||
enqueueRemoteNotification(taskId, task.title, 'completed', context.setAppState, task.toolUseId);
|
||||
}
|
||||
void evictTaskOutput(taskId);
|
||||
void removeRemoteAgentMetadata(taskId);
|
||||
runCompletionHook(taskId, task);
|
||||
|
|
@ -732,7 +812,19 @@ function startRemoteSessionPolling(taskId: string, context: TaskContext): () =>
|
|||
updateTaskState<RemoteAgentTaskState>(taskId, context.setAppState, t =>
|
||||
t.status === 'running' ? { ...t, status: 'completed', endTime: Date.now() } : t,
|
||||
);
|
||||
enqueueRemoteNotification(taskId, completionResult, 'completed', context.setAppState, task.toolUseId);
|
||||
const richContent = tryExtractRichContent(task, accumulatedLog);
|
||||
if (richContent) {
|
||||
enqueueRichRemoteNotification(
|
||||
taskId,
|
||||
completionResult,
|
||||
'completed',
|
||||
richContent,
|
||||
context.setAppState,
|
||||
task.toolUseId,
|
||||
);
|
||||
} else {
|
||||
enqueueRemoteNotification(taskId, completionResult, 'completed', context.setAppState, task.toolUseId);
|
||||
}
|
||||
void evictTaskOutput(taskId);
|
||||
void removeRemoteAgentMetadata(taskId);
|
||||
runCompletionHook(taskId, task);
|
||||
|
|
@ -917,7 +1009,21 @@ function startRemoteSessionPolling(taskId: string, context: TaskContext): () =>
|
|||
return; // Stop polling
|
||||
}
|
||||
|
||||
enqueueRemoteNotification(taskId, task.title, finalStatus, context.setAppState, task.toolUseId);
|
||||
// finalStatus is 'completed' | 'failed' on this path — kill is a
|
||||
// separate code path (RemoteAgentTask.kill) and never reaches here.
|
||||
const richContent = tryExtractRichContent(task, accumulatedLog);
|
||||
if (richContent) {
|
||||
enqueueRichRemoteNotification(
|
||||
taskId,
|
||||
task.title,
|
||||
finalStatus,
|
||||
richContent,
|
||||
context.setAppState,
|
||||
task.toolUseId,
|
||||
);
|
||||
} else {
|
||||
enqueueRemoteNotification(taskId, task.title, finalStatus, context.setAppState, task.toolUseId);
|
||||
}
|
||||
void evictTaskOutput(taskId);
|
||||
void removeRemoteAgentMetadata(taskId);
|
||||
runCompletionHook(taskId, task);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user