Merge pull request #3425 from deepseek-harness/feat/ptc-disable-workflow-plugin

feat(presets): omit workflow from Web PTC mode
This commit is contained in:
Tianyi Cui 2026-09-01 23:18:39 +08:00 • committed by GitHub
commit 3c5b7097ae
16 changed files with 141 additions and 53 deletions

View file

@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-01-ptc-omits-workflow-tool.md
2026-09-01-ptc-omits-workflow-tool.md: 7453e06187690fe7fa1eec157ffad8b474c95cf8
2026-09-01-ptc-omits-workflow-tool.zh.md: 71f64c11048d85c562432cdd51b3b717966fea29

View file

@ -0,0 +1,29 @@
# Agent Note: PTC preset omits the general workflow tool
Status: implemented
English | [中文](2026-09-01-ptc-omits-workflow-tool.zh.md)
## Problem
The shipped Web `ptc` preset exposed the general `workflow` tool through its generated SDK. PTC mode already makes `run_code` the model-authored composition interface, so `workflow` added a second orchestration language with different execution semantics. The preset description also claimed complete parity with Standard mode and could not state this intentional difference.
## Decision
The shipped Web `ptc` preset disables its `tool-workflow` row. Its generated PTC mode SDK therefore omits the `workflow` binding, while the model-facing wire contract remains the single `run_code` tool.
The preset retains `workflow-worker-thread` in its isolated workflow realm because `tool-ralph` consumes the same engine. `ralph` remains available through the PTC mode SDK. The Standard and Creator presets continue to expose `workflow`, and a user-authored preset may mount the tool explicitly.
The workflow package and its durable Session event types remain installed. Existing workflow records continue to render; this default composition change only prevents new top-level workflow calls from agents using the shipped `ptc` preset.
## Alternatives considered
**Disable the complete workflow realm in PTC mode.** Rejected because that also removes the provider required by `ralph`, even though Ralph's fixed fresh-agent loop is not a second model-authored workflow language.
**Hide `workflow` only in the generated SDK.** Rejected because presentation-only filtering would leave executable tool lookup and the declared preset composition out of agreement. Disabling the consumer row removes the binding from registration, lookup, and presentation together.
**Keep `workflow` until `run_code` has feature parity.** Rejected because the shipped PTC default is intended to make `run_code` its composition interface. Users who require declarative workflow semantics can select Standard mode or an explicit custom preset while capability gaps are evaluated independently.
## Consequences
The PTC picker description states the exception instead of promising full Standard parity. A real Web Loader composition test pins the `run_code` wire catalog, the absent `workflow` SDK binding, and the retained `ralph` binding. The keyless recorded Web PTC session owns the assembled prompt evidence, while preset tests pin that Standard and Creator remain unchanged.

View file

@ -0,0 +1,29 @@
# Agent Note: PTC preset 不提供通用 workflow 工具
Status: implemented
[English](2026-09-01-ptc-omits-workflow-tool.md) | 中文
## 问题
Web 端随附的 `ptc` preset 会通过生成的 SDK 提供通用 `workflow` 工具。PTC mode 已经把 `run_code` 作为模型编写的组合接口,因此 `workflow` 又增加了一套执行语义不同的编排语言。preset 描述还声称与标准模式能力完全相同,无法说明这个有意的差异。
## 决策
Web 端随附的 `ptc` preset 禁用自身的 `tool-workflow` 配置项。因此,它生成的 PTC mode SDK 不包含 `workflow` 绑定,模型可见的协议约定仍然只有一个 `run_code` 工具。
preset 在隔离的 workflow realm 中保留 `workflow-worker-thread`,因为 `tool-ralph` 使用同一个引擎。PTC mode SDK 继续提供 `ralph`。标准模式与创造模式继续提供 `workflow`,用户自定义 preset 也可以显式挂载该工具。
workflow 包及其持久 Session 事件类型仍然随产品安装。现有 workflow 记录继续正常渲染;这次默认组合变更只会阻止使用随附 `ptc` preset 的 agent 发起新的顶层 workflow 调用。
## 曾考虑的替代方案
**在 PTC mode 中禁用整个 workflow realm。** 不予采用,因为这也会移除 `ralph` 所需的 provider,而 Ralph 固定的全新 agent 循环不是第二套由模型编写的 workflow 语言。
**只在生成的 SDK 中隐藏 `workflow`。** 不予采用,因为只修改呈现会导致可执行工具查找与 preset 声明的组合不一致。禁用 consumer 配置项会同时从注册、查找与呈现中移除该绑定。
**等到 `run_code` 功能完全对等后再移除 `workflow`。** 不予采用,因为随附的 PTC 默认模式就是要以 `run_code` 作为组合接口。在独立评估能力缺口期间,需要声明式 workflow 语义的用户可以选择标准模式或显式自定义 preset。
## 后果
PTC 选择器描述会说明这一例外,不再承诺与标准模式完全对等。真实 Web Loader 组合测试固定 `run_code` 协议工具清单、缺失的 `workflow` SDK 绑定和保留的 `ralph` 绑定。无密钥的 Web PTC 录制会话负责组装提示词证据,preset 测试则固定标准模式与创造模式保持不变。

