@sqdr2/dsh-fusion 0.0.0-stage → 0.3.2

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.
Files changed (213) hide show
  1. package/INSTALL.md +89 -0
  2. package/INSTALL.zh.md +86 -0
  3. package/LICENSE +201 -0
  4. package/README.en.md +193 -0
  5. package/README.md +182 -2
  6. package/RELEASING.md +123 -0
  7. package/RELEASING.zh.md +115 -0
  8. package/contracts/fusion.ts +279 -0
  9. package/contracts/storage.sql +86 -0
  10. package/cordis.patch.yml +3 -0
  11. package/docs/COMPATIBILITY.md +162 -0
  12. package/docs/EVIDENCE.md +141 -0
  13. package/dshx.yml +39 -0
  14. package/lib/client.js +1920 -0
  15. package/lib/client.js.map +1 -0
  16. package/lib/dsh-fusion.js +12664 -0
  17. package/lib/types/activity.d.ts +20 -0
  18. package/lib/types/approval/logical.d.ts +10 -0
  19. package/lib/types/benchmark/naive.d.ts +37 -0
  20. package/lib/types/benchmark/native-run.d.ts +68 -0
  21. package/lib/types/benchmark/output-limits.d.ts +19 -0
  22. package/lib/types/bilingual.d.ts +2 -0
  23. package/lib/types/client/ActivityRows.d.ts +8 -0
  24. package/lib/types/client/FusionCache.d.ts +4 -0
  25. package/lib/types/client/FusionHint.d.ts +9 -0
  26. package/lib/types/client/FusionHistory.d.ts +5 -0
  27. package/lib/types/client/FusionPanelIcon.d.ts +13 -0
  28. package/lib/types/client/FusionRecovery.d.ts +5 -0
  29. package/lib/types/client/FusionSettings.d.ts +9 -0
  30. package/lib/types/client/FusionStatus.d.ts +5 -0
  31. package/lib/types/client/FusionTool.d.ts +4 -0
  32. package/lib/types/client/activity-model.d.ts +14 -0
  33. package/lib/types/client/i18n.d.ts +15 -0
  34. package/lib/types/client/index.d.ts +22 -0
  35. package/lib/types/client/settings-events.d.ts +2 -0
  36. package/lib/types/client/status-polling.d.ts +9 -0
  37. package/lib/types/context/guard.d.ts +21 -0
  38. package/lib/types/context/handoff.d.ts +17 -0
  39. package/lib/types/contracts.d.ts +449 -0
  40. package/lib/types/digest.d.ts +5 -0
  41. package/lib/types/dsh-fusion.d.ts +13 -0
  42. package/lib/types/errors.d.ts +28 -0
  43. package/lib/types/evidence/gate.d.ts +23 -0
  44. package/lib/types/evidence/receipts.d.ts +65 -0
  45. package/lib/types/execution/write-lease.d.ts +42 -0
  46. package/lib/types/host/bindings.d.ts +30 -0
  47. package/lib/types/host/cache-defaults.d.ts +48 -0
  48. package/lib/types/host/cache-policy.d.ts +155 -0
  49. package/lib/types/host/change-evidence.d.ts +38 -0
  50. package/lib/types/host/compaction-routing.d.ts +15 -0
  51. package/lib/types/host/coordinator.d.ts +169 -0
  52. package/lib/types/host/evidence-page.d.ts +13 -0
  53. package/lib/types/host/history.d.ts +88 -0
  54. package/lib/types/host/http.d.ts +4 -0
  55. package/lib/types/host/model-output.d.ts +13 -0
  56. package/lib/types/host/native-activity.d.ts +26 -0
  57. package/lib/types/host/native-approvals.d.ts +29 -0
  58. package/lib/types/host/native-auxiliary.d.ts +17 -0
  59. package/lib/types/host/native-budget.d.ts +33 -0
  60. package/lib/types/host/native-checks.d.ts +104 -0
  61. package/lib/types/host/native-compaction.d.ts +8 -0
  62. package/lib/types/host/native-context.d.ts +36 -0
  63. package/lib/types/host/native-effects.d.ts +75 -0
  64. package/lib/types/host/native-evidence.d.ts +4 -0
  65. package/lib/types/host/native-exploration.d.ts +15 -0
  66. package/lib/types/host/native-keepalive.d.ts +82 -0
  67. package/lib/types/host/native-model-control.d.ts +99 -0
  68. package/lib/types/host/native-on-demand-context.d.ts +39 -0
  69. package/lib/types/host/native-request.d.ts +5 -0
  70. package/lib/types/host/native-review.d.ts +11 -0
  71. package/lib/types/host/native-role-sandbox.d.ts +49 -0
  72. package/lib/types/host/native-scopes.d.ts +49 -0
  73. package/lib/types/host/native-selection.d.ts +29 -0
  74. package/lib/types/host/native-task-context.d.ts +39 -0
  75. package/lib/types/host/native-usage.d.ts +46 -0
  76. package/lib/types/host/native-worker-brief.d.ts +74 -0
  77. package/lib/types/host/native-worker-notices.d.ts +15 -0
  78. package/lib/types/host/native-worker.d.ts +44 -0
  79. package/lib/types/host/native-workflow.d.ts +41 -0
  80. package/lib/types/host/route-transport.d.ts +22 -0
  81. package/lib/types/host/settings.d.ts +39 -0
  82. package/lib/types/host/shell.d.ts +11 -0
  83. package/lib/types/host/status.d.ts +7 -0
  84. package/lib/types/host/subagent-seam.d.ts +10 -0
  85. package/lib/types/host/tool-table.d.ts +35 -0
  86. package/lib/types/host/worker-terminal.d.ts +68 -0
  87. package/lib/types/host/workspace-guard.d.ts +13 -0
  88. package/lib/types/host/workspace.d.ts +52 -0
  89. package/lib/types/identity.d.ts +64 -0
  90. package/lib/types/index.d.ts +34 -0
  91. package/lib/types/message-source.d.ts +29 -0
  92. package/lib/types/profile/compactor.d.ts +5 -0
  93. package/lib/types/profile/keepalive.d.ts +3 -0
  94. package/lib/types/profile/resolve.d.ts +17 -0
  95. package/lib/types/prompts.d.ts +8 -0
  96. package/lib/types/recovery/reconcile.d.ts +28 -0
  97. package/lib/types/review/dispatch.d.ts +20 -0
  98. package/lib/types/review/protocol.d.ts +51 -0
  99. package/lib/types/review/ticket.d.ts +32 -0
  100. package/lib/types/status.d.ts +88 -0
  101. package/lib/types/store-migration.d.ts +29 -0
  102. package/lib/types/task/deletion.d.ts +64 -0
  103. package/lib/types/task/reducer.d.ts +6 -0
  104. package/lib/types/task/sqlite-store.d.ts +71 -0
  105. package/lib/types/task/state.d.ts +51 -0
  106. package/lib/types/task/status-index.d.ts +28 -0
  107. package/lib/types/task/store.d.ts +48 -0
  108. package/lib/types/usage/budget.d.ts +27 -0
  109. package/lib/types/usage/ledger.d.ts +66 -0
  110. package/lib/types/usage/normalize.d.ts +50 -0
  111. package/lib/types/usage/pricing.d.ts +18 -0
  112. package/package.json +178 -4
  113. package/profiles/fusion-auto.example.json +45 -0
  114. package/prompts/compact.md +7 -0
  115. package/prompts/lead.md +42 -0
  116. package/prompts/model-lead.md +13 -0
  117. package/prompts/model-worker.md +5 -0
  118. package/prompts/worker.md +21 -0
  119. package/src/activity.ts +23 -0
  120. package/src/approval/logical.ts +44 -0
  121. package/src/benchmark/naive.ts +216 -0
  122. package/src/benchmark/native-run.ts +215 -0
  123. package/src/benchmark/output-limits.ts +33 -0
  124. package/src/bilingual.ts +2 -0
  125. package/src/client/ActivityRows.tsx +59 -0
  126. package/src/client/FusionCache.tsx +130 -0
  127. package/src/client/FusionHint.tsx +39 -0
  128. package/src/client/FusionHistory.tsx +247 -0
  129. package/src/client/FusionPanelIcon.tsx +39 -0
  130. package/src/client/FusionRecovery.tsx +69 -0
  131. package/src/client/FusionSettings.tsx +205 -0
  132. package/src/client/FusionStatus.tsx +103 -0
  133. package/src/client/FusionTool.tsx +66 -0
  134. package/src/client/activity-model.ts +31 -0
  135. package/src/client/i18n.ts +30 -0
  136. package/src/client/index.ts +50 -0
  137. package/src/client/settings-events.ts +3 -0
  138. package/src/client/status-polling.ts +88 -0
  139. package/src/context/guard.ts +81 -0
  140. package/src/context/handoff.ts +28 -0
  141. package/src/contracts.ts +449 -0
  142. package/src/digest.ts +28 -0
  143. package/src/dsh-fusion.ts +321 -0
  144. package/src/errors.ts +34 -0
  145. package/src/evidence/gate.ts +290 -0
  146. package/src/evidence/receipts.ts +175 -0
  147. package/src/execution/write-lease.ts +187 -0
  148. package/src/host/bindings.ts +91 -0
  149. package/src/host/cache-defaults.ts +103 -0
  150. package/src/host/cache-policy.ts +168 -0
  151. package/src/host/change-evidence.ts +156 -0
  152. package/src/host/compaction-routing.ts +46 -0
  153. package/src/host/coordinator.ts +2160 -0
  154. package/src/host/evidence-page.ts +25 -0
  155. package/src/host/history.ts +114 -0
  156. package/src/host/http.ts +30 -0
  157. package/src/host/model-output.ts +22 -0
  158. package/src/host/native-activity.ts +199 -0
  159. package/src/host/native-approvals.ts +196 -0
  160. package/src/host/native-auxiliary.ts +19 -0
  161. package/src/host/native-budget.ts +112 -0
  162. package/src/host/native-checks.ts +474 -0
  163. package/src/host/native-compaction.ts +34 -0
  164. package/src/host/native-context.ts +204 -0
  165. package/src/host/native-effects.ts +260 -0
  166. package/src/host/native-evidence.ts +31 -0
  167. package/src/host/native-exploration.ts +114 -0
  168. package/src/host/native-keepalive.ts +292 -0
  169. package/src/host/native-model-control.ts +298 -0
  170. package/src/host/native-on-demand-context.ts +74 -0
  171. package/src/host/native-request.ts +12 -0
  172. package/src/host/native-review.ts +51 -0
  173. package/src/host/native-role-sandbox.ts +171 -0
  174. package/src/host/native-scopes.ts +203 -0
  175. package/src/host/native-selection.ts +110 -0
  176. package/src/host/native-task-context.ts +264 -0
  177. package/src/host/native-usage.ts +121 -0
  178. package/src/host/native-worker-brief.ts +122 -0
  179. package/src/host/native-worker-notices.ts +72 -0
  180. package/src/host/native-worker.ts +215 -0
  181. package/src/host/native-workflow.ts +103 -0
  182. package/src/host/route-transport.ts +54 -0
  183. package/src/host/settings.ts +93 -0
  184. package/src/host/shell.ts +13 -0
  185. package/src/host/status.ts +112 -0
  186. package/src/host/subagent-seam.ts +20 -0
  187. package/src/host/tool-table.ts +56 -0
  188. package/src/host/worker-terminal.ts +91 -0
  189. package/src/host/workspace-guard.ts +17 -0
  190. package/src/host/workspace.ts +184 -0
  191. package/src/identity.ts +76 -0
  192. package/src/index.ts +50 -0
  193. package/src/message-source.ts +31 -0
  194. package/src/profile/compactor.ts +26 -0
  195. package/src/profile/keepalive.ts +13 -0
  196. package/src/profile/resolve.ts +131 -0
  197. package/src/prompts.ts +32 -0
  198. package/src/recovery/reconcile.ts +38 -0
  199. package/src/review/dispatch.ts +48 -0
  200. package/src/review/protocol.ts +212 -0
  201. package/src/review/ticket.ts +166 -0
  202. package/src/status.ts +55 -0
  203. package/src/store-migration.ts +65 -0
  204. package/src/task/deletion.ts +105 -0
  205. package/src/task/reducer.ts +679 -0
  206. package/src/task/sqlite-store.ts +386 -0
  207. package/src/task/state.ts +72 -0
  208. package/src/task/status-index.ts +198 -0
  209. package/src/task/store.ts +287 -0
  210. package/src/usage/budget.ts +88 -0
  211. package/src/usage/ledger.ts +218 -0
  212. package/src/usage/normalize.ts +308 -0
  213. package/src/usage/pricing.ts +95 -0
