@deepseek-ai/dsh-tool-ralph 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/workflow/tool-ralph/README.md
5
+ README.md: daf242364d1093cff30cd1dc95823e1ecb89a9c7
6
+ README.zh.md: 92c3256188d9b4c38a292ba7697f702dc46db737
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @deepseek-ai/dsh-tool-ralph
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ The model-facing `ralph` tool runs a fixed foreground workflow that gives one immutable objective to a sequence of fresh child agents. It demonstrates a specialized orchestration policy as an ordinary plugin over [`ctx.workflows`](../workflow/README.md) and [`ctx.subagents`](../../subagent/subagent/README.md): no Ralph mode or fresh-agent loop is added to `agent-loop`, and the same-session [goal domain](../../goal/goal/README.md) remains independent. The [Ralph Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) owns the policy and deferred work.
6
+
7
+ ## Contract
8
+
9
+ `ralph({ objective, maxRounds? })` waits for the entire run. The deployment config's `maxRounds` is both the default and a ceiling on a call override. Every Ralph round starts one child through `subagentProvider`; that provider must exist, support structured output, and report `inheritsParentContext: false`. The configured provider is carried as `WorkflowStartRequest.subagentProvider`, so the fixed script cannot inspect or change routing and the ordinary model-written `workflow` tool gains no provider selector. The resolved round cap is also carried as `WorkflowStartRequest.maxTotalAgents`, coordinating the fixed loop with the engine's total-child backstop; the engine rejects a Ralph cap above its deployment ceiling before publishing a run.
10
+
11
+ Each child receives only the immutable objective, its current Ralph round and cap, a shared-workspace-as-authority instruction, and the previous structured handoff. The workspace is long-term memory; parent conversation and prior child sessions are not seeded. Reports have `status: continue | complete | blocked`, a non-empty summary, evidence, next steps, and blocker text. Status-specific semantics and the serialized `maxHandoffChars` ceiling are validated inside the fixed workflow and again at the consumer boundary. Invalid, missing, or oversized reports fail the workflow instead of being truncated or mistaken for cap exhaustion.
12
+
13
+ The successful terminal tool result is `complete`, `blocked`, or `budget-limited`, with the last bounded report and number of rounds started. The canonical envelope is `{ runId, agentsStarted, result }`; completion and blocker labels in its Native renderer explicitly say that a worker reported the outcome, not independent certification. `maxResultChars` bounds only that rendered text including its truncation marker, without altering the validated report in the canonical value or the cross-round handoff.
14
+
15
+ An ordinary child failure produces an error naming the failed round and retaining the last successful handoff when one exists. Ralph does not retry that round. Fatal provider-start, transport, worker, or workflow failures remain workflow errors and may settle before the fixed script can return a handoff. Cancellation is also an error; partial output is never success.
16
+
17
+ ## Lifecycle and cancellation
18
+
19
+ The caller's agent is the parent of every fresh child, preserving cwd and lineage without copying its conversation. `exec.signal` enters the workflow engine and is also bridged to `run.cancel()` for implementation independence. The tool awaits `run.result` and calls `run.dispose()` in `finally`, so a cancelled parent step waits for the engine's bounded termination and child quiescence before returning.
20
+
21
+ ## Render intent
22
+
23
+ The pending call is a `generic` card titled `ralph`; the immutable objective is its `rawInput`. The result keeps the generic card. Both presentation functions depend only on tool arguments and the settled tool envelope.
24
+
25
+ ## Config
26
+
27
+ | Key | Default | Meaning |
28
+ |---|---|---|
29
+ | `subagentProvider` | `spawn` | Fresh structured-output provider used for every round. |
30
+ | `maxRounds` | `256` | Default and deployment ceiling for one Ralph run. |
31
+ | `maxHandoffChars` | `16384` | Maximum serialized characters in one round report. |
32
+ | `maxResultChars` | `16384` | Maximum characters in the complete successful parent result. |
33
+
34
+ All config values are normalized and validated when the plugin applies, including direct application outside Loader schema normalization. Provider capabilities are resolved immediately before each call because provider registration can change under plugin lifecycle and HMR.
35
+
36
+ ## Model Experience
37
+
38
+ ### System prompt
39
+
40
+ #### What the model sees
41
+
42
+ Every parent request in this plugin's registration scope receives the fixed routing guidance below.
43
+
44
+ ##### Ralph guidance
45
+
46
+ ```markdown
47
+ 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.
48
+ ```
49
+
50
+ #### Token effect
51
+
52
+ Small fixed guidance cost per request while the plugin is active.
53
+
54
+ #### KV Cache effect
55
+
56
+ Prefix-stable while the plugin scope and guidance text are unchanged. Activation or disposal may invalidate reuse from this prompt section.
57
+
58
+ ### Tool schema
59
+
60
+ #### What the model sees
61
+
62
+ The generated [`ralph` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ralph) exposes one required `objective` string and one optional `maxRounds` number. Provider choice, handoff size, report schema, workflow script, and orchestration behavior are deployment-owned and absent from the call surface.
63
+
64
+ #### Token effect
65
+
66
+ Small fixed schema cost on each request where the tool is visible.
67
+
68
+ #### KV Cache effect
69
+
70
+ Prefix-stable while the definition and visibility are unchanged.
71
+
72
+ ### Child requests and parent result
73
+
74
+ #### What the model sees
75
+
76
+ Each child sees the standalone fixed round prompt plus the structured-output capture contract. The parent sees only the original call and one terminal result containing a worker-reported status, round count, and pretty-printed final report; intermediate child messages and reports do not enter the parent conversation. A failed ordinary child instead yields an error with its round number and, after round one, the last successful handoff.
77
+
78
+ #### Token effect
79
+
80
+ Every round pays for a fresh child context. `maxHandoffChars` bounds cross-round state and `maxResultChars` independently bounds the complete successful parent text; child work remains outside the parent context.
81
+
82
+ #### KV Cache effect
83
+
84
+ Each fresh child has an independent request cache. The parent result appends after the reusable request prefix.
85
+
86
+ ## Known Limitations and Deferred Work
87
+
88
+ - **Completion is worker self-declaration** — there is no independent evaluator or verifier deciding whether the objective is actually complete; evaluator policy and evaluator-driven continuation are deferred.
89
+ - **Foreground only** — there is no task id, background collection, process-resume checkpoint, scheduler, or wall-clock start policy.
90
+ - **The workspace is the only cross-round long-term memory** — one bounded report is the explicit handoff, and uncommitted conversational reasoning disappears with each child.
91
+ - **One round is one fresh child** — there is no within-round fan-out, model/provider switching, fork context, or model-call-selected provider.
92
+ - **Ordinary child failure is terminal for the run** — the fixed script reports the failed round and last successful handoff but does not retry; fatal workflow infrastructure failures can end before that state is returned.
93
+ - **Only round count bounds aggregate effort** — token, price, and elapsed-time budgets are deferred.
package/README.zh.md ADDED
@@ -0,0 +1,93 @@
1
+ # @deepseek-ai/dsh-tool-ralph
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 面向模型的 `ralph` 工具运行固定的前台工作流,把一个不可变目标依次交给多个全新子 agent(智能体)。它展示如何把专用编排策略实现为基于 [`ctx.workflows`](../workflow/README.md) 和 [`ctx.subagents`](../../subagent/subagent/README.md) 的普通插件:不会向 `agent-loop` 添加 Ralph 模式或全新 agent loop(智能体循环),同会话的[目标领域](../../goal/goal/README.md)也保持独立。策略和暂缓事项由 [Ralph Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md)负责。
6
+
7
+ ## 约定
8
+
9
+ `ralph({ objective, maxRounds? })` 会等待整个运行完成。部署配置中的 `maxRounds` 既是默认值,也是调用覆盖值的上限。每个 Ralph Round 通过 `subagentProvider` 启动一个子 agent;该提供方必须存在、支持结构化输出,并报告 `inheritsParentContext: false`。已配置的提供方以 `WorkflowStartRequest.subagentProvider` 传递,使固定脚本无法检查或更改路由,普通的模型编写 `workflow` 工具也不会因此获得提供方选择器。解析后的 Round 上限还会作为 `WorkflowStartRequest.maxTotalAgents` 传递,使固定循环与引擎的子 agent 总数后备上限协同;Ralph 上限超过引擎部署上限时,引擎会在发布运行前拒绝。
10
+
11
+ 每个子 agent 只接收不可变目标、当前 Ralph Round 及其上限、一条「共享工作区是权威状态」指令,以及上一个结构化交接内容。工作区是长期记忆;不会把父级对话或先前子 agent 会话作为初始内容。报告包含 `status: continue | complete | blocked`、非空摘要、证据、后续步骤和阻塞文本。固定工作流内部及消费方边界都会校验特定状态的语义和序列化后的 `maxHandoffChars` 上限。无效、缺失或过大的报告会使工作流失败,而不会被截断或误认为上限耗尽。
12
+
13
+ 成功的终态工具结果为 `complete`、`blocked` 或 `budget-limited`,并包含最后一份有界报告和已启动的 Round 数量。规范包络为 `{ runId, agentsStarted, result }`;Native 渲染器中的完成与阻塞标签会明确说明结果由 worker 报告,而非独立认证。`maxResultChars` 只限制包含截断标记的渲染文本,不会改变规范值中经过校验的报告或跨 Round 交接内容。
14
+
15
+ 普通子 agent 失败会产生错误,其中标明失败的 Round;如果已有上一次成功交接,也会保留它。Ralph 不会重试该 Round。致命的提供方启动、传输、worker 或工作流失败仍是工作流错误,并可能在固定脚本返回交接内容前结算。取消同样属于错误;局部输出绝不会视为成功。
16
+
17
+ ## 生命周期与取消
18
+
19
+ 调用方 agent 是每个全新子 agent 的父级,因此会保留 cwd 和谱系,但不会复制其对话。`exec.signal` 进入工作流引擎,同时也桥接到 `run.cancel()`,以便不依赖具体实现。工具等待 `run.result` 并调用 `run.dispose()`,后一个调用位于 `finally` 中,因此取消的父级步骤会等到引擎完成有界终止且子 agent 完全停稳后才返回。
20
+
21
+ ## 渲染意图
22
+
23
+ 待处理调用使用 `generic` 卡片,标题为 `ralph`;不可变目标作为其 `rawInput`。结果继续使用 generic 卡片。两个呈现函数都只依赖工具参数和已结算的工具包络。
24
+
25
+ ## 配置
26
+
27
+ | 键 | 默认值 | 含义 |
28
+ |---|---|---|
29
+ | `subagentProvider` | `spawn` | 每个 Round 使用的全新结构化输出提供方。 |
30
+ | `maxRounds` | `256` | 一次 Ralph 运行的默认值和部署上限。 |
31
+ | `maxHandoffChars` | `16384` | 一份 Round 报告序列化后的最大字符数。 |
32
+ | `maxResultChars` | `16384` | 返回给父级的完整成功结果最大字符数。 |
33
+
34
+ 插件应用时会规范化并校验所有配置值,也包括绕过 Loader schema 规范化而直接应用的情况。每次调用前都会立即解析提供方能力,因为提供方注册可能随插件生命周期和热模块替换(HMR)变化。
35
+
36
+ ## 模型体验
37
+
38
+ ### 系统提示词
39
+
40
+ #### 模型看到的内容
41
+
42
+ 在该插件的注册作用域内,每个父级请求都会收到下方的固定路由指导。
43
+
44
+ ##### Ralph 指导
45
+
46
+ ```markdown
47
+ 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.
48
+ ```
49
+
50
+ #### Token 影响
51
+
52
+ 插件启用期间,每个请求都会产生少量固定的指导 token 开销。
53
+
54
+ #### KV Cache 影响
55
+
56
+ 只要插件作用域和指导文本不变,前缀就保持稳定。启用或 dispose(资源释放)可能会使从该提示词段起的缓存复用失效。
57
+
58
+ ### 工具 schema
59
+
60
+ #### 模型看到的内容
61
+
62
+ 已生成的 [`ralph` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ralph)公开一个必填 `objective` 字符串和一个可选 `maxRounds` 数字。提供方选择、交接大小、报告 schema、工作流脚本和编排行为均由部署侧控制,不在调用接口中。
63
+
64
+ #### Token 影响
65
+
66
+ 工具可见时,每个请求都会产生少量固定的 schema token 开销。
67
+
68
+ #### KV Cache 影响
69
+
70
+ 只要定义和可见性不变,前缀就保持稳定。
71
+
72
+ ### 子 agent 请求与父级结果
73
+
74
+ #### 模型看到的内容
75
+
76
+ 每个子 agent 都会看到独立的固定 Round 提示词和结构化输出捕获约定。父级只看到原始调用和一个终态结果,其中包含 worker 报告的状态、Round 数量及经过美化打印的最终报告;中间子 agent 消息和报告不会进入父级对话。普通子 agent 失败时会改为产生错误,其中包含对应 Round 编号;从第二个 Round 起,还会包含上一次成功交接。
77
+
78
+ #### Token 影响
79
+
80
+ 每个 Round 都会支付全新子 agent 上下文的成本。`maxHandoffChars` 限制跨 Round 状态,`maxResultChars` 独立限制完整的父级成功文本;子 agent 工作留在父级上下文之外。
81
+
82
+ #### KV Cache 影响
83
+
84
+ 每个全新子 agent 都有独立的请求缓存。父级结果追加在可复用请求前缀之后。
85
+
86
+ ## 已知限制与暂缓事项
87
+
88
+ - **完成由 worker 自行声明**:没有独立的评估器或验证器判断目标是否实际完成;评估器策略及评估器驱动的延续均暂缓处理。
89
+ - **仅支持前台**:没有 task id、后台收集、进程恢复检查点、调度器或基于挂钟时间的启动策略。
90
+ - **工作区是唯一的跨 Round 长期记忆**:一份有界报告作为显式交接内容,每个子 agent 结束后,未提交的对话推理都会消失。
91
+ - **一个 Round 对应一个全新子 agent**:Round 内没有扇出、模型/提供方切换、fork 上下文或由模型调用选择的提供方。
92
+ - **普通子 agent 失败会终止运行**:固定脚本报告失败的 Round 和上一次成功交接,但不会重试;致命的工作流基础设施失败可能在该状态返回前结束。
93
+ - **聚合工作量仅受 Round 数量限制**:token、价格和耗时预算均暂缓处理。
package/lib/index.js ADDED
@@ -0,0 +1,371 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ //#region lib/types/index.js
4
+ /**
5
+ * Model-facing foreground Ralph loop over the workflow and subagent seams. A
6
+ * fixed script starts one fresh structured-output child per round, carrying
7
+ * only the immutable objective and the previous bounded handoff between them.
8
+ * @module @deepseek-ai/dsh-tool-ralph
9
+ */
10
+ const name = "tool-ralph";
11
+ const inject = [
12
+ "tools",
13
+ "workflows",
14
+ "subagents",
15
+ "systemPrompt"
16
+ ];
17
+ /** Schemastery configuration for the Ralph tool. */
18
+ const Config = z.object({
19
+ subagentProvider: z.string().default("spawn"),
20
+ maxRounds: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(256),
21
+ maxHandoffChars: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(16384),
22
+ maxResultChars: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(16384)
23
+ });
24
+ const RALPH_META = {
25
+ name: "ralph-loop",
26
+ description: "Iterate toward one objective with a fresh child and bounded structured handoff per round.",
27
+ phases: [{
28
+ title: "Fresh-agent rounds",
29
+ detail: "One clean child context per Ralph round."
30
+ }]
31
+ };
32
+ /**
33
+ * Fixed, deployment-owned orchestration. The model supplies data only; it
34
+ * cannot alter the loop, provider route, schema, or handoff validation.
35
+ */
36
+ const RALPH_SCRIPT = String.raw`
37
+ const reportSchema = {
38
+ type: 'object',
39
+ properties: {
40
+ status: { type: 'string', enum: ['continue', 'complete', 'blocked'] },
41
+ summary: { type: 'string' },
42
+ evidence: { type: 'array', items: { type: 'string' } },
43
+ nextSteps: { type: 'array', items: { type: 'string' } },
44
+ blocker: { type: 'string' },
45
+ },
46
+ required: ['status', 'summary', 'evidence', 'nextSteps', 'blocker'],
47
+ additionalProperties: false,
48
+ }
49
+
50
+ function normalizedText(value) {
51
+ return typeof value === 'string' && value.length > 0 && value === value.trim()
52
+ }
53
+
54
+ function normalizedList(value) {
55
+ return Array.isArray(value) && value.every(normalizedText)
56
+ }
57
+
58
+ function validateReport(report) {
59
+ if (report === null || typeof report !== 'object' || Array.isArray(report)) {
60
+ throw new Error('Ralph child returned no structured round report')
61
+ }
62
+ if (!normalizedText(report.summary)) {
63
+ throw new Error('Ralph round report summary must be non-empty and normalized')
64
+ }
65
+ if (!normalizedList(report.evidence) || !normalizedList(report.nextSteps)) {
66
+ throw new Error('Ralph round report evidence and nextSteps must contain only non-empty normalized strings')
67
+ }
68
+ if (typeof report.blocker !== 'string' || report.blocker !== report.blocker.trim()) {
69
+ throw new Error('Ralph round report blocker must be a normalized string')
70
+ }
71
+ switch (report.status) {
72
+ case 'continue':
73
+ if (report.nextSteps.length === 0 || report.blocker !== '') {
74
+ throw new Error('a continuing Ralph report needs nextSteps and an empty blocker')
75
+ }
76
+ break
77
+ case 'complete':
78
+ if (report.evidence.length === 0 || report.nextSteps.length !== 0 || report.blocker !== '') {
79
+ throw new Error('a complete Ralph report needs evidence, no nextSteps, and an empty blocker')
80
+ }
81
+ break
82
+ case 'blocked':
83
+ if (!normalizedText(report.blocker)) {
84
+ throw new Error('a blocked Ralph report needs a concrete blocker')
85
+ }
86
+ break
87
+ default:
88
+ throw new Error('Ralph round report status is invalid')
89
+ }
90
+ const serialized = JSON.stringify(report)
91
+ if (serialized.length > args.maxHandoffChars) {
92
+ throw new Error('Ralph round report exceeds maxHandoffChars (' + serialized.length + ' > ' + args.maxHandoffChars + ')')
93
+ }
94
+ return report
95
+ }
96
+
97
+ let previous
98
+ phase('Fresh-agent rounds')
99
+ for (let round = 1; round <= args.maxRounds; round += 1) {
100
+ const prior = previous === undefined ? '(none — this is the first round)' : JSON.stringify(previous)
101
+ const prompt = [
102
+ 'You are one fresh worker in a foreground Ralph loop. You receive no parent conversation and no prior child session. Do not call the ralph tool: this round already is its worker.',
103
+ 'Immutable objective:\n' + args.objective,
104
+ 'Ralph round: ' + round + ' of ' + args.maxRounds + '.',
105
+ 'The shared workspace and its current working tree are the long-term memory and source of truth. Inspect them before acting, preserve existing work, perform concrete in-scope work, and verify what you change. Treat the previous report only as a bounded handoff; confirm it against the workspace.',
106
+ 'Previous structured handoff:\n' + prior,
107
+ 'Return one report with exact normalized strings. Use status continue with at least one nextSteps entry while useful work remains; complete only with concrete evidence and no nextSteps; blocked only when no meaningful progress is possible without human input or an external-state change. blocker must be empty unless blocked.',
108
+ ].join('\n\n')
109
+ const rawReport = await agent(prompt, {
110
+ label: 'Ralph round ' + round,
111
+ phase: 'Fresh-agent rounds',
112
+ schema: reportSchema,
113
+ })
114
+ if (rawReport === null) {
115
+ return { status: 'round-failed', roundsStarted: round, lastReport: previous ?? null }
116
+ }
117
+ const report = validateReport(rawReport)
118
+ if (report.status === 'complete') return { status: 'complete', roundsStarted: round, report }
119
+ if (report.status === 'blocked') return { status: 'blocked', roundsStarted: round, report }
120
+ previous = report
121
+ }
122
+ return { status: 'budget-limited', roundsStarted: args.maxRounds, report: previous }
123
+ `;
124
+ const DESCRIPTION = "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.";
125
+ /** Validate defaults even when a caller invokes apply() without Loader normalization. */
126
+ function resolveConfig(config) {
127
+ const subagentProvider = config.subagentProvider ?? "spawn";
128
+ const maxRounds = config.maxRounds ?? 256;
129
+ const maxHandoffChars = config.maxHandoffChars ?? 16384;
130
+ const maxResultChars = config.maxResultChars ?? 16384;
131
+ if (subagentProvider.length === 0 || subagentProvider !== subagentProvider.trim()) throw new TypeError("subagentProvider must be a non-empty normalized string");
132
+ if (!Number.isSafeInteger(maxRounds) || maxRounds < 1) throw new TypeError("maxRounds must be a positive safe integer");
133
+ if (!Number.isSafeInteger(maxHandoffChars) || maxHandoffChars < 1) throw new TypeError("maxHandoffChars must be a positive safe integer");
134
+ if (!Number.isSafeInteger(maxResultChars) || maxResultChars < 1) throw new TypeError("maxResultChars must be a positive safe integer");
135
+ return {
136
+ subagentProvider,
137
+ maxRounds,
138
+ maxHandoffChars,
139
+ maxResultChars
140
+ };
141
+ }
142
+ /** Resolve one model-selected cap against the deployment ceiling. */
143
+ function resolveMaxRounds(requested, ceiling) {
144
+ const value = requested ?? ceiling;
145
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("Ralph maxRounds must be a positive safe integer");
146
+ if (value > ceiling) throw new TypeError(`Ralph maxRounds ${value} exceeds the deployment ceiling ${ceiling}`);
147
+ return value;
148
+ }
149
+ /** Require the configured route to mean a genuinely fresh structured child. */
150
+ function requireFreshProvider(ctx, name) {
151
+ const provider = ctx.subagents.getProvider(name);
152
+ if (provider === void 0) throw new Error(`Ralph subagent provider "${name}" is not registered`);
153
+ if (!provider.capabilities.outputSchema) throw new Error(`Ralph subagent provider "${name}" does not support structured output`);
154
+ if (provider.inheritsParentContext) throw new Error(`Ralph subagent provider "${name}" inherits parent context; Ralph requires a fresh provider`);
155
+ return provider;
156
+ }
157
+ function isRecord(value) {
158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159
+ }
160
+ function normalizedText(value) {
161
+ return typeof value === "string" && value.length > 0 && value === value.trim();
162
+ }
163
+ function normalizedList(value) {
164
+ return Array.isArray(value) && value.every(normalizedText);
165
+ }
166
+ /** Defensively decode the fixed script's report across a provider boundary. */
167
+ function readReport(value, expectedStatus, maxChars) {
168
+ if (!isRecord(value) || Object.keys(value).sort().join(",") !== "blocker,evidence,nextSteps,status,summary" || value["status"] !== expectedStatus || !normalizedText(value["summary"]) || !normalizedList(value["evidence"]) || !normalizedList(value["nextSteps"]) || typeof value["blocker"] !== "string" || value["blocker"] !== value["blocker"].trim()) throw new Error("Ralph workflow returned a malformed round report");
169
+ const report = {
170
+ status: expectedStatus,
171
+ summary: value["summary"],
172
+ evidence: value["evidence"],
173
+ nextSteps: value["nextSteps"],
174
+ blocker: value["blocker"]
175
+ };
176
+ if (expectedStatus === "continue" && (report.nextSteps.length === 0 || report.blocker !== "")) throw new Error("Ralph workflow returned an invalid continuing report");
177
+ if (expectedStatus === "complete" && (report.evidence.length === 0 || report.nextSteps.length !== 0 || report.blocker !== "")) throw new Error("Ralph workflow returned an invalid completion report");
178
+ if (expectedStatus === "blocked" && !normalizedText(report.blocker)) throw new Error("Ralph workflow returned an invalid blocked report");
179
+ const chars = JSON.stringify(report).length;
180
+ if (chars > maxChars) throw new Error(`Ralph workflow returned an oversized handoff (${chars} > ${maxChars})`);
181
+ return report;
182
+ }
183
+ /** Defensively decode the fixed script's terminal value. */
184
+ function readRunResult(value, maxRounds, maxHandoffChars) {
185
+ if (!isRecord(value) || typeof value["roundsStarted"] !== "number" || !Number.isSafeInteger(value["roundsStarted"]) || value["roundsStarted"] < 1 || value["roundsStarted"] > maxRounds) throw new Error("Ralph workflow returned a malformed terminal result");
186
+ const roundsStarted = value["roundsStarted"];
187
+ switch (value["status"]) {
188
+ case "complete":
189
+ if (Object.keys(value).sort().join(",") !== "report,roundsStarted,status") throw new Error("Ralph workflow returned a malformed terminal result");
190
+ return {
191
+ status: "complete",
192
+ roundsStarted,
193
+ report: readReport(value["report"], "complete", maxHandoffChars)
194
+ };
195
+ case "blocked":
196
+ if (Object.keys(value).sort().join(",") !== "report,roundsStarted,status") throw new Error("Ralph workflow returned a malformed terminal result");
197
+ return {
198
+ status: "blocked",
199
+ roundsStarted,
200
+ report: readReport(value["report"], "blocked", maxHandoffChars)
201
+ };
202
+ case "budget-limited":
203
+ if (Object.keys(value).sort().join(",") !== "report,roundsStarted,status") throw new Error("Ralph workflow returned a malformed terminal result");
204
+ if (roundsStarted !== maxRounds) throw new Error("Ralph workflow returned budget-limited before the round limit");
205
+ return {
206
+ status: "budget-limited",
207
+ roundsStarted,
208
+ report: readReport(value["report"], "continue", maxHandoffChars)
209
+ };
210
+ case "round-failed":
211
+ if (Object.keys(value).sort().join(",") !== "lastReport,roundsStarted,status") throw new Error("Ralph workflow returned a malformed terminal result");
212
+ if (roundsStarted === 1) {
213
+ if (value["lastReport"] !== null) throw new Error("Ralph workflow returned an invalid first-round failure");
214
+ return {
215
+ status: "round-failed",
216
+ roundsStarted
217
+ };
218
+ }
219
+ if (value["lastReport"] === null) throw new Error("Ralph workflow returned a round failure without its last handoff");
220
+ return {
221
+ status: "round-failed",
222
+ roundsStarted,
223
+ lastReport: readReport(value["lastReport"], "continue", maxHandoffChars)
224
+ };
225
+ default: throw new Error("Ralph workflow returned an unknown terminal status");
226
+ }
227
+ }
228
+ /** A non-clean workflow finish is an error, never a partial Ralph success. */
229
+ function stopReasonError(result) {
230
+ switch (result.stopReason) {
231
+ case "completed": return;
232
+ case "cancelled": return `Ralph workflow was cancelled${result.error === void 0 ? "" : ` (${result.error})`}`;
233
+ case "error": return `Ralph workflow failed: ${result.error ?? "unknown error"}`;
234
+ /* v8 ignore start -- WorkflowStopReason is closed; a future variant must fail loud here. */
235
+ default: return `Ralph workflow ended abnormally (${String(result.stopReason)})`;
236
+ }
237
+ }
238
+ const TRUNCATION_NOTICE = "\n… [truncated]";
239
+ /** Bound complete parent-facing text, including its envelope and truncation marker. */
240
+ function boundResult(text, maxChars) {
241
+ if (text.length <= maxChars) return text;
242
+ if (maxChars <= 14) return TRUNCATION_NOTICE.slice(0, maxChars);
243
+ return `${text.slice(0, maxChars - 14)}${TRUNCATION_NOTICE}`;
244
+ }
245
+ /** Render the fixed terminal envelope without presenting self-report as certification. */
246
+ function renderResult(result, maxChars) {
247
+ const rounds = `${result.roundsStarted} round${result.roundsStarted === 1 ? "" : "s"}`;
248
+ let text;
249
+ switch (result.status) {
250
+ case "complete":
251
+ text = `Ralph worker reported completion after ${rounds}.\nFinal report:\n${JSON.stringify(result.report, null, 2)}`;
252
+ break;
253
+ case "blocked":
254
+ text = `Ralph worker reported a blocker after ${rounds}.\nFinal report:\n${JSON.stringify(result.report, null, 2)}`;
255
+ break;
256
+ case "budget-limited":
257
+ text = `Ralph reached its ${rounds} limit; the worker reported work remaining.\nFinal report:\n${JSON.stringify(result.report, null, 2)}`;
258
+ break;
259
+ }
260
+ return boundResult(text, maxChars);
261
+ }
262
+ /** Canonical Ralph result fields shared by schema inference and rendering. */
263
+ const RALPH_OUTPUT_PROPERTIES = {
264
+ runId: {
265
+ type: "string",
266
+ required: true
267
+ },
268
+ agentsStarted: {
269
+ type: "integer",
270
+ required: true
271
+ },
272
+ result: {
273
+ type: "json",
274
+ required: true
275
+ }
276
+ };
277
+ /** Render an ordinary child failure with the most recent durable handoff. */
278
+ function renderRoundFailure(result, maxChars) {
279
+ const header = `Ralph round ${result.roundsStarted} child failed before producing a structured report.`;
280
+ return boundResult(result.lastReport === void 0 ? `${header}\nNo previous handoff was available.` : `${header}\nLast successful handoff:\n${JSON.stringify(result.lastReport, null, 2)}`, maxChars);
281
+ }
282
+ function presentCall(args) {
283
+ return {
284
+ card: "generic",
285
+ title: "ralph",
286
+ rawInput: args.objective
287
+ };
288
+ }
289
+ function presentResult(args, result) {
290
+ return { card: "generic" };
291
+ }
292
+ /** Register the fixed Ralph tool and its explicit-ask usage policy. */
293
+ function apply(ctx, config) {
294
+ const resolved = resolveConfig(config);
295
+ ctx.systemPrompt.section({
296
+ name: "tool:ralph",
297
+ order: 116,
298
+ text: "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."
299
+ });
300
+ ctx.tools.register(defineTool({
301
+ name: "ralph",
302
+ description: DESCRIPTION,
303
+ parameters: {
304
+ objective: {
305
+ type: "string",
306
+ required: true,
307
+ description: "The immutable completion objective for every fresh Ralph round."
308
+ },
309
+ maxRounds: {
310
+ type: "number",
311
+ description: "Optional positive safe-integer round cap, bounded by the deployment ceiling."
312
+ }
313
+ },
314
+ output: {
315
+ schema: {
316
+ type: "object",
317
+ additionalProperties: false,
318
+ properties: RALPH_OUTPUT_PROPERTIES
319
+ },
320
+ render: (_args, value) => [{
321
+ type: "text",
322
+ text: renderResult(value.result, resolved.maxResultChars)
323
+ }]
324
+ },
325
+ async execute(args, exec) {
326
+ const parent = exec.agent;
327
+ if (parent === void 0) throw new Error("Ralph tool requires a calling agent (exec.agent was undefined)");
328
+ const objective = args.objective.trim();
329
+ if (objective.length === 0) throw new Error("Ralph objective must be a non-empty string");
330
+ const maxRounds = resolveMaxRounds(args.maxRounds, resolved.maxRounds);
331
+ requireFreshProvider(ctx, resolved.subagentProvider);
332
+ const run = ctx.workflows.start({
333
+ script: RALPH_SCRIPT,
334
+ meta: RALPH_META,
335
+ args: {
336
+ objective,
337
+ maxRounds,
338
+ maxHandoffChars: resolved.maxHandoffChars
339
+ },
340
+ subagentProvider: resolved.subagentProvider,
341
+ maxTotalAgents: maxRounds,
342
+ parent,
343
+ signal: exec.signal
344
+ });
345
+ const onAbort = () => {
346
+ run.cancel("parent step aborted");
347
+ };
348
+ exec.signal.addEventListener("abort", onAbort, { once: true });
349
+ if (exec.signal.aborted) run.cancel("parent step aborted");
350
+ try {
351
+ const settled = await run.result;
352
+ const error = stopReasonError(settled);
353
+ if (error !== void 0) throw new Error(error);
354
+ const value = readRunResult(settled.value, maxRounds, resolved.maxHandoffChars);
355
+ if (value.status === "round-failed") throw new Error(renderRoundFailure(value, resolved.maxResultChars));
356
+ return {
357
+ runId: run.id,
358
+ agentsStarted: settled.agentsStarted,
359
+ result: value
360
+ };
361
+ } finally {
362
+ exec.signal.removeEventListener("abort", onAbort);
363
+ await run.dispose();
364
+ }
365
+ },
366
+ presentCall,
367
+ presentResult
368
+ }));
369
+ }
370
+ //#endregion
371
+ export { Config, apply, inject, name };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-tool-ralph`.
4
+ * @module @deepseek-ai/dsh-tool-ralph/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-tool-ralph";
7
+ /** Cordis companion plugin name. */
8
+ const name = "tool-ralph-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this model-facing orchestration adapter owns no independent event stream;
13
+ * workflow and subagent owners validate the runs and child lifecycles it starts.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Model-facing foreground Ralph loop over the workflow and subagent seams. A
3
+ * fixed script starts one fresh structured-output child per round, carrying
4
+ * only the immutable objective and the previous bounded handoff between them.
5
+ * @module @deepseek-ai/dsh-tool-ralph
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import z from '@deepseek-ai/schemastery';
9
+ export declare const name = "tool-ralph";
10
+ export declare const inject: string[];
11
+ /** Deployment policy for the fixed Ralph workflow. */
12
+ export interface Config {
13
+ /** Fresh structured-output provider used for every round (default `spawn`). */
14
+ subagentProvider?: string;
15
+ /** Default and deployment ceiling for one call's round count (default 256). */
16
+ maxRounds?: number;
17
+ /** Maximum serialized characters in one structured handoff (default 16384). */
18
+ maxHandoffChars?: number;
19
+ /** Maximum characters in a successful parent-facing terminal text (default 16384). */
20
+ maxResultChars?: number;
21
+ }
22
+ /** Schemastery configuration for the Ralph tool. */
23
+ export declare const Config: z<Config>;
24
+ /** Register the fixed Ralph tool and its explicit-ask usage policy. */
25
+ export declare function apply(ctx: Context, config: Config): void;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-tool-ralph`.
3
+ * @module @deepseek-ai/dsh-tool-ralph/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "tool-ralph-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-tool-ralph",
3
+ "description": "Model-facing fresh-agent Ralph loop over the workflow and subagent seams",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/workflow/tool-ralph"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-subagent": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
38
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
39
+ "@deepseek-ai/dsh-workflow": "^0.0.1-rc.1",
40
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
41
+ "@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
42
+ "@deepseek-ai/dsh-agent": "^0.0.1-rc.1"
43
+ },
44
+ "dependencies": {
45
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1"
46
+ },
47
+ "devDependencies": {
48
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.1-rc.1",
49
+ "@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
50
+ "@deepseek-ai/dsh-agent-loop": "^0.0.1-rc.1",
51
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.0.1-rc.1",
52
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
53
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
54
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
55
+ "@deepseek-ai/dsh-subagent": "^0.0.1-rc.1",
56
+ "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1-rc.1",
57
+ "@deepseek-ai/dsh-subagent-spawn": "^0.0.1-rc.1",
58
+ "@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
59
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
60
+ "@deepseek-ai/dsh-workflow": "^0.0.1-rc.1",
61
+ "@deepseek-ai/dsh-workflow-workerthread": "^0.0.1-rc.1",
62
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
63
+ }
64
+ }