-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(workflows): support opt-in background runs #8303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,10 @@ import { randomBytes } from 'node:crypto'; | |
| import type { Config } from '../../config/config.js'; | ||
| import { logWorkflowRun } from '../../telemetry/loggers.js'; | ||
| import { WorkflowRunEvent } from '../../telemetry/types.js'; | ||
| import { createChildAbortController } from '../../utils/abortController.js'; | ||
| import { | ||
| createAbortController, | ||
| createChildAbortController, | ||
| } from '../../utils/abortController.js'; | ||
| import { | ||
| type WorkflowRunRegistry, | ||
| type WorkflowTask, | ||
|
|
@@ -35,6 +38,7 @@ export interface WorkflowRunnerOptions { | |
| resumeFromRunId?: string; | ||
| dispatch?: WorkflowAgentDispatch; | ||
| onUpdate?: (entry: WorkflowTask) => void; | ||
| runInBackground?: boolean; | ||
| } | ||
|
|
||
| export type WorkflowRunSettlement = | ||
|
|
@@ -64,6 +68,7 @@ export class WorkflowRunner { | |
| options: WorkflowRunnerOptions, | ||
| ): Promise<WorkflowRunHandle> { | ||
| const config = options.config; | ||
| const runInBackground = options.runInBackground === true; | ||
| const budget = WorkflowBudgetImpl.fromEnv(); | ||
| const loaded = | ||
| options.scriptPath && options.script === undefined | ||
|
|
@@ -83,7 +88,13 @@ export class WorkflowRunner { | |
| const resumeReplay: JournalReplay | undefined = options.resumeFromRunId | ||
| ? await journal?.load() | ||
| : undefined; | ||
| const controller = createChildAbortController(options.signal); | ||
| if (runInBackground && options.signal.aborted) { | ||
| throw new Error('Background workflow start was cancelled.'); | ||
| } | ||
| const callerWasAbortedBeforeStart = options.signal.aborted; | ||
| const controller = runInBackground | ||
| ? createAbortController() | ||
|
Comment on lines
+94
to
+96
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] if (
(!runInBackground && options.signal.aborted) ||
entry?.status === 'cancelled'
) {中文说明
if (
(!runInBackground && options.signal.aborted) ||
entry?.status === 'cancelled'
) {— qwen3.8-max-preview via Qwen Code /review (v0.21.2) |
||
| : createChildAbortController(options.signal); | ||
| const registry = config.getWorkflowRunRegistry?.(); | ||
| const dispatch = | ||
| options.dispatch ?? | ||
|
|
@@ -96,17 +107,24 @@ export class WorkflowRunner { | |
| : undefined, | ||
| ); | ||
| const orchestrator = new WorkflowOrchestrator(dispatch); | ||
| const entry = registry?.register({ | ||
| runId, | ||
| meta: null, | ||
| status: 'running', | ||
| startTime: Date.now(), | ||
| outputFile: '', | ||
| abortController: controller, | ||
| tokenBudgetTotal: budget.total, | ||
| script, | ||
| scriptPath, | ||
| }); | ||
| let entry: WorkflowTask | undefined; | ||
| try { | ||
| entry = registry?.register({ | ||
| runId, | ||
| meta: null, | ||
| status: 'running', | ||
| startTime: Date.now(), | ||
| outputFile: '', | ||
| abortController: controller, | ||
| tokenBudgetTotal: budget.total, | ||
| script, | ||
| scriptPath, | ||
| isBackgrounded: runInBackground, | ||
| }); | ||
| } catch (error) { | ||
| controller.abort(); | ||
| throw error; | ||
| } | ||
| const emitUpdate = (): void => { | ||
| if (!entry || !options.onUpdate) return; | ||
| try { | ||
|
|
@@ -172,7 +190,11 @@ export class WorkflowRunner { | |
| const message = extractErrorMessage(error); | ||
| if (entry && details?.meta && !entry.meta) entry.meta = details.meta; | ||
| if (details?.logs) registry?.setRecentLogs(runId, details.logs); | ||
| if (options.signal.aborted) { | ||
| if ( | ||
| callerWasAbortedBeforeStart || | ||
| (!runInBackground && options.signal.aborted) || | ||
| entry?.status === 'cancelled' | ||
| ) { | ||
| registry?.cancel(runId, Date.now()); | ||
| } else { | ||
| registry?.fail(runId, message, Date.now()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The
expect.objectContainingmatcher in this new test assertstypeandnotificationDisplayTextbut nottodoWorkChainId, even though the callback is invoked with{ todoWorkChainId: 'workflow-chain' }. A mutation that dropstodoWorkChainId: meta.todoWorkChainIdfrom the queue push inuseGeminiStream.tswould therefore survive this test green.Failure scenario: if
todoWorkChainIdis silently dropped from the queue entry, the queue drain's contiguous-batch split (which groups notifications bytodoWorkChainId) would merge completions from unrelated work chains into one batchedsendMessageStreamcall and deliver no chain-routing metadata to the model loop — and this test would still pass.中文说明
[Suggestion] 这个新增测试里的
expect.objectContaining匹配器断言了type和notificationDisplayText,但没有断言todoWorkChainId,尽管 callback 是以{ todoWorkChainId: 'workflow-chain' }调用的。因此,一个把useGeminiStream.ts队列 push 中的todoWorkChainId: meta.todoWorkChainId删掉的 mutation 仍能让该测试通过。失败场景:如果
todoWorkChainId被静默地从队列条目中丢弃,队列 drain 的连续分批逻辑(按todoWorkChainId分组通知)会把来自不同 work chain 的 completion 合并进同一个批量sendMessageStream调用,并且不会把 chain 路由元数据传给模型 loop——而本测试仍会通过。— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已修复:已补充 workflow completion 的 todoWorkChainId 路由断言。\n\n验证:cd packages/cli && npx vitest run src/ui/hooks/useGeminiStream.test.tsx(169 passed)。