From 5dcb67522e9e88c54a50e40b717ca5a1101b1cac Mon Sep 17 00:00:00 2001 From: seekskyworld Date: Sun, 30 Aug 2026 15:40:49 +0800 Subject: [PATCH] fix(workflows): remove background launch alias Signed-off-by: seekskyworld --- .../OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md | 12 +++---- extensions/workflows/coordinator.ts | 19 ++--------- extensions/workflows/index.ts | 12 ++----- extensions/workflows/prompt.ts | 2 -- package.json | 2 +- skills/workflows/REFERENCE.md | 2 +- .../extensions/workflows/coordinator.test.ts | 32 ++----------------- .../extensions/workflows/execute.e2e.test.ts | 10 +++--- tests/extensions/workflows/rendering.test.ts | 23 +++++++------ 9 files changed, 30 insertions(+), 84 deletions(-) diff --git a/docs/design/OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md b/docs/design/OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md index 61ed7f67..e0435004 100644 --- a/docs/design/OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md +++ b/docs/design/OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md @@ -6,7 +6,7 @@ > > 依据:当前 OpenPI 源码、Issues #71/#74/#75/#90、Claude Code `2.1.241` 运行时合同访谈,以及三份相互独立的 interface 设计评审。 > -> 后续决定(2026-08-30):Issue #132 / PR #139 将新调用策略收敛为 `wait`,同时为已发布的 `background` alias 保留迁移窗口。本文件保留 Workflow V2 落地时的历史合同与验证证据;当前行为以代码和当前用户文档为准,后续结果见文末 addendum。 +> 后续决定(2026-08-30):Issue #132 / PR #139 先将新调用策略收敛为 `wait` 并保留兼容窗口;在 OpenPI 1.0.0 breaking release 中移除 workflow 调用参数 `background`。本文件保留 Workflow V2 落地时的历史合同与验证证据;当前行为以代码和当前用户文档为准,后续结果见文末 addendum。 ## 结论 @@ -35,14 +35,14 @@ workflow_stop 本轮没有把整个 2200 行 Workflow extension 塞进一个新的大类,而是按不变量拆成三个深 seam: -- `coordinator.ts`:宿主默认值、`wait/background` 兼容解析,以及 wait/terminal 仲裁; +- `coordinator.ts`:宿主默认值、`wait` 解析,以及 wait/terminal 仲裁; - `result-delivery.ts`:逐 run delivery identity、pending/receipt/retry/restore; - `shared/result-budget.ts` 与 `shared/text-projection.ts`:Subagent/Workflow 共用的公平预算与 head/tail 投影。 已经落地: - TUI 默认 detached,print/无可靠投递宿主默认 wait; -- `wait:true` 是唯一正向同步选择,`background` 仅为 deprecated inverse alias; +- `wait:true` 是唯一正向同步选择; - 中断 wait 不取消 run,stop/shutdown 才拥有取消权; - terminal execution state 与 delivery state 正交持久化; - send failure 以同一 per-run id 重试,成功 sibling 不重发; @@ -668,13 +668,13 @@ Lifecycle、delivery、Schema stability、dynamic capacity、fair projection 和 ## 15. 后续合同变更(2026-08-30) -Issue #132 / PR #139 将 `wait` 作为唯一推荐的新调用策略。由于 `background` 从 OpenPI v0.2.0 起就是已发布输入,本次继续把它作为 deprecated inverse alias 接受:`background: true` 对应 `wait: false`,`background: false` 对应 `wait: true`;真正删除只在另行公告的 breaking release 进行。除这一已发布兼容字段外,未知输入继续 fail closed。 +Issue #132 / PR #139 将 `wait` 作为唯一推荐的新调用策略,并在兼容窗口接受已发布的 `background` inverse alias。OpenPI 1.0.0 现在移除该调用字段;未知输入(包括旧 `background` 参数)由 `additionalProperties: false` fail closed。除调用 schema 外,历史 artifact 的兼容读取不受影响。 -Coordinator 在单一输入边界完成 legacy 映射,内部仍只产生 `inline | detached` 运行模式。`WorkflowDetails.background` 与 persisted artifact 中的同名字段继续记录实际 detached 状态,不记录调用时使用的是 `wait` 还是兼容 alias,也不改写历史 artifact。 +Coordinator 在单一输入边界解析 `wait`,内部仍只产生 `inline | detached` 运行模式。`WorkflowDetails.background` 与 persisted artifact 中的同名字段继续记录实际 detached 状态,不记录调用参数,也不改写历史 artifact。 该后续变更的最终验证以 PR #139 exact-head review 为准,至少包括: -- `wait`、legacy `background`、冲突输入、host delivery 能力和 wait interruption 的专项测试; +- `wait`、未知输入拒绝、host delivery 能力和 wait interruption 的专项测试; - `bun run check`; - `bun run test`; - GitHub CI:Node 22.19.0、Node 24 与 Windows background-terminal suite。 diff --git a/extensions/workflows/coordinator.ts b/extensions/workflows/coordinator.ts index b6ab4500..30e97819 100644 --- a/extensions/workflows/coordinator.ts +++ b/extensions/workflows/coordinator.ts @@ -1,28 +1,13 @@ export interface WorkflowLaunchPolicyInput { wait?: boolean; - background?: boolean; } -/** - * Resolve the caller's launch preference to one positive runtime mode. - * `background` remains only as the published inverse compatibility alias. - */ +/** Resolve the caller's launch preference to one positive runtime mode. */ export function resolveWorkflowLaunchMode( input: WorkflowLaunchPolicyInput, canDeliverLater: boolean, ) { - if ( - input.wait !== undefined && - input.background !== undefined && - input.wait === input.background - ) { - throw new Error( - "wait and background conflict: background is the deprecated inverse of wait; remove background and provide only wait", - ); - } - const wait = - input.wait ?? - (input.background !== undefined ? !input.background : !canDeliverLater); + const wait = input.wait ?? !canDeliverLater; if (!wait && !canDeliverLater) { throw new Error( "This host cannot deliver a workflow result later; use wait: true", diff --git a/extensions/workflows/index.ts b/extensions/workflows/index.ts index 370003ff..ee915ca8 100644 --- a/extensions/workflows/index.ts +++ b/extensions/workflows/index.ts @@ -562,12 +562,6 @@ const WorkflowParams = Type.Object( description: WORKFLOW_PARAMETER_DESCRIPTIONS.args, }), ), - background: Type.Optional( - Type.Boolean({ - deprecated: true, - description: WORKFLOW_PARAMETER_DESCRIPTIONS.background, - }), - ), wait: Type.Optional( Type.Boolean({ description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait, @@ -1234,7 +1228,7 @@ export default function workflows( const runDir = path.join(getAgentDir(), "workflows", runId); const canDeliverLater = ctx.hasUI && ctx.mode === "tui"; const launchMode = resolveWorkflowLaunchMode( - { wait: params.wait, background: params.background }, + { wait: params.wait }, canDeliverLater, ); const background = launchMode === "detached"; @@ -2324,9 +2318,7 @@ export default function workflows( let text = theme.fg("toolTitle", theme.bold("workflow ")) + theme.fg("accent", (meta as WorkflowMeta).name ?? "(script)"); - if (args.background !== undefined) { - text += theme.fg("dim", ` (deprecated: use wait: ${!args.background})`); - } else if (args.wait === true) { + if (args.wait === true) { text += theme.fg("dim", " (wait)"); } const description = (meta as WorkflowMeta).description; diff --git a/extensions/workflows/prompt.ts b/extensions/workflows/prompt.ts index a56afddc..daacbbce 100644 --- a/extensions/workflows/prompt.ts +++ b/extensions/workflows/prompt.ts @@ -17,8 +17,6 @@ export const WORKFLOW_PARAMETER_DESCRIPTIONS = { script: "JavaScript workflow script. May start with `export const meta = {...}`, then use phase(), agent(), parallel(), args, and a final `return`.", args: "Optional JSON string exposed to the script as `args` (parsed when valid JSON, otherwise passed through as the raw string).", - background: - "Deprecated compatibility alias for published callers only; new calls must use wait. Replace true with wait=false and false with wait=true. Do not provide both fields. The alias will be removed in the next announced breaking release.", wait: "Wait for the final result in this tool call. Interactive sessions default to false and deliver completion later; print/automation defaults to true. Interrupting the wait does not cancel the workflow.", resumeFromRunId: "Optional prior run id or unique suffix for safe read-only replay. See the workflows Skill for matching rules.", diff --git a/package.json b/package.json index 4f02ddbb..f30f7d86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tt-a1i/openpi", - "version": "0.4.0", + "version": "1.0.0", "description": "OpenPI — a Pi-native multi-agent workbench with background execution, isolated subagents, replay-safe workflows, goals, tasks, and observable TUI", "license": "MIT", "author": "tt-a1i", diff --git a/skills/workflows/REFERENCE.md b/skills/workflows/REFERENCE.md index 96979a88..780dab80 100644 --- a/skills/workflows/REFERENCE.md +++ b/skills/workflows/REFERENCE.md @@ -42,7 +42,7 @@ Each call persists intent, admission, and execution state. Interrupted nontermin Interactive TUI runs return an accepted run id immediately by default, release the parent turn, and later deliver a terminal completion with a stable delivery id. Delivery is at least once: normal retries do not duplicate a run, but a process loss after Pi accepts the message and before the receipt is persisted can replay the same id. `wait: true` explicitly waits inline; interrupting that wait releases only the waiter and the run continues. Print/automation defaults to waiting because it has no later delivery channel. -New calls must use `wait`. For compatibility with released OpenPI versions, the deprecated `background` alias remains accepted during the current migration window: replace `background: true` with `wait: false`, or `background: false` with `wait: true`, and do not provide both fields. The alias will be removed only in an announced breaking release. Persisted artifact/details fields named `background` remain actual detached-state facts and are not part of that removal. +Workflow calls use the positive `wait` policy. Interactive TUI runs default to detached delivery (`wait: false`); print and automation hosts default to inline waiting (`wait: true`). Persisted artifact/details fields named `background` remain actual detached-state facts and are not call parameters. Loading the Workflow capability exposes `workflow`, `workflow_status`, and `workflow_stop` as one stable group; starting or settling a run does not mutate the model tool Schema. `workflow_status` returns a bounded state/coverage summary and artifact path without consuming or repeating the full completion. `workflow_stop` is idempotent and preserves partial artifacts. A failed completion send remains pending with the same per-run delivery identity and is retried when the parent settles or the Session is restored. diff --git a/tests/extensions/workflows/coordinator.test.ts b/tests/extensions/workflows/coordinator.test.ts index 0e446c2c..d502a428 100644 --- a/tests/extensions/workflows/coordinator.test.ts +++ b/tests/extensions/workflows/coordinator.test.ts @@ -10,44 +10,16 @@ test("interactive launch defaults detached while non-delivery hosts wait", () => assert.equal(resolveWorkflowLaunchMode({}, false), "inline"); }); -test("wait is authoritative and legacy background maps to its inverse", () => { +test("wait selects inline or detached execution", () => { assert.equal(resolveWorkflowLaunchMode({ wait: true }, true), "inline"); assert.equal(resolveWorkflowLaunchMode({ wait: false }, true), "detached"); - assert.equal( - resolveWorkflowLaunchMode({ background: true }, true), - "detached", - ); - assert.equal( - resolveWorkflowLaunchMode({ background: false }, true), - "inline", - ); - assert.equal( - resolveWorkflowLaunchMode({ wait: true, background: false }, true), - "inline", - ); - assert.equal( - resolveWorkflowLaunchMode({ wait: false, background: true }, true), - "detached", - ); }); -test("conflicting aliases and unsupported detached delivery fail closed", () => { - assert.throws( - () => resolveWorkflowLaunchMode({ wait: true, background: true }, true), - /conflict.*background is the deprecated inverse of wait/i, - ); - assert.throws( - () => resolveWorkflowLaunchMode({ wait: false, background: false }, true), - /conflict.*background is the deprecated inverse of wait/i, - ); +test("unsupported detached delivery fails closed", () => { assert.throws( () => resolveWorkflowLaunchMode({ wait: false }, false), /cannot deliver/, ); - assert.throws( - () => resolveWorkflowLaunchMode({ background: true }, false), - /cannot deliver.*wait: true/i, - ); }); test("wait cancellation does not cancel the underlying completion", async () => { diff --git a/tests/extensions/workflows/execute.e2e.test.ts b/tests/extensions/workflows/execute.e2e.test.ts index 7db782a3..c90a4ee7 100644 --- a/tests/extensions/workflows/execute.e2e.test.ts +++ b/tests/extensions/workflows/execute.e2e.test.ts @@ -418,7 +418,7 @@ test("print hosts wait by default and reject detached delivery", async () => { { script: 'export const meta = { name: "print-legacy-inline" };\nreturn { inline: true };', - background: false, + wait: true, }, undefined, undefined, @@ -436,7 +436,7 @@ test("print hosts wait by default and reject detached delivery", async () => { const workflowsDir = join(agentDir, "workflows"); const runDirsBefore = readdirSync(workflowsDir).sort(); const messagesBefore = sentMessages.length; - for (const input of [{ wait: false }, { background: true }]) { + for (const input of [{ wait: false }]) { await assert.rejects( Promise.resolve().then(() => workflow.execute( @@ -729,7 +729,7 @@ test("shutdown preserves a failed completion for reload recovery", async () => { { script: 'export const meta = { name: "shutdown-reload-delivery" };\nreturn { durable: true };', - background: true, + wait: false, }, undefined, undefined, @@ -816,7 +816,7 @@ test("cancelled detached delivery preserves aborted status after artifact persis script: 'export const meta = { name: "cancelled-persistence-failure" };\n' + 'return await agent("wait for cancellation", { agent_type: "reviewer" });', - background: true, + wait: false, }, undefined, undefined, @@ -1632,7 +1632,7 @@ test("forced settlement persists worktree cleanup that finishes later", async () 'export const meta = { name: "forced-worktree-cleanup" };\n' + 'await agent("finish in isolation", { agent_type: "reviewer", isolation: "worktree" });\n' + "return true;", - background: true, + wait: false, }, undefined, undefined, diff --git a/tests/extensions/workflows/rendering.test.ts b/tests/extensions/workflows/rendering.test.ts index 4674722d..66c3515c 100644 --- a/tests/extensions/workflows/rendering.test.ts +++ b/tests/extensions/workflows/rendering.test.ts @@ -99,7 +99,7 @@ function captureRenderers() { return { workflow, message }; } -test("workflow launch schema recommends wait while preserving only the published alias", () => { +test("workflow launch schema exposes only wait", () => { const { workflow } = captureRenderers(); const parameters = workflow.parameters as unknown as { properties?: Record; @@ -107,10 +107,7 @@ test("workflow launch schema recommends wait while preserving only the published }; assert.ok(parameters.properties?.wait); - assert.deepEqual( - (parameters.properties?.background as { deprecated?: unknown })?.deprecated, - true, - ); + assert.equal(parameters.properties?.background, undefined); assert.equal(parameters.additionalProperties, false); const toolCall = (args: Record) => ({ @@ -125,9 +122,10 @@ test("workflow launch schema recommends wait while preserving only the published validateToolArguments(workflow, toolCall({ script, wait: false })), { script, wait: false }, ); - assert.deepEqual( - validateToolArguments(workflow, toolCall({ script, background: true })), - { script, background: true }, + assert.throws( + () => + validateToolArguments(workflow, toolCall({ script, background: true })), + /Invalid tool arguments|additional/i, ); assert.throws( () => validateToolArguments(workflow, toolCall({ script, detached: true })), @@ -161,12 +159,12 @@ test("workflow call rendering labels an explicit inline wait", () => { assert.match(component.render(100).join("\n"), /workflow inline \(wait\)/); }); -test("workflow call rendering gives legacy callers an actionable migration", () => { +test("workflow call rendering leaves detached calls unlabelled", () => { const { workflow } = captureRenderers(); assert.ok(workflow.renderCall); const args = { script: 'export const meta = { name: "legacy" }; return 1;', - background: true, + wait: false, }; const component = workflow.renderCall(args, theme, { @@ -184,9 +182,10 @@ test("workflow call rendering gives legacy callers an actionable migration", () isError: false, }); - assert.match( + assert.match(component.render(100).join("\n"), /workflow legacy/); + assert.doesNotMatch( component.render(100).join("\n"), - /workflow legacy \(deprecated: use wait: false\)/, + /deprecated|background/, ); });