View file

@ -33,7 +33,7 @@
- text: 复制
- listitem:
- 'button "设为默认: PTC 模式"':
- text: PTC 模式 内置 具备标准模式的全部能力,并通过 PTC 模式 SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
- text: PTC 模式 内置 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
- code: ptc
- 'button "查看: PTC 模式"':
- img

View file

@ -33,7 +33,7 @@
- text: 复制
- listitem:
- 'button "设为默认: PTC 模式"':
- text: PTC 模式 内置 具备标准模式的全部能力,并通过 PTC 模式 SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
- text: PTC 模式 内置 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
- code: ptc
- 'button "查看: PTC 模式"':
- img

View file

@ -33,7 +33,7 @@
- text: 复制
- listitem:
- 'button "设为默认: PTC 模式"':
- text: PTC 模式 内置 具备标准模式的全部能力,并通过 PTC 模式 SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
- text: PTC 模式 内置 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
- code: ptc
- 'button "查看: PTC 模式"':
- img

View file

@ -2,7 +2,7 @@
- menuitem "Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.":
- text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
- img
- menuitem "PTC mode All Standard mode capabilities, with tools exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program."
- menuitem "PTC mode Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program."
- menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor."
- menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
- menuitem "Refusing mode Resolves, then refuses to start."

View file