@@ -0,0 +1,216 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type { Agent } from '@deepseek-ai/dsh-agent'
3
+ import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
4
+ import type { SessionId } from '@deepseek-ai/dsh-session'
5
+ import { defineTool } from '@deepseek-ai/dsh-tools'
6
+ import { hasStartActivation, requireSubagentMethod } from '../host/subagent-seam.js'
7
+ import type { PairProfile } from '../contracts.js'
8
+ import { digestOf } from '../digest.js'
9
+ import type { SqliteFusionStore } from '../task/sqlite-store.js'
10
+ import { isShellTool, nativeShellTool } from '../host/shell.js'
11
+
12
+ /** A declared benchmark treatment, never installed in product sessions. */
13
+ export const naivePrompts = {
14
+ lead: 'Complete the user task. You may work directly or use naive_delegate for a bounded implementation or test task. Every delegation starts a fresh independent Worker, which sees only your brief and the shared workspace. Include the relevant requirements and constraints in that brief. Inspect the returned result, correct defects or delegate again as needed, and run relevant tests before finishing. Delegation and direct commands run sequentially under the same total allowance.',
15
+ worker: 'Complete the delegated task using the native tools. Inspect the code, implement the requested changes and run relevant tests. Preserve existing acceptance tests. Return a concise account of changes, verification and unresolved limitations. This is a fresh independent session; you have no earlier Worker conversation.',
16
+ }
17
+
18
+ export interface NaiveDelegation {
19
+ ordinal: number
20
+ callId: string
21
+ briefDigest: string
22
+ startedAt: string
23
+ endedAt: string | null
24
+ childSessionId: string | null
25
+ parentSessionId: string
26
+ state: 'starting' | 'running' | 'settled' | 'failed'
27
+ stopReason: string | null
28
+ disposed: boolean
29
+ failure: string | null
30
+ }
31
+
32
+ interface NaiveResult { readonly stopReason: string; readonly output: readonly { readonly type: string; readonly text?: string }[] }
33
+ interface NaiveChild {
34
+ readonly childId: SessionId
35
+ readonly result: Promise<NaiveResult>
36
+ dispose(): Promise<void>
37
+ /** Set only for the 0.2.0 `SubagentRun`. Absent means read the child from `ctx.agents`. */
38
+ readonly localAgent?: Agent
39
+ readonly readChild: 'localAgent' | 'registry'
40
+ }
41
+ interface NaiveSubagents {
42
+ startActivation?(spec: {
43
+ readonly provider: string
44
+ readonly label: string
45
+ readonly delivery: 'caller'
46
+ readonly request: {
47
+ readonly parent: Agent
48
+ readonly prompt: readonly { readonly type: 'text'; readonly text: string }[]
49
+ readonly agentOptions: object
50
+ readonly maxDepth: number
51
+ }
52
+ readonly signal: AbortSignal
53
+ }): Promise<{ readonly childId: SessionId; readonly result: Promise<NaiveResult>; dispose(): Promise<void> }>
54
+ start?(provider: string, request: {
55
+ readonly parent: Agent
56
+ readonly label: string
57
+ readonly prompt: readonly { readonly type: 'text'; readonly text: string }[]
58
+ readonly persona: string
59
+ readonly signal: AbortSignal
60
+ readonly agentOptions: object
61
+ readonly toolFilter: { readonly allow: readonly string[] }
62
+ readonly maxDepth: number
63
+ }): Promise<{ readonly id: SessionId; readonly localAgent: Agent | undefined; readonly result: Promise<NaiveResult>; dispose(): Promise<void> }>
64
+ }
65
+
66
+ /** Simple sequential coordinator using the native single-activation child lifecycle. */
67
+ export class NaiveCoordinator {
68
+ readonly delegations: NaiveDelegation[] = []
69
+ readonly #dispose: (() => void)[] = []
70
+ readonly #operations = new Set<Promise<string>>()
71
+ readonly #controllers = new Set<AbortController>()
72
+ #delegating = false
73
+ #writer: symbol | undefined
74
+ #closed = false
75
+ #cleanupFailure: unknown
76
+
77
+ constructor(readonly ctx: Context, readonly parent: Agent, readonly store: SqliteFusionStore,
78
+ readonly profile: PairProfile, readonly workerMaxTokens: number, readonly tools: readonly string[] = [nativeShellTool]) {
79
+ const provider = ctx.subagents.getProvider('spawn')
80
+ if (!provider || provider.inheritsParentContext !== false) throw new Error('Naive requires a fresh-context native spawn provider')
81
+ // 0.2.1 removed `persona` and `toolFilter` and rejects them. A benchmark
82
+ // baseline must not depend on a user-configured Agent Preset, so on that
83
+ // seam the Worker prompt and the declared tool set are applied at
84
+ // `agent/created`, before the child's first request. 0.2.0 still receives
85
+ // both on `start`, and this hook stays off so that path is unchanged.
86
+ if (hasStartActivation(ctx.subagents)) {
87
+ this.#dispose.push(ctx.on('agent/created', ({ agent }) => {
88
+ if (agent.session.header.parentSession !== parent.id) return undefined
89
+ agent.ctx.tools.restrict({ allow: [...this.tools] })
90
+ agent.ctx.systemPrompt.section({ name: 'naive-benchmark-worker', order: 90, text: naivePrompts.worker })
91
+ return undefined
92
+ }))
93
+ }
94
+ this.#dispose.push(parent.ctx.systemPrompt.section({ name: 'naive-benchmark', order: 90, text: naivePrompts.lead }))
95
+ // Deny overlap even if an external caller bypasses the native sibling
96
+ // scheduler. The declared tool remains exclusive under that scheduler.
97
+ this.#dispose.push(ctx.on('tools/execute', async (exec, next) => {
98
+ if (!exec.agent || !this.owns(exec.agent)) return next()
99
+ this.assertReady()
100
+ if (!isShellTool(exec.name)) return next()
101
+ if (this.#writer || this.#delegating && exec.agent === parent) throw new Error('Naive requires one native writer at a time')
102
+ this.#writer = exec.token
103
+ try { return await next() }
104
+ finally { if (this.#writer === exec.token) this.#writer = undefined }
105
+ }, { prepend: true }))
106
+ this.#dispose.push(parent.ctx.tools.register(defineTool({
107
+ name: 'naive_delegate',
108
+ description: 'Run one fresh independent Worker on a complete task brief. Wait for its result and dispose it. A subsequent delegation starts a new Worker without prior chat history. Use sequentially with other tools.',
109
+ parameters: { brief: { type: 'string', required: true } },
110
+ output: { schema: { type: 'string' }, render: (_args, text) => [{ type: 'text', text }] },
111
+ execute: async (args, exec) => {
112
+ if (exec.agent !== parent || this.#closed || this.#delegating || this.#writer) throw new Error('Naive delegation requires an idle exclusive Lead tool slot')
113
+ const operation = this.#delegate(args.brief, exec.callId, exec.signal)
114
+ this.#operations.add(operation)
115
+ try { return await operation }
116
+ finally { this.#operations.delete(operation) }
117
+ },
118
+ })))
119
+ }
120
+
121
+ owns(agent: Agent): boolean {
122
+ return agent === this.parent || agent.session.header.parentSession === this.parent.id
123
+ }
124
+
125
+ assertReady(): void {
126
+ if (this.#closed) throw new Error('Naive benchmark is closing')
127
+ if (this.#cleanupFailure) throw new Error(`Naive recording or cleanup failed: ${String(this.#cleanupFailure)}`)
128
+ }
129
+
130
+ async #delegate(brief: string, callId: string, signal: AbortSignal): Promise<string> {
131
+ if (!brief.trim()) throw new Error('A nonempty Worker brief is required')
132
+ signal.throwIfAborted()
133
+ this.#delegating = true
134
+ const controller = new AbortController()
135
+ this.#controllers.add(controller)
136
+ const row: NaiveDelegation = { ordinal: this.delegations.length + 1, callId, briefDigest: digestOf(brief),
137
+ startedAt: new Date().toISOString(), endedAt: null, childSessionId: null, parentSessionId: this.parent.id,
138
+ state: 'starting', stopReason: null, disposed: false, failure: null }
139
+ this.delegations.push(row)
140
+ const key = `naive-delegation:${this.parent.id}:${row.ordinal}`
141
+ let revision = 0, started: NaiveChild | undefined
142
+ const persist = () => { revision = this.store.writeDocument(key, revision, { ...row }) }
143
+ try {
144
+ persist()
145
+ const route = this.profile.worker
146
+ const prompt = [{ type: 'text' as const, text: brief }]
147
+ const agentOptions = { provider: route.provider, model: route.model,
148
+ ...(route.reasoningEffort ? { reasoningEffort: ReasoningEffortId(route.reasoningEffort) } : {}),
149
+ maxTokens: this.workerMaxTokens }
150
+ const maxDepth = (this.parent.session.header.delegationDepth ?? 0) + 1
151
+ const runSignal = AbortSignal.any([signal, controller.signal])
152
+ const subagents = this.ctx.subagents as NaiveSubagents
153
+ // 0.2.1: `startActivation` with `delivery: 'caller'` returns the result to
154
+ // the calling tool and does not push it into the Lead inbox. 0.2.0: the
155
+ // one-shot `start`, with persona and toolFilter still on the request.
156
+ const start = requireSubagentMethod(subagents, 'startActivation', 'start')
157
+ if (start === 'startActivation') {
158
+ const activation = await subagents.startActivation!({
159
+ provider: 'spawn', label: `Naive Worker ${row.ordinal}`, delivery: 'caller',
160
+ request: { parent: this.parent, prompt, agentOptions, maxDepth }, signal: runSignal,
161
+ })
162
+ started = { childId: activation.childId, result: activation.result, dispose: () => activation.dispose(), readChild: 'registry' }
163
+ } else {
164
+ const run = await subagents.start!('spawn', {
165
+ parent: this.parent, label: `Naive Worker ${row.ordinal}`, prompt, persona: naivePrompts.worker,
166
+ signal: runSignal, agentOptions, toolFilter: { allow: [...this.tools] }, maxDepth,
167
+ })
168
+ started = { childId: run.id, result: run.result, dispose: () => run.dispose(), localAgent: run.localAgent, readChild: 'localAgent' }
169
+ }
170
+ // A later validation/journal failure still owns disposal. Observe an
171
+ // infrastructure rejection immediately, before those checks can throw;
172
+ // the normal await below retains the actual rejection as the outcome.
173
+ void started.result.catch(() => undefined)
174
+ // 0.2.0 published the child on `SubagentRun.localAgent`. 0.2.1 removed
175
+ // that field; the same Agent is `ctx.agents.get(childId)`.
176
+ const child = started.readChild === 'localAgent' ? started.localAgent : this.ctx.agents.get(started.childId)
177
+ if (!child || child.session.header.parentSession !== this.parent.id) throw new Error('Naive requires an owned native child')
178
+ if (this.delegations.some(other => other !== row && other.childSessionId === started!.childId)) throw new Error('Naive child identity was reused')
179
+ row.childSessionId = started.childId
180
+ row.state = 'running'
181
+ persist()
182
+ const result = await started.result
183
+ row.stopReason = result.stopReason
184
+ row.state = 'settled'
185
+ persist()
186
+ if (result.stopReason !== 'completed') throw new Error(`Naive Worker ended ${result.stopReason}; its output is incomplete`)
187
+ return JSON.stringify({ childSessionId: started.childId, stopReason: result.stopReason, output: result.output })
188
+ } catch (error) {
189
+ row.state = 'failed'
190
+ row.failure = String(error)
191
+ throw error
192
+ } finally {
193
+ try {
194
+ if (started) {
195
+ await started.dispose()
196
+ if (this.ctx.agents.get(started.childId)) throw new Error('Naive child remained resident after disposal')
197
+ row.disposed = true
198
+ }
199
+ row.endedAt = new Date().toISOString()
200
+ persist()
201
+ } catch (error) { this.#cleanupFailure = error; throw error }
202
+ finally { this.#controllers.delete(controller); this.#delegating = false }
203
+ }
204
+ }
205
+
206
+ async close(): Promise<void> {
207
+ this.#closed = true
208
+ for (const controller of this.#controllers) controller.abort(new Error('Naive benchmark closing'))
209
+ await Promise.allSettled([...this.#operations])
210
+ for (const dispose of this.#dispose.reverse()) dispose()
211
+ if (this.#cleanupFailure) throw this.#cleanupFailure
212
+ if (this.#writer || this.#delegating || this.delegations.some(row => row.childSessionId && !row.disposed)) {
213
+ throw new Error('Naive child/tool quiescence unproven')
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,215 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdirSync, realpathSync, writeFileSync } from 'node:fs'
3
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path'
4
+ import type { Context } from '@deepseek-ai/cordis'
5
+ import { installModelSelection } from '@deepseek-ai/dsh-agent'
6
+ import { createUserMessage, isAgentLoopRequest, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
7
+ import { SessionId, type Session } from '@deepseek-ai/dsh-session'
8
+ import { TaskId } from '../contracts.js'
9
+ import type { PhysicalRoute } from '../contracts.js'
10
+ import { digestOf } from '../digest.js'
11
+ import { FusionCoordinator } from '../host/coordinator.js'
12
+ import { NativeRequestBudget } from '../host/native-budget.js'
13
+ import { nativeRequestAgent } from '../host/native-request.js'
14
+ import { observeNativeUsage } from '../host/native-usage.js'
15
+ import type { NativeUsageRecord } from '../host/native-usage.js'
16
+ import { nativeShellTool } from '../host/shell.js'
17
+ import { snapshotWorkspace } from '../host/workspace.js'
18
+ import type { ResolvedProfile } from '../profile/resolve.js'
19
+ import { profileRoutes } from '../profile/compactor.js'
20
+ import { SqliteFusionStore } from '../task/sqlite-store.js'
21
+ import { NaiveCoordinator, naivePrompts } from './naive.js'
22
+ import { resolveBenchmarkOutputLimits, type BenchmarkOutputLimits } from './output-limits.js'
23
+
24
+ export interface NativeBenchmarkOptions {
25
+ runId: string
26
+ variant: 'lead_only' | 'worker_only' | 'worker_selfreview' | 'naive' | 'fusion'
27
+ dataKind: 'real' | 'synthetic'
28
+ /** Version read by the trusted launcher from the loaded artifact's metadata. */
29
+ engineVersion?: string
30
+ workspace: string
31
+ /** Fresh directory, outside the candidate's filesystem. Never reused. */
32
+ output: string
33
+ prompt: string
34
+ profile: ResolvedProfile
35
+ /** Required for real runs: exact public model limits frozen before the attempt. */
36
+ outputLimits?: BenchmarkOutputLimits
37
+ /** One campaign-wide ledger, so attempts cannot reset the approved limit. */
38
+ budget: NativeRequestBudget
39
+ maxRequests: number
40
+ timeoutMs: number
41
+ }
42
+
43
+ // This is an explicit benchmark treatment, not a change to the Fusion prompt.
44
+ // The same Worker, tools, deadline and allowance cover both native turns.
45
+ const selfReviewPrompt = `Perform a second self-review of your work against the original user request. Inspect the current implementation, look for defects or missed requirements, fix any you find, and run the relevant tests. Preserve the existing acceptance tests. Use the remaining allowance for this same attempt; no additional budget has been granted. Finish with the result of this review and any unresolved limitations.`
46
+
47
+ /**
48
+ * Run one fresh attempt through the same native AgentLoop and FusionCoordinator
49
+ * as the product. The caller composes public Host services and a sandboxed shell;
50
+ * this function does not implement another LLM/tool loop or grade candidate code.
51
+ */
52
+ export async function runNativeBenchmark(ctx: Context, options: NativeBenchmarkOptions) {
53
+ if (!/^[a-zA-Z0-9._-]{1,120}$/.test(options.runId)) throw new Error('Invalid run id')
54
+ if (!['lead_only', 'worker_only', 'worker_selfreview', 'naive', 'fusion'].includes(options.variant)
55
+ || !['real', 'synthetic'].includes(options.dataKind)) throw new Error('Invalid benchmark mode')
56
+ if (!options.prompt.trim() || !Number.isSafeInteger(options.maxRequests) || options.maxRequests < 1
57
+ || !Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1) throw new Error('Bounded attempt limits required')
58
+ if (options.dataKind === 'real' && !options.outputLimits) throw new Error('Real attempts require preregistered role output limits')
59
+ const { profile, prompts } = options.profile
60
+ const benchmarkTools = [nativeShellTool, 'read', 'glob', 'grep']
61
+ for (const name of benchmarkTools) {
62
+ if (!ctx.tools.get(name)) throw new Error(`Native benchmark requires the ${name} tool before any model request`)
63
+ }
64
+ if (!profile.enabled) throw new Error('Disabled benchmark profile')
65
+ const workspace = realpathSync(options.workspace)
66
+ const inWorkspace = (path: string) => {
67
+ const rel = relative(workspace, resolve(path))
68
+ return rel === '' || !(rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
69
+ }
70
+ if (inWorkspace(options.output) || inWorkspace(options.budget.store.filename)
71
+ || options.budget.authorizationFile && inWorkspace(options.budget.authorizationFile)) {
72
+ throw new Error('Benchmark controller state and authorization must be outside the candidate workspace')
73
+ }
74
+ const selfReview = options.variant === 'worker_selfreview'
75
+ const role = options.variant === 'worker_only' || selfReview ? 'worker' : 'lead'
76
+ const route = profile[role]
77
+ const nativeRoute = { provider: route.provider, model: route.model,
78
+ ...(route.reasoningEffort ? { reasoningEffort: ReasoningEffortId(route.reasoningEffort) } : {}) }
79
+ options.budget.check(options.variant === 'fusion' ? profileRoutes(profile)
80
+ : options.variant === 'naive' ? [profile.lead, profile.worker] : [route])
81
+ const outputLimits = await resolveBenchmarkOutputLimits(ctx, profile)
82
+ if (options.outputLimits && digestOf(options.outputLimits) !== digestOf(outputLimits)) {
83
+ throw new Error('Frozen role output limits changed; preregister the new conditions before model calls')
84
+ }
85
+ // mkdir without recursive prevents overwriting any earlier attempt evidence.
86
+ mkdirSync(options.output, { mode: 0o700 })
87
+ const before = snapshotWorkspace(workspace)
88
+ const store = new SqliteFusionStore(join(options.output, 'fusion.sqlite'))
89
+ const soloTask = TaskId(`benchmark-${options.runId}`)
90
+ const startedAt = new Date().toISOString(), started = performance.now()
91
+ let requests = 0, timedOut = false, failure: string | null = null
92
+ let coordinator: FusionCoordinator | undefined
93
+ let naive: NaiveCoordinator | undefined
94
+ let stopUsage: (() => void) | undefined
95
+ let stopGuard: (() => void) | undefined
96
+ let stopSessions: (() => void) | undefined
97
+ let timer: ReturnType<typeof setTimeout> | undefined
98
+ let workerSession: Session | undefined
99
+ const childSessions = new Map<string, Session>()
100
+ const phases: Array<{ stage: 'implementation' | 'self_review'; completed: boolean; nativeRequests: number; endReason: unknown }> = []
101
+ const reserve = (actual: PhysicalRoute, maxTokens: number) => {
102
+ if (requests >= options.maxRequests) throw new Error('Attempt native request limit exhausted')
103
+ options.budget.reserve(actual, maxTokens)
104
+ requests++
105
+ }
106
+ let parent
107
+ try {
108
+ parent = (await ctx.agents.create({ sessionId: SessionId(`bench-${options.runId}-${randomUUID()}`),
109
+ meta: { cwd: workspace }, agentOptions: { ...nativeRoute, maxTokens: outputLimits.roles[role].maxTokens } })).agent
110
+ installModelSelection(parent.ctx, { current: nativeRoute, assembled: undefined })
111
+ } catch (error) { store.close(); throw error }
112
+ let taskId = soloTask
113
+ try {
114
+ // The native parent turn can drain its child before whenIdle returns. Keep
115
+ // public Session references from creation, including restored generations.
116
+ stopSessions = ctx.on('session/created', session => {
117
+ if (session.header.parentSession === parent.id) childSessions.set(session.id, session)
118
+ })
119
+ if (options.variant === 'fusion') {
120
+ coordinator = new FusionCoordinator(ctx, store, { profile: options.profile, workerTools: benchmarkTools,
121
+ leaseRoot: join(options.output, 'leases'), maxWorkerSteps: options.maxRequests, commandMaxSeconds: 60,
122
+ authorizeRequest: (_agent, binding) => { options.budget.check(profileRoutes(binding.profile)) },
123
+ reserveRequest: request => reserve(request, request.maxTokens ?? 0) })
124
+ await coordinator.select(parent)
125
+ taskId = coordinator.bindings.read(parent.id)!.binding.taskId
126
+ } else {
127
+ if (options.variant === 'naive') naive = new NaiveCoordinator(ctx, parent, store, profile, outputLimits.roles.worker.maxTokens, benchmarkTools)
128
+ stopUsage = observeNativeUsage(ctx, store, agent => agent === parent ? { taskId, role }
129
+ : naive?.owns(agent) ? { taskId, role: 'worker' } : undefined)
130
+ }
131
+ stopGuard = ctx.on('llm/stream', async function* (request, next) {
132
+ const agent = nativeRequestAgent(ctx, request)
133
+ if (!agent || (coordinator ? !coordinator.owner(agent) : naive ? !naive.owns(agent) : agent !== parent)) {
134
+ throw new Error('Benchmark refuses unowned auxiliary model calls')
135
+ }
136
+ naive?.assertReady()
137
+ if (isAgentLoopRequest(request) && request.purpose === undefined) {
138
+ const requestRole = coordinator?.owner(agent)?.role ?? (agent === parent ? role : 'worker')
139
+ const frozen = outputLimits.roles[requestRole]
140
+ if (request.provider !== frozen.provider || request.model !== frozen.model || request.maxTokens !== frozen.maxTokens) {
141
+ throw new Error(`Benchmark ${requestRole} request diverged from frozen role output limits`)
142
+ }
143
+ }
144
+ if (!coordinator) reserve(request, request.maxTokens ?? 0)
145
+ yield* next()
146
+ }, { prepend: true })
147
+ const frozen = { schemaVersion: 1, runId: options.runId, variant: options.variant, dataKind: options.dataKind,
148
+ startedAt, pluginVersion: options.engineVersion ?? 'unknown', profile, promptsDigest: digestOf(prompts), taskPromptDigest: digestOf(options.prompt),
149
+ initialSnapshot: before, maxRequests: options.maxRequests, timeoutMs: options.timeoutMs, outputLimits,
150
+ protocol: { requiredStages: selfReview ? ['implementation', 'self_review'] : ['implementation'],
151
+ selfReviewPromptDigest: selfReview ? digestOf(selfReviewPrompt) : null,
152
+ ...(naive ? { delegationMode: 'fresh-one-shot', rolePromptsDigest: digestOf(naivePrompts),
153
+ completionRule: 'parent-completed-and-children-quiescent' } : {}) },
154
+ tools: benchmarkTools, readToolSemantics: 'readonly-command-vm-python-regex-fnmatch-v1',
155
+ commandNetwork: 'caller-must-attest', authorizationId: options.budget.check([route]).authorizationId }
156
+ writeFileSync(join(options.output, 'manifest.json'), JSON.stringify(frozen, null, 2), { mode: 0o600, flag: 'wx' })
157
+ timer = setTimeout(() => {
158
+ timedOut = true
159
+ parent.cancel({ kind: 'hook', reason: 'Benchmark attempt deadline' })
160
+ for (const childId of childSessions.keys()) ctx.agents.get(SessionId(childId))?.cancel({ kind: 'hook', reason: 'Benchmark attempt deadline' })
161
+ }, options.timeoutMs)
162
+ const turn = async (stage: 'implementation' | 'self_review', text: string) => {
163
+ const start = parent.session.snapshotEvents().length
164
+ const requestsBefore = requests
165
+ parent.followup(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } }))
166
+ await parent.whenIdle()
167
+ const end = parent.session.snapshotEvents().slice(start).findLast(event => event.type === 'turn/end')
168
+ const endReason = end?.type === 'turn/end' ? end.data.reason : null
169
+ phases.push({ stage, completed: endReason?.kind === 'completed', nativeRequests: requests - requestsBefore, endReason })
170
+ }
171
+ await turn('implementation', options.prompt)
172
+ if (selfReview && phases[0]?.completed && !timedOut) await turn('self_review', selfReviewPrompt)
173
+ } catch (error) { failure = String(error) }
174
+ finally {
175
+ if (timer) clearTimeout(timer)
176
+ // Native release removes the Agent handle. Read the retained public
177
+ // Session only after close proves quiescence, including final events.
178
+ const childId = coordinator?.bindings.read(parent.id)?.binding.workerId
179
+ workerSession = childId ? childSessions.get(childId) : undefined
180
+ // Await the persistent child and native tools before declaring a snapshot.
181
+ try { await coordinator?.close(); await naive?.close() }
182
+ catch (error) { store.close(); throw error }
183
+ finally { stopGuard?.(); stopUsage?.(); stopSessions?.() }
184
+ }
185
+ try {
186
+ const state = coordinator?.state(taskId)
187
+ const events = parent.session.snapshotEvents()
188
+ const end = events.findLast(event => event.type === 'turn/end')
189
+ const endReason = end?.type === 'turn/end' ? end.data.reason : null
190
+ const protocolComplete = !timedOut && !failure && endReason?.kind === 'completed'
191
+ && phases.length === (selfReview ? 2 : 1) && phases.every(phase => phase.completed)
192
+ && (state ? state.phase === 'COMPLETED' && state.verification === 'verified' && !state.outcomeUnknown : true)
193
+ const usage = store.listDocumentIds(`usage:${taskId}:`).map(id => store.readDocument(id)!.value as NativeUsageRecord)
194
+ const delivered = snapshotWorkspace(workspace)
195
+ const workers = naive ? [...childSessions.values()] : workerSession ? [workerSession] : []
196
+ const workerSessions = workers.map((session, index) => ({ sessionId: session.id,
197
+ parentSessionId: session.header.parentSession, eventsFile: naive ? `worker-events-${String(index + 1).padStart(3, '0')}.json` : 'worker-events.json' }))
198
+ const result = { schemaVersion: 1, runId: options.runId, variant: options.variant, dataKind: options.dataKind,
199
+ startedAt, endedAt: new Date().toISOString(), wallSeconds: (performance.now() - started) / 1000,
200
+ parentSessionId: parent.id, workerSessionId: coordinator?.bindings.read(parent.id)?.binding.workerId ?? null,
201
+ taskId, nativeRequests: requests, protocolComplete, artifactPass: null,
202
+ workerParentSessionId: workerSession?.header.parentSession ?? null,
203
+ workerSessions, ...(naive ? { naiveDelegations: naive.delegations } : {}),
204
+ status: timedOut ? 'timeout' : protocolComplete ? 'completed' : 'failed', failure, endReason, phases,
205
+ deliveredSnapshot: delivered, fusionState: state ?? null, usage,
206
+ actualBilledUsd: null, apiEquivalentUsd: null, upstreamHttpCalls: null }
207
+ writeFileSync(join(options.output, 'parent-events.json'), JSON.stringify(events, null, 2), { mode: 0o600, flag: 'wx' })
208
+ workers.forEach((session, index) => writeFileSync(join(options.output, workerSessions[index]!.eventsFile),
209
+ JSON.stringify(session.snapshotEvents(), null, 2), { mode: 0o600, flag: 'wx' }))
210
+ writeFileSync(join(options.output, 'result.json'), JSON.stringify(result, null, 2), { mode: 0o600, flag: 'wx' })
211
+ return result
212
+ } finally { store.close() }
213
+ }
214
+
215
+ export { NativeRequestBudget }
@@ -0,0 +1,33 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type { PairProfile, Role } from '../contracts.js'
3
+ import { requestOutputReservation } from '../context/guard.js'
4
+ import { resolveModelOutputLimits } from '../host/model-output.js'
5
+
6
+ export interface BenchmarkOutputLimits {
7
+ policy: 'role-route-auto-v1'
8
+ roles: Record<Role, {
9
+ provider: string
10
+ model: string
11
+ contextWindow: number
12
+ adapterDefault: number | null
13
+ testedCap: number | null
14
+ fallback: number
15
+ maxTokens: number
16
+ }>
17
+ }
18
+
19
+ /** Run before preregistration, then compare the exact snapshot before spending.
20
+ * Every treatment uses the same role's automatic product allowance; the profile
21
+ * reservation is a fallback, not a universal per-request cap.
22
+ */
23
+ export async function resolveBenchmarkOutputLimits(ctx: Context, profile: PairProfile): Promise<BenchmarkOutputLimits> {
24
+ const roles = {} as BenchmarkOutputLimits['roles']
25
+ for (const role of ['lead', 'worker'] as const) {
26
+ const { provider, model } = profile[role]
27
+ const { contextWindow, defaultMaxTokens, testedOutputCap } = await resolveModelOutputLimits(ctx, provider, model)
28
+ const fallback = profile.context[role].reserveOutputTokens
29
+ roles[role] = { provider, model, contextWindow, adapterDefault: defaultMaxTokens ?? null, testedCap: testedOutputCap ?? null,
30
+ fallback, maxTokens: requestOutputReservation(contextWindow, undefined, defaultMaxTokens, testedOutputCap, fallback) }
31
+ }
32
+ return { policy: 'role-route-auto-v1', roles }
33
+ }
@@ -0,0 +1,2 @@
1
+ /** Host-side messages reach the UI and the command line as text; they carry both languages. */
2
+ export const bi = (zh: string, en: string) => `${zh} / ${en}`
@@ -0,0 +1,59 @@
1
+ import { useState } from 'react'
2
+ import { DiffBlock, TerminalBlock } from '@deepseek-ai/dsh-client-ui-primitives'
3
+ import { activityDiffs } from './activity-model.js'
4
+ import { tr, useLang, type Lang } from './i18n.js'
5
+ import type { ActivityRow } from './activity-model.js'
6
+
7
+ const labelsFor = (lang: Lang) => ({ codeLabel: tr(lang, '代码', 'Code'), wrapLabel: tr(lang, '自动换行', 'Wrap'), unwrapLabel: tr(lang, '取消自动换行', 'No wrap'),
8
+ copy: tr(lang, '复制', 'Copy'), copied: tr(lang, '已复制', 'Copied'), collapseAria: tr(lang, '收起内容', 'Collapse'), collapse: tr(lang, '收起', 'Collapse'),
9
+ expandAria: (n: number) => tr(lang, `展开其余 ${n} 行`, `Show ${n} more lines`), expand: (n: number) => tr(lang, `展开其余 ${n} 行`, `Show ${n} more lines`) })
10
+ // `commandLine` is required by TerminalBlockLabels on DSH 0.2.1 and ignored on 0.2.0.
11
+ const terminalLabelsFor = (lang: Lang) => ({ ...labelsFor(lang), commandLine: (n: number) => tr(lang, `命令行第 ${n} 行`, `command line ${n}`),
12
+ signal: (s: string) => tr(lang, `信号 ${s}`, `signal ${s}`), exitCode: (n: number) => tr(lang, `退出码 ${n}`, `exit code ${n}`),
13
+ running: tr(lang, '执行中', 'Running'), failed: tr(lang, '执行失败', 'Failed'), done: tr(lang, '已返回', 'Done'), noOutput: tr(lang, '无输出', 'No output'), noExitCode: tr(lang, '未记录退出码', 'No exit code') })
14
+ const titleFor = (lang: Lang): Record<string, string> => ({ bash: tr(lang, '终端', 'Terminal'), pwsh: tr(lang, '终端', 'Terminal'), read: tr(lang, '读取', 'Read'), write: tr(lang, '写入', 'Write'), edit: tr(lang, '编辑', 'Edit'),
15
+ glob: tr(lang, '查找文件', 'Find files'), grep: tr(lang, '搜索', 'Search'), job_output: tr(lang, '命令输出', 'Command output'), job_kill: tr(lang, '停止命令', 'Stop command'), str_replace_editor: tr(lang, '编辑', 'Edit') })
16
+
17
+ export function ActivityCard({ row, done, cwd, openFile }: {
18
+ row: ActivityRow; done: boolean; cwd?: string; openFile(path: string): void
19
+ }) {
20
+ const lang = useLang()
21
+ const [open, setOpen] = useState(false)
22
+ let args: Record<string, unknown> = {}
23
+ try { const parsed: unknown = JSON.parse(row.call.data.arguments); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) args = parsed as Record<string, unknown> } catch { /* Raw call remains inspectable below. */ }
24
+ const result = row.result?.data.message
25
+ const output = result?.content.flatMap(part => part.type === 'text' ? [part.text] : []).join('\n') ?? ''
26
+ const path = typeof args.file_path === 'string' ? args.file_path : typeof args.path === 'string' ? args.path : undefined
27
+ const command = typeof args.command === 'string' ? args.command : undefined
28
+ const exit = /\n\[exit code: (\d+)\]\s*$/.exec(output)
29
+ const failed = result?.isError || (exit && Number(exit[1]) !== 0)
30
+ const diffs = activityDiffs(row)
31
+ const label = titleFor(lang)[row.call.data.name] ?? row.call.data.name
32
+ const summary = path ?? (typeof args.description === 'string' ? args.description : command)
33
+ ?? (typeof args.pattern === 'string' ? args.pattern : label)
34
+ const status = failed ? tr(lang, '失败', 'Failed') : !row.result ? (done ? tr(lang, '结果未记录', 'No result recorded') : tr(lang, '执行中', 'Running')) : ''
35
+ return <div className="fusion-activity-row" data-fusion-tool={row.call.data.name} data-fusion-call={row.id}>
36
+ <button type="button" aria-expanded={open} onClick={() => setOpen(!open)} className="fusion-activity-toggle">
37
+ <span aria-hidden>{open ? '⌄' : '›'}</span><span>{label}</span>
38
+ <span className="fusion-activity-summary">{summary}</span>
39
+ {status && <small role="status" data-failed={Boolean(failed)}>{status}</small>}
40
+ </button>
41
+ {open && <div className="fusion-activity-body">
42
+ {path && <button type="button" className="fusion-activity-file" onClick={() => openFile(path)}>{tr(lang, '打开', 'Open')} {path}</button>}
43
+ {diffs.length ? <DiffBlock diffs={diffs} labels={labelsFor(lang)} maxLines={16} />
44
+ : (row.call.data.name === 'bash' || row.call.data.name === 'pwsh') && command ? <TerminalBlock command={command} cwd={cwd} output={output}
45
+ running={!row.result && !done} exitCode={exit ? Number(exit[1]) : undefined} labels={terminalLabelsFor(lang)} />
46
+ : <pre>{output || (row.result ? tr(lang, '已返回,无文本输出。', 'Returned without text output.') : row.call.data.arguments)}</pre>}
47
+ </div>}
48
+ </div>
49
+ }
50
+
51
+ export const activityStyles = `
52
+ .fusion-activity-row{font-size:13px;line-height:1.5;margin:4px 0;min-width:0;color:var(--dsw-alias-label-secondary)}
53
+ .fusion-activity-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:6px 0;background:none;border:0;color:inherit;text-align:left;cursor:pointer;font:inherit}
54
+ .fusion-activity-toggle:focus-visible,.fusion-activity-file:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
55
+ .fusion-activity-summary{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary)}
56
+ .fusion-activity-toggle small{white-space:nowrap}.fusion-activity-toggle small[data-failed=true]{color:var(--dsw-alias-state-error-primary)}
57
+ .fusion-activity-body{padding:4px 0 10px;overflow:hidden}.fusion-activity-body pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto;font-size:12px}
58
+ .fusion-activity-file{font:inherit;border:0;background:none;color:var(--dsw-alias-brand-primary);padding:0 0 6px;cursor:pointer}
59
+ `