@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.
- package/INSTALL.md +89 -0
- package/INSTALL.zh.md +86 -0
- package/LICENSE +201 -0
- package/README.en.md +193 -0
- package/README.md +182 -2
- package/RELEASING.md +123 -0
- package/RELEASING.zh.md +115 -0
- package/contracts/fusion.ts +279 -0
- package/contracts/storage.sql +86 -0
- package/cordis.patch.yml +3 -0
- package/docs/COMPATIBILITY.md +162 -0
- package/docs/EVIDENCE.md +141 -0
- package/dshx.yml +39 -0
- package/lib/client.js +1920 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-fusion.js +12664 -0
- package/lib/types/activity.d.ts +20 -0
- package/lib/types/approval/logical.d.ts +10 -0
- package/lib/types/benchmark/naive.d.ts +37 -0
- package/lib/types/benchmark/native-run.d.ts +68 -0
- package/lib/types/benchmark/output-limits.d.ts +19 -0
- package/lib/types/bilingual.d.ts +2 -0
- package/lib/types/client/ActivityRows.d.ts +8 -0
- package/lib/types/client/FusionCache.d.ts +4 -0
- package/lib/types/client/FusionHint.d.ts +9 -0
- package/lib/types/client/FusionHistory.d.ts +5 -0
- package/lib/types/client/FusionPanelIcon.d.ts +13 -0
- package/lib/types/client/FusionRecovery.d.ts +5 -0
- package/lib/types/client/FusionSettings.d.ts +9 -0
- package/lib/types/client/FusionStatus.d.ts +5 -0
- package/lib/types/client/FusionTool.d.ts +4 -0
- package/lib/types/client/activity-model.d.ts +14 -0
- package/lib/types/client/i18n.d.ts +15 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/settings-events.d.ts +2 -0
- package/lib/types/client/status-polling.d.ts +9 -0
- package/lib/types/context/guard.d.ts +21 -0
- package/lib/types/context/handoff.d.ts +17 -0
- package/lib/types/contracts.d.ts +449 -0
- package/lib/types/digest.d.ts +5 -0
- package/lib/types/dsh-fusion.d.ts +13 -0
- package/lib/types/errors.d.ts +28 -0
- package/lib/types/evidence/gate.d.ts +23 -0
- package/lib/types/evidence/receipts.d.ts +65 -0
- package/lib/types/execution/write-lease.d.ts +42 -0
- package/lib/types/host/bindings.d.ts +30 -0
- package/lib/types/host/cache-defaults.d.ts +48 -0
- package/lib/types/host/cache-policy.d.ts +155 -0
- package/lib/types/host/change-evidence.d.ts +38 -0
- package/lib/types/host/compaction-routing.d.ts +15 -0
- package/lib/types/host/coordinator.d.ts +169 -0
- package/lib/types/host/evidence-page.d.ts +13 -0
- package/lib/types/host/history.d.ts +88 -0
- package/lib/types/host/http.d.ts +4 -0
- package/lib/types/host/model-output.d.ts +13 -0
- package/lib/types/host/native-activity.d.ts +26 -0
- package/lib/types/host/native-approvals.d.ts +29 -0
- package/lib/types/host/native-auxiliary.d.ts +17 -0
- package/lib/types/host/native-budget.d.ts +33 -0
- package/lib/types/host/native-checks.d.ts +104 -0
- package/lib/types/host/native-compaction.d.ts +8 -0
- package/lib/types/host/native-context.d.ts +36 -0
- package/lib/types/host/native-effects.d.ts +75 -0
- package/lib/types/host/native-evidence.d.ts +4 -0
- package/lib/types/host/native-exploration.d.ts +15 -0
- package/lib/types/host/native-keepalive.d.ts +82 -0
- package/lib/types/host/native-model-control.d.ts +99 -0
- package/lib/types/host/native-on-demand-context.d.ts +39 -0
- package/lib/types/host/native-request.d.ts +5 -0
- package/lib/types/host/native-review.d.ts +11 -0
- package/lib/types/host/native-role-sandbox.d.ts +49 -0
- package/lib/types/host/native-scopes.d.ts +49 -0
- package/lib/types/host/native-selection.d.ts +29 -0
- package/lib/types/host/native-task-context.d.ts +39 -0
- package/lib/types/host/native-usage.d.ts +46 -0
- package/lib/types/host/native-worker-brief.d.ts +74 -0
- package/lib/types/host/native-worker-notices.d.ts +15 -0
- package/lib/types/host/native-worker.d.ts +44 -0
- package/lib/types/host/native-workflow.d.ts +41 -0
- package/lib/types/host/route-transport.d.ts +22 -0
- package/lib/types/host/settings.d.ts +39 -0
- package/lib/types/host/shell.d.ts +11 -0
- package/lib/types/host/status.d.ts +7 -0
- package/lib/types/host/subagent-seam.d.ts +10 -0
- package/lib/types/host/tool-table.d.ts +35 -0
- package/lib/types/host/worker-terminal.d.ts +68 -0
- package/lib/types/host/workspace-guard.d.ts +13 -0
- package/lib/types/host/workspace.d.ts +52 -0
- package/lib/types/identity.d.ts +64 -0
- package/lib/types/index.d.ts +34 -0
- package/lib/types/message-source.d.ts +29 -0
- package/lib/types/profile/compactor.d.ts +5 -0
- package/lib/types/profile/keepalive.d.ts +3 -0
- package/lib/types/profile/resolve.d.ts +17 -0
- package/lib/types/prompts.d.ts +8 -0
- package/lib/types/recovery/reconcile.d.ts +28 -0
- package/lib/types/review/dispatch.d.ts +20 -0
- package/lib/types/review/protocol.d.ts +51 -0
- package/lib/types/review/ticket.d.ts +32 -0
- package/lib/types/status.d.ts +88 -0
- package/lib/types/store-migration.d.ts +29 -0
- package/lib/types/task/deletion.d.ts +64 -0
- package/lib/types/task/reducer.d.ts +6 -0
- package/lib/types/task/sqlite-store.d.ts +71 -0
- package/lib/types/task/state.d.ts +51 -0
- package/lib/types/task/status-index.d.ts +28 -0
- package/lib/types/task/store.d.ts +48 -0
- package/lib/types/usage/budget.d.ts +27 -0
- package/lib/types/usage/ledger.d.ts +66 -0
- package/lib/types/usage/normalize.d.ts +50 -0
- package/lib/types/usage/pricing.d.ts +18 -0
- package/package.json +178 -4
- package/profiles/fusion-auto.example.json +45 -0
- package/prompts/compact.md +7 -0
- package/prompts/lead.md +42 -0
- package/prompts/model-lead.md +13 -0
- package/prompts/model-worker.md +5 -0
- package/prompts/worker.md +21 -0
- package/src/activity.ts +23 -0
- package/src/approval/logical.ts +44 -0
- package/src/benchmark/naive.ts +216 -0
- package/src/benchmark/native-run.ts +215 -0
- package/src/benchmark/output-limits.ts +33 -0
- package/src/bilingual.ts +2 -0
- package/src/client/ActivityRows.tsx +59 -0
- package/src/client/FusionCache.tsx +130 -0
- package/src/client/FusionHint.tsx +39 -0
- package/src/client/FusionHistory.tsx +247 -0
- package/src/client/FusionPanelIcon.tsx +39 -0
- package/src/client/FusionRecovery.tsx +69 -0
- package/src/client/FusionSettings.tsx +205 -0
- package/src/client/FusionStatus.tsx +103 -0
- package/src/client/FusionTool.tsx +66 -0
- package/src/client/activity-model.ts +31 -0
- package/src/client/i18n.ts +30 -0
- package/src/client/index.ts +50 -0
- package/src/client/settings-events.ts +3 -0
- package/src/client/status-polling.ts +88 -0
- package/src/context/guard.ts +81 -0
- package/src/context/handoff.ts +28 -0
- package/src/contracts.ts +449 -0
- package/src/digest.ts +28 -0
- package/src/dsh-fusion.ts +321 -0
- package/src/errors.ts +34 -0
- package/src/evidence/gate.ts +290 -0
- package/src/evidence/receipts.ts +175 -0
- package/src/execution/write-lease.ts +187 -0
- package/src/host/bindings.ts +91 -0
- package/src/host/cache-defaults.ts +103 -0
- package/src/host/cache-policy.ts +168 -0
- package/src/host/change-evidence.ts +156 -0
- package/src/host/compaction-routing.ts +46 -0
- package/src/host/coordinator.ts +2160 -0
- package/src/host/evidence-page.ts +25 -0
- package/src/host/history.ts +114 -0
- package/src/host/http.ts +30 -0
- package/src/host/model-output.ts +22 -0
- package/src/host/native-activity.ts +199 -0
- package/src/host/native-approvals.ts +196 -0
- package/src/host/native-auxiliary.ts +19 -0
- package/src/host/native-budget.ts +112 -0
- package/src/host/native-checks.ts +474 -0
- package/src/host/native-compaction.ts +34 -0
- package/src/host/native-context.ts +204 -0
- package/src/host/native-effects.ts +260 -0
- package/src/host/native-evidence.ts +31 -0
- package/src/host/native-exploration.ts +114 -0
- package/src/host/native-keepalive.ts +292 -0
- package/src/host/native-model-control.ts +298 -0
- package/src/host/native-on-demand-context.ts +74 -0
- package/src/host/native-request.ts +12 -0
- package/src/host/native-review.ts +51 -0
- package/src/host/native-role-sandbox.ts +171 -0
- package/src/host/native-scopes.ts +203 -0
- package/src/host/native-selection.ts +110 -0
- package/src/host/native-task-context.ts +264 -0
- package/src/host/native-usage.ts +121 -0
- package/src/host/native-worker-brief.ts +122 -0
- package/src/host/native-worker-notices.ts +72 -0
- package/src/host/native-worker.ts +215 -0
- package/src/host/native-workflow.ts +103 -0
- package/src/host/route-transport.ts +54 -0
- package/src/host/settings.ts +93 -0
- package/src/host/shell.ts +13 -0
- package/src/host/status.ts +112 -0
- package/src/host/subagent-seam.ts +20 -0
- package/src/host/tool-table.ts +56 -0
- package/src/host/worker-terminal.ts +91 -0
- package/src/host/workspace-guard.ts +17 -0
- package/src/host/workspace.ts +184 -0
- package/src/identity.ts +76 -0
- package/src/index.ts +50 -0
- package/src/message-source.ts +31 -0
- package/src/profile/compactor.ts +26 -0
- package/src/profile/keepalive.ts +13 -0
- package/src/profile/resolve.ts +131 -0
- package/src/prompts.ts +32 -0
- package/src/recovery/reconcile.ts +38 -0
- package/src/review/dispatch.ts +48 -0
- package/src/review/protocol.ts +212 -0
- package/src/review/ticket.ts +166 -0
- package/src/status.ts +55 -0
- package/src/store-migration.ts +65 -0
- package/src/task/deletion.ts +105 -0
- package/src/task/reducer.ts +679 -0
- package/src/task/sqlite-store.ts +386 -0
- package/src/task/state.ts +72 -0
- package/src/task/status-index.ts +198 -0
- package/src/task/store.ts +287 -0
- package/src/usage/budget.ts +88 -0
- package/src/usage/ledger.ts +218 -0
- package/src/usage/normalize.ts +308 -0
- 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
|
+
}
|
package/src/bilingual.ts
ADDED
|
@@ -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
|
+
`
|