@ -30,7 +30,7 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => {
beforeAll(async () => {
scaffold = await launchWebScaffold({
toolsMode: 'ptc',
agentPresets: { roots: [], default: 'ptc' },
compareReplaySession: true,
...(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 }),
})

View file

@ -10,8 +10,8 @@ import { afterEach, expect, it } from 'vitest'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
import { SessionId } from '@deepseek-ai/dsh-session'
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
import type {} from '@deepseek-ai/dsh-tools'
// These imports carry the tools/sandboxPolicy/approval Context merges.
import { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import type {} from '@deepseek-ai/dsh-user-approval'
import type {} from '@deepseek-ai/dsh-permission-presets'
@ -191,6 +191,24 @@ it('assembles the shipped Web transport, catalog, guidance, and defaults', async
}
}, 120_000)
it('ships PTC with run_code but without the general workflow SDK binding', async () => {
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
const ctx = scaffold.ctx
const handle = await ctx.agents.create({
sessionId: SessionId('shipped-ptc-composition'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'ptc').then(() => undefined),
})
try {
const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
expect(assembly.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toContain(' ralph: {')
expect(sdk).not.toContain(' workflow: {')
} finally {
await handle.dispose()
}
}, 120_000)
it('lets a preset producer reach the background-job registry', async () => {
scaffold = await launchWebScaffold()
const ctx = scaffold.ctx

View file

@ -36,7 +36,7 @@ export const en: Record<AgentPresetSettingsKey, string> = {
'Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.',
presetPtcName: 'PTC mode',
presetPtcDescription:
'All Standard mode capabilities, with tools exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program.',
'Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program.',
presetMinimalName: 'Minimal mode',
presetMinimalDescription:
'Two-tool coding agent with persistent bash and str_replace_editor.',
@ -96,7 +96,7 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
presetStandardName: '标准模式',
presetStandardDescription: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。',
presetPtcName: 'PTC 模式',
presetPtcDescription: '具备标准模式的全部能力,并通过 PTC 模式 SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。',
presetPtcDescription: '功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。',
presetMinimalName: '极简模式',
presetMinimalDescription: '仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。',
presetCordisName: '创造模式',

View file

@ -1,9 +1,9 @@
# The `ptc` agent preset: the standard coding agent, presented as PTC mode.
#
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
# row: instead of one tool call per action, the model writes a TypeScript
# program against a generated SDK and `run_code` executes it, so a sequence
# that would be five round trips becomes one.
# Most of `standard` is here unchanged. The deliberate exception is the
# general-purpose `workflow` tool: PTC mode uses `run_code` as its model-authored
# composition surface. The `tool-presentation` row turns the remaining registry
# into a generated SDK, so a sequence that would be five round trips becomes one.
#
# The registry itself stays on the host plane — the agent loop's scheduler and
# the API proxy's presenters are its consumers — so what this preset owns is
@ -232,6 +232,9 @@
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
# Keep the engine above for `ralph`, but do not publish a second
# model-authored orchestration surface beside `run_code` in PTC mode.
disabled: true
- id: tool-ralph
name: '@deepseek-ai/dsh-tool-ralph'

View file

@ -1,3 +1,3 @@
name: PTC 模式
description: 具备标准模式的全部能力,并通过 PTC 模式 SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
description: 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
order: 2

View file

@ -53,6 +53,34 @@ async function roster(config: Partial<Config> = {}): Promise<Context> {
return ctx
}
interface ShippedEntry {
id?: unknown
disabled?: unknown
config?: unknown
}
/** Find one entry through the shipped composition's nested groups. */
function findEntry(entries: unknown[], id: string): ShippedEntry | undefined {
for (const entry of entries) {
if (typeof entry !== 'object' || entry === null) continue
const candidate = entry as ShippedEntry
if (candidate.id === id) return candidate
if (Array.isArray(candidate.config)) {
const nested = findEntry(candidate.config, id)
if (nested !== undefined) return nested
}
}
return undefined
}
/** Read and validate one shipped preset's Cordis entry list. */
async function shippedEntries(id: string): Promise<unknown[]> {
const source = await readFile(join(SHIPPED_PRESET_ROOT, id, 'agent.cordis.yml'), 'utf8')
const entries: unknown = yaml.load(source, { schema: entryListSchema })
if (!Array.isArray(entries)) throw new TypeError(`${id} preset must contain a Cordis entry list`)
return entries.map((entry: unknown) => entry)
}
describe('the shipped preset root', () => {
it('supplies the built-in presets from a bare roster, healthy and system-trusted', async () => {
const ctx = await roster({ includeUserRoot: false })
@ -99,9 +127,7 @@ describe('the shipped preset root', () => {
it('enables web_fetch in each tool-bearing Web app preset', async () => {
for (const id of ['cordis', 'ptc', 'standard']) {
const source = await readFile(join(SHIPPED_PRESET_ROOT, id, 'agent.cordis.yml'), 'utf8')
const entries: unknown = yaml.load(source, { schema: entryListSchema })
if (!Array.isArray(entries)) throw new TypeError(`${id} preset must contain a Cordis entry list`)
const entries = await shippedEntries(id)
const toolWeb: unknown = entries.find((entry: unknown) =>
typeof entry === 'object' && entry !== null && 'id' in entry && entry.id === 'tool-web')
if (typeof toolWeb !== 'object' || toolWeb === null || !('config' in toolWeb)
@ -111,4 +137,15 @@ describe('the shipped preset root', () => {
expect(toolWeb.config.fetch, id).toBe(true)
}
})
it('omits the general workflow tool only from PTC while retaining Ralph infrastructure', async () => {
const ptc = await shippedEntries('ptc')
expect(findEntry(ptc, 'tool-workflow')?.disabled).toBe(true)
expect(findEntry(ptc, 'workflow-worker-thread')?.disabled).not.toBe(true)
expect(findEntry(ptc, 'tool-ralph')?.disabled).not.toBe(true)
for (const id of ['standard', 'cordis']) {
expect(findEntry(await shippedEntries(id), 'tool-workflow')?.disabled, id).not.toBe(true)
}
})
})

View file

@ -1,4 +1,4 @@
{"type":"session","version":0,"id":"{{session:1}}","createdAt":1787736414061,"cwd":"{{cwd}}","agentPreset":"standard"}
{"type":"session","version":0,"id":"{{session:1}}","createdAt":1787736414061,"cwd":"{{cwd}}","agentPreset":"ptc"}
{"type":"permission/preset","data":{"preset":"workspace-write"}}
{"type":"sandbox/mode","data":{"mode":"workspace-write"}}
{"type":"approval/policy","data":{"policy":"ask"}}

View file

@ -30,8 +30,6 @@ Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL (for ex
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
Use subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.
@ -254,33 +252,6 @@ interface ToolArgsMap {
/** Required search queries; accepts 1–4 items and merges their results. */
queries: string[];
} & Record<string, JsonValue>;
/** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return <value>` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise<any>` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise<any[]>` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise<any[]>` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */
workflow: {
/** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return <json-value>`). */
script: string;
/** The workflow identity block (plain JSON — never code). */
meta: {
/** Short kebab-case workflow name. */
name: string;
/** One-line description of what the workflow does. */
description: string;
/** Optional guidance on when this workflow applies. */
whenToUse?: string;
/** Optional phase declarations matched by phase() calls. */
phases?: ({
/** The phase title phase() calls match by exact string. */
title: string;
/** Optional one-line description of the phase. */
detail?: string;
/** Optional provider override this phase is expected to use. */
provider?: string;
/** Optional model override this phase is expected to use. */
model?: string;
} & Record<string, JsonValue>)[];
} & Record<string, JsonValue>;
/** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {"files": [...]}). */
args?: Record<string, JsonValue>;
} & Record<string, JsonValue>;
/** Create or fully replace a UTF-8 text file. */
write: {
/** Path to write, resolved by the filesystem backend. */
@ -551,11 +522,6 @@ interface ToolOutputMap {
}[];
truncated: boolean;
};
workflow: {
runId: string;
agentsStarted: number;
result: JsonValue;
};
write: {
path: string;
operation: "create" | "update";

View file

@ -2,7 +2,7 @@
- navigation "Session hierarchy":
- 'button "Using ONE run_code program: run" [disabled]'
- img
- text: Standard mode
- text: PTC mode
- button "Session log":
- text: Session log
- img