@mokuyoaxis/dsh-iris 0.1.3 → 0.2.0

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 (91) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
  2. package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
  3. package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
  4. package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
  5. package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
  6. package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
  7. package/CHANGELOG.md +77 -2
  8. package/README.en.md +98 -19
  9. package/README.md +98 -19
  10. package/bin/dsh-iris.js +392 -13
  11. package/docs/AGENT_QUICKSTART.md +35 -0
  12. package/docs/ARCHITECTURE.md +82 -52
  13. package/docs/ARTIFACT_LIBRARY.md +24 -0
  14. package/docs/ARTIFACT_MANIFEST.md +60 -0
  15. package/docs/CORE_RUNTIME_CONTRACT.md +83 -0
  16. package/docs/DSH_CORE_MIGRATION.md +106 -0
  17. package/docs/DSH_RC2_ADAPTATION.md +36 -0
  18. package/docs/HEADLESS_CLI.md +217 -0
  19. package/docs/HOST_ADAPTER_CONTRACT.md +105 -0
  20. package/docs/HOST_DOCTOR.md +49 -0
  21. package/docs/MODEL_PORT_CONTRACT.md +300 -0
  22. package/docs/PROVIDER_ADAPTER_CONTRACT.md +138 -0
  23. package/docs/PROVIDER_HEALTH.md +39 -0
  24. package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
  25. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  26. package/docs/ROADMAP.md +42 -76
  27. package/docs/SECURITY.md +10 -5
  28. package/docs/TASK_SEMANTICS.md +3 -2
  29. package/docs/assets/diagrams/iris-architecture.png +0 -0
  30. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  31. package/docs/assets/logo/README.md +55 -0
  32. package/docs/assets/logo/iris-bubble-concept.svg +67 -0
  33. package/docs/assets/logo/iris-bubble.svg +14 -0
  34. package/docs/assets/logo/iris-interim-flower.png +0 -0
  35. package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
  36. package/docs/assets/logo/iris-logo-mono.svg +78 -0
  37. package/docs/assets/logo/iris-logo.svg +78 -0
  38. package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
  39. package/docs/assets/logo/iris-mark-mono.svg +42 -0
  40. package/docs/assets/logo/iris-mark.svg +42 -0
  41. package/docs/releases/0.1.4.md +59 -0
  42. package/docs/releases/0.2.0.md +48 -0
  43. package/lib/actions.js +673 -525
  44. package/lib/adapters.js +58 -21
  45. package/lib/api.js +182 -16
  46. package/lib/artifacts.js +239 -0
  47. package/lib/bundled-skills.js +5 -5
  48. package/lib/client.js +1116 -86
  49. package/lib/command-service.js +420 -0
  50. package/lib/config.js +245 -49
  51. package/lib/core-artifact-store.js +598 -0
  52. package/lib/core-artifacts.js +4 -0
  53. package/lib/core-attention.js +103 -0
  54. package/lib/core-contract.js +105 -0
  55. package/lib/core-doctor.js +41 -0
  56. package/lib/core-lease-recovery.js +170 -0
  57. package/lib/core-runtime.js +232 -0
  58. package/lib/core-tasks.js +560 -0
  59. package/lib/core-user-projection.js +192 -0
  60. package/lib/doctor.js +260 -30
  61. package/lib/dsh-core-adapter.js +599 -0
  62. package/lib/dsh-host-adapter.js +240 -0
  63. package/lib/generation-input.js +95 -0
  64. package/lib/guard.js +3 -3
  65. package/lib/host-contract.js +131 -0
  66. package/lib/host-runtime.js +156 -0
  67. package/lib/index.js +255 -166
  68. package/lib/locate.js +1 -1
  69. package/lib/media.js +16 -6
  70. package/lib/model-invoker.js +218 -0
  71. package/lib/model-port-contract.js +333 -0
  72. package/lib/models.js +97 -11
  73. package/lib/prompt-optimizer.js +10 -20
  74. package/lib/provider-adapter.js +397 -0
  75. package/lib/provider-adapters.js +342 -0
  76. package/lib/provider-catalog.js +171 -0
  77. package/lib/provider-contract.js +72 -3
  78. package/lib/provider-health.js +59 -0
  79. package/lib/provider-protocol.js +51 -2
  80. package/lib/provider-task-runner.js +328 -0
  81. package/lib/render.js +43 -12
  82. package/lib/tasks.js +45 -9
  83. package/lib/vision.js +51 -56
  84. package/package.json +20 -14
  85. package/user_guide.md +67 -12
  86. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  87. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  88. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  89. package/docs/releases/0.1.1.md +0 -51
  90. package/docs/releases/0.1.2.md +0 -26
  91. package/docs/releases/0.1.3.md +0 -30
@@ -0,0 +1,420 @@
1
+ 'use strict';
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {
7
+ createCoreArtifact,
8
+ exportCoreArtifact,
9
+ inspectCoreArtifact,
10
+ listCoreArtifacts,
11
+ readCoreArtifactBytes,
12
+ rebuildCoreArtifactIndex
13
+ } from './core-artifacts.js';
14
+ import { inspectCoreTask, listCoreTasks } from './core-tasks.js';
15
+ import { cropImage, pixelDiff, PixelError } from './pixels.js';
16
+ import { extractFrames, normalizeFramesOptions, probeVideo } from './media-probe.js';
17
+ import { hasProviderOperation } from './provider-adapter.js';
18
+ import { createProviderTaskRunner } from './provider-task-runner.js';
19
+ import { GenerationInputError, normalizeGenerationInput } from './generation-input.js';
20
+
21
+ export const COMMAND_CONTRACT_VERSION = 0;
22
+ export const CORE_COMMANDS = Object.freeze([
23
+ 'crop', 'media.diff', 'media.frames', 'task.inspect', 'task.list', 'task.observe', 'task.reobserve', 'task.redeliver', 'task.cancel', 'task.retry',
24
+ 'artifact.inspect', 'artifact.list', 'artifact.export', 'artifact.rebuild'
25
+ ]);
26
+
27
+ export class CommandError extends Error {
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = 'CommandError';
31
+ this.code = code;
32
+ }
33
+ }
34
+
35
+ function commandError(code, message) {
36
+ return new CommandError(code, message);
37
+ }
38
+
39
+ function plain(value) {
40
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
41
+ }
42
+
43
+ function exactInput(input, allowed) {
44
+ if (!plain(input)) throw commandError('IRIS_COMMAND_INPUT_INVALID', 'Command 输入必须是 JSON 对象');
45
+ for (const key of Object.keys(input)) {
46
+ if (!allowed.includes(key)) throw commandError('IRIS_COMMAND_INPUT_INVALID', `Command 不支持输入字段:${key}`);
47
+ }
48
+ }
49
+
50
+ function localFile(value, field, label) {
51
+ const supplied = String(value || '').trim();
52
+ if (!supplied || !path.isAbsolute(supplied)) {
53
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', field + ' 必须是宿主可读的绝对路径');
54
+ }
55
+ try {
56
+ const resolved = fs.realpathSync.native(supplied);
57
+ if (!fs.statSync(resolved).isFile()) throw new Error('not file');
58
+ return resolved;
59
+ } catch (_) {
60
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', label + '不存在或不是普通文件');
61
+ }
62
+ }
63
+
64
+ function localImage(input) {
65
+ return localFile(input.image_path, 'image_path', '图片');
66
+ }
67
+
68
+ function coordinates(input) {
69
+ const values = {};
70
+ for (const name of ['left', 'top', 'width', 'height']) {
71
+ if (typeof input[name] !== 'number' || !Number.isFinite(input[name])) {
72
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', `${name} 必须是有限数字`);
73
+ }
74
+ values[name] = input[name];
75
+ }
76
+ return values;
77
+ }
78
+
79
+ function abortError() {
80
+ const error = new Error('Core Command 已取消');
81
+ error.name = 'AbortError';
82
+ return error;
83
+ }
84
+
85
+ export function createCommandService(runtime, ports = {}) {
86
+ if (!runtime || typeof runtime.run !== 'function') throw new TypeError('Command Service 需要 Core Runtime');
87
+
88
+ async function execute(name, input) {
89
+ if (!CORE_COMMANDS.includes(name)) throw commandError('IRIS_COMMAND_UNKNOWN', `未知 Command:${String(name)}`);
90
+ if (name === 'crop') {
91
+ exactInput(input, ['image_path', 'artifact_id', 'left', 'top', 'width', 'height']);
92
+ const hasPath = Boolean(String(input.image_path || '').trim());
93
+ const hasArtifact = Boolean(String(input.artifact_id || '').trim());
94
+ if (hasPath === hasArtifact) {
95
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', 'crop 必须且只能提供 image_path 或 artifact_id 之一');
96
+ }
97
+ const localPath = hasPath ? localImage(input) : null;
98
+ const region = coordinates(input);
99
+ return runtime.run('execute', async ({ dataRoot, signal }) => {
100
+ if (signal.aborted) throw abortError();
101
+ const source = hasArtifact ? readCoreArtifactBytes(dataRoot, input.artifact_id) : null;
102
+ const image = source ? source.bytes : localPath;
103
+ let cropped;
104
+ try { cropped = await cropImage({ input: image, ...region }); }
105
+ catch (error) {
106
+ if (error instanceof PixelError) throw commandError('IRIS_COMMAND_INPUT_INVALID', error.message);
107
+ throw commandError('IRIS_COMMAND_PROCESSING_FAILED', '图片无法裁剪;请确认格式受支持且文件可读取');
108
+ }
109
+ if (signal.aborted) throw abortError();
110
+ const artifact = createCoreArtifact(dataRoot, {
111
+ bytes: cropped.buffer,
112
+ mediaType: cropped.mime,
113
+ kind: 'crop',
114
+ metadata: { width: cropped.width, height: cropped.height },
115
+ relations: source ? [{ type: 'derived-from', artifactId: source.artifact.id }] : []
116
+ });
117
+ return Object.freeze({
118
+ contractVersion: COMMAND_CONTRACT_VERSION,
119
+ command: 'crop',
120
+ artifact
121
+ });
122
+ });
123
+ }
124
+ if (name === 'media.diff') {
125
+ exactInput(input, [
126
+ 'image_a_path', 'image_a_artifact_id', 'image_b_path', 'image_b_artifact_id',
127
+ 'grid', 'top_regions'
128
+ ]);
129
+ const aPathSupplied = Boolean(String(input.image_a_path || '').trim());
130
+ const aArtifactSupplied = Boolean(String(input.image_a_artifact_id || '').trim());
131
+ const bPathSupplied = Boolean(String(input.image_b_path || '').trim());
132
+ const bArtifactSupplied = Boolean(String(input.image_b_artifact_id || '').trim());
133
+ if (aPathSupplied === aArtifactSupplied || bPathSupplied === bArtifactSupplied) {
134
+ throw commandError('IRIS_COMMAND_INPUT_INVALID',
135
+ 'media.diff 的 A/B 两侧都必须且只能提供本地路径或 Artifact ID 之一');
136
+ }
137
+ const imageAPath = aPathSupplied
138
+ ? localFile(input.image_a_path, 'image_a_path', '图片 A') : null;
139
+ const imageBPath = bPathSupplied
140
+ ? localFile(input.image_b_path, 'image_b_path', '图片 B') : null;
141
+ const grid = input.grid === undefined ? 8 : Number(input.grid);
142
+ const topRegions = input.top_regions === undefined ? 3 : Number(input.top_regions);
143
+ if (!Number.isSafeInteger(grid) || grid < 1 || grid > 64
144
+ || !Number.isSafeInteger(topRegions) || topRegions < 1 || topRegions > 100) {
145
+ throw commandError('IRIS_COMMAND_INPUT_INVALID',
146
+ 'grid 必须是 1–64 的整数,top_regions 必须是 1–100 的整数');
147
+ }
148
+ return runtime.run('execute', async ({ dataRoot, signal }) => {
149
+ if (signal.aborted) throw abortError();
150
+ const sourceA = aArtifactSupplied
151
+ ? readCoreArtifactBytes(dataRoot, input.image_a_artifact_id) : null;
152
+ const sourceB = bArtifactSupplied
153
+ ? readCoreArtifactBytes(dataRoot, input.image_b_artifact_id) : null;
154
+ for (const source of [sourceA, sourceB]) {
155
+ if (source && !source.artifact.mediaType.startsWith('image/')) {
156
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', 'media.diff 的 Artifact 输入必须是图片');
157
+ }
158
+ }
159
+ let diff;
160
+ try {
161
+ diff = await pixelDiff({
162
+ inputA: sourceA ? sourceA.bytes : imageAPath,
163
+ inputB: sourceB ? sourceB.bytes : imageBPath,
164
+ grid,
165
+ topRegions
166
+ });
167
+ } catch (error) {
168
+ if (signal.aborted) throw abortError();
169
+ if (error instanceof PixelError) {
170
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', error.message);
171
+ }
172
+ throw commandError('IRIS_COMMAND_PROCESSING_FAILED',
173
+ '图片差异分析失败;请确认格式受支持且文件可读取');
174
+ }
175
+ if (signal.aborted) throw abortError();
176
+ const sourceIds = [...new Set([sourceA?.artifact.id, sourceB?.artifact.id].filter(Boolean))];
177
+ const metrics = Object.freeze({
178
+ ratio: diff.ratio,
179
+ diffPixels: diff.diffPixels,
180
+ totalPixels: diff.totalPixels,
181
+ width: diff.width,
182
+ height: diff.height,
183
+ worstRegions: Object.freeze(diff.worstRegions.map((region) => Object.freeze({ ...region })))
184
+ });
185
+ const artifact = createCoreArtifact(dataRoot, {
186
+ bytes: diff.heatmap,
187
+ mediaType: diff.mime,
188
+ kind: 'pixel-diff',
189
+ metadata: { ...metrics, worstRegions: diff.worstRegions },
190
+ relations: sourceIds.map((artifactId) => ({ type: 'derived-from', artifactId }))
191
+ });
192
+ return Object.freeze({
193
+ contractVersion: COMMAND_CONTRACT_VERSION,
194
+ command: name,
195
+ metrics,
196
+ artifact
197
+ });
198
+ });
199
+ }
200
+ if (name === 'media.frames') {
201
+ exactInput(input, ['video_path', 'artifact_id', 'max_frames', 'target_width', 'quality', 'format']);
202
+ const hasPath = Boolean(String(input.video_path || '').trim());
203
+ const hasArtifact = Boolean(String(input.artifact_id || '').trim());
204
+ if (hasPath === hasArtifact) {
205
+ throw commandError('IRIS_COMMAND_INPUT_INVALID',
206
+ 'media.frames 必须且只能提供 video_path 或 artifact_id 之一');
207
+ }
208
+ const videoPath = hasPath ? localFile(input.video_path, 'video_path', '视频') : null;
209
+ let frameOptions;
210
+ try {
211
+ frameOptions = normalizeFramesOptions({
212
+ ...(input.max_frames !== undefined ? { maxFrames: input.max_frames } : {}),
213
+ ...(input.target_width !== undefined ? { targetWidth: input.target_width } : {}),
214
+ ...(input.quality !== undefined ? { quality: input.quality } : {}),
215
+ ...(input.format !== undefined ? { format: input.format } : {})
216
+ });
217
+ } catch (error) {
218
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', error.message);
219
+ }
220
+ return runtime.run('execute', async ({ dataRoot, signal }) => {
221
+ if (signal.aborted) throw abortError();
222
+ const source = hasArtifact ? readCoreArtifactBytes(dataRoot, input.artifact_id) : null;
223
+ if (source && !source.artifact.mediaType.startsWith('video/')) {
224
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', 'media.frames 的 Artifact 输入必须是视频');
225
+ }
226
+ let temporaryDirectory;
227
+ let readablePath = videoPath;
228
+ if (source) {
229
+ try {
230
+ temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'iris-command-frames-'));
231
+ readablePath = path.join(temporaryDirectory, 'source.mp4');
232
+ fs.writeFileSync(readablePath, source.bytes, { mode: 0o600 });
233
+ } catch (_) {
234
+ if (temporaryDirectory) {
235
+ try { fs.rmSync(temporaryDirectory, { recursive: true, force: true }); } catch (_) { /* 临时副本 */ }
236
+ }
237
+ throw commandError('IRIS_COMMAND_PROCESSING_FAILED', '视频临时副本无法安全写入');
238
+ }
239
+ }
240
+ let media;
241
+ let frames;
242
+ try {
243
+ media = probeVideo(readablePath);
244
+ frames = await extractFrames({ inputPath: readablePath, ...frameOptions, signal });
245
+ } catch (error) {
246
+ if (signal.aborted) throw abortError();
247
+ throw commandError('IRIS_COMMAND_PROCESSING_FAILED',
248
+ '视频抽帧失败;请确认 ffmpeg 可用且文件格式受支持');
249
+ } finally {
250
+ if (temporaryDirectory) {
251
+ try { fs.rmSync(temporaryDirectory, { recursive: true, force: true }); } catch (_) { /* 临时副本 */ }
252
+ }
253
+ }
254
+ if (signal.aborted) throw abortError();
255
+ const relations = source ? [{ type: 'frame-of', artifactId: source.artifact.id }] : [];
256
+ const mediaType = frameOptions.format === 'png' ? 'image/png' : 'image/jpeg';
257
+ const artifacts = frames.map((frame, index) => createCoreArtifact(dataRoot, {
258
+ bytes: frame.buffer,
259
+ mediaType,
260
+ kind: 'video-frame',
261
+ metadata: {
262
+ frameIndex: index + 1,
263
+ atSec: frame.atSec,
264
+ width: frame.width,
265
+ height: frame.height
266
+ },
267
+ relations
268
+ }));
269
+ return Object.freeze({
270
+ contractVersion: COMMAND_CONTRACT_VERSION,
271
+ command: name,
272
+ media: Object.freeze({ ...media }),
273
+ artifacts: Object.freeze(artifacts)
274
+ });
275
+ });
276
+ }
277
+ // task.reobserve 是人工触发观察的对外命令名(DSH API/工作台),与 task.observe
278
+ // 共用同一份门与单步 poll 实现;语义不区分,绝不 submit。
279
+ if (name === 'task.observe' || name === 'task.reobserve') {
280
+ exactInput(input, ['task_id']);
281
+ return runtime.run('execute', async ({ dataRoot }) => {
282
+ const before = inspectCoreTask(dataRoot, input.task_id);
283
+ if (before.acceptance !== 'accepted' || !before.remoteTaskId || !['none', 'unknown'].includes(before.outcome)) throw commandError('IRIS_TASK_NOT_OBSERVABLE', 'Task 没有可观察的远端受理事实,或已终止;不会重新提交');
284
+ if (typeof ports.resolveTaskAdapter !== 'function') throw commandError('IRIS_COMMAND_PROVIDER_REQUIRED', 'task.observe 需要宿主显式提供 Task Provider resolver');
285
+ const adapter = await ports.resolveTaskAdapter(before);
286
+ if (!adapter || adapter.id !== before.providerId || !adapter.capabilities?.includes(before.capability)) throw commandError('IRIS_PROVIDER_TASK_IDENTITY_MISMATCH', 'Task Provider resolver 返回了不匹配的 Adapter');
287
+ if (!hasProviderOperation(adapter, 'poll')) throw commandError('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', 'Task 原 Provider 不支持 poll;不会重新提交');
288
+ const task = await createProviderTaskRunner(runtime).observe(input.task_id, adapter, { resumeActive: true });
289
+ return Object.freeze({ contractVersion: COMMAND_CONTRACT_VERSION, command: name, taskId: task.id, task });
290
+ });
291
+ }
292
+ // task.redeliver:只对 outcome=succeeded / deliveryState=failed 的任务重新取回
293
+ // 已生成的远端产物(re-poll 拿产物清单,绝不 submit,也不会重新生成)。
294
+ if (name === 'task.redeliver') {
295
+ exactInput(input, ['task_id']);
296
+ return runtime.run('execute', async ({ dataRoot }) => {
297
+ const before = inspectCoreTask(dataRoot, input.task_id);
298
+ if (before.outcome !== 'succeeded' || before.deliveryState !== 'failed' || !before.remoteTaskId) throw commandError('IRIS_TASK_NOT_REDELIVERABLE', 'Task 不处于可重新交付状态;不会重新生成');
299
+ if (typeof ports.resolveTaskAdapter !== 'function') throw commandError('IRIS_COMMAND_PROVIDER_REQUIRED', 'task.redeliver 需要宿主显式提供 Task Provider resolver');
300
+ const adapter = await ports.resolveTaskAdapter(before);
301
+ if (!adapter || adapter.id !== before.providerId || !adapter.capabilities?.includes(before.capability)) throw commandError('IRIS_PROVIDER_TASK_IDENTITY_MISMATCH', 'Task Provider resolver 返回了不匹配的 Adapter');
302
+ if (!hasProviderOperation(adapter, 'poll')) throw commandError('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', 'Task 原 Provider 不支持 poll;不会重新生成');
303
+ const task = await createProviderTaskRunner(runtime).redeliver(input.task_id, adapter);
304
+ return Object.freeze({ contractVersion: COMMAND_CONTRACT_VERSION, command: name, taskId: task.id, task });
305
+ });
306
+ }
307
+ // task.cancel:只对已受理、有远端 ID、结果未定论且非终态的任务开放。
308
+ // 只有 Provider 明确确认才写 outcome=canceled;不支持或无法确认时保持真实状态。
309
+ if (name === 'task.cancel') {
310
+ exactInput(input, ['task_id']);
311
+ return runtime.run('execute', async ({ dataRoot }) => {
312
+ const before = inspectCoreTask(dataRoot, input.task_id);
313
+ if (before.acceptance !== 'accepted' || !before.remoteTaskId
314
+ || !['none', 'unknown'].includes(before.outcome) || before.phase === 'terminal') {
315
+ throw commandError('IRIS_TASK_NOT_CANCELABLE', 'Task 缺少可取消的远端受理事实,或已终止');
316
+ }
317
+ if (before.cancelState !== 'none') {
318
+ throw commandError('IRIS_TASK_CANCEL_ALREADY_REQUESTED', '该 Task 已请求过取消;请显式重新观察以确认远端真实状态');
319
+ }
320
+ if (typeof ports.resolveTaskAdapter !== 'function') throw commandError('IRIS_COMMAND_PROVIDER_REQUIRED', 'task.cancel 需要宿主显式提供 Task Provider resolver');
321
+ const adapter = await ports.resolveTaskAdapter(before);
322
+ if (!adapter || adapter.id !== before.providerId || !adapter.capabilities?.includes(before.capability)) throw commandError('IRIS_PROVIDER_TASK_IDENTITY_MISMATCH', 'Task Provider resolver 返回了不匹配的 Adapter');
323
+ const task = await createProviderTaskRunner(runtime).cancel(input.task_id, adapter);
324
+ return Object.freeze({ contractVersion: COMMAND_CONTRACT_VERSION, command: name, taskId: task.id, task });
325
+ });
326
+ }
327
+ // task.retry:D4 重试为新任务。产生新的真实计费,必须显式 confirm_billing;
328
+ // 只对终态且未成功交付的 Task 开放;新 Task 有全新 id/attempts/binding 并记录
329
+ // 单向 retriedFrom 关系,旧 Task 不动;Core 不持久化 Prompt,prompt 必须由
330
+ // 调用方重新提供(绝不从旧记录"恢复")。
331
+ if (name === 'task.retry') {
332
+ exactInput(input, ['task_id', 'provider_input', 'model_ref', 'confirm_billing']);
333
+ if (input.confirm_billing !== true) {
334
+ throw commandError('IRIS_COMMAND_BILLING_CONFIRM_REQUIRED', '重试会创建新任务并可能产生重复生成费用;必须显式确认计费');
335
+ }
336
+ return runtime.run('execute', async ({ dataRoot }) => {
337
+ const before = inspectCoreTask(dataRoot, input.task_id);
338
+ if (before.phase !== 'terminal' || (before.outcome === 'succeeded' && before.deliveryState === 'ready')) {
339
+ throw commandError('IRIS_TASK_NOT_RETRYABLE', '只有终态且未成功交付的 Task 可以重试为新任务');
340
+ }
341
+ let normalized;
342
+ try { normalized = normalizeGenerationInput(before.capability, input.provider_input); }
343
+ catch (error) {
344
+ if (error instanceof GenerationInputError) throw commandError('IRIS_COMMAND_INPUT_INVALID', error.message);
345
+ throw error;
346
+ }
347
+ const providerInput = normalized.providerInput;
348
+ if (Object.hasOwn(input.provider_input, 'model_ref')) {
349
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', 'retry 的模型覆盖请使用外层 model_ref');
350
+ }
351
+ const modelRef = String(input.model_ref || '').trim();
352
+ if (input.model_ref !== undefined && !modelRef) throw commandError('IRIS_COMMAND_INPUT_INVALID', 'model_ref 必须是 providerId::modelId 复合引用');
353
+ if (typeof ports.resolveTaskCandidates !== 'function') throw commandError('IRIS_COMMAND_PROVIDER_REQUIRED', 'task.retry 需要宿主显式提供候选链 resolver');
354
+ const candidates = await ports.resolveTaskCandidates({ capability: before.capability, modelRef });
355
+ const result = await createProviderTaskRunner(runtime).submit({
356
+ capability: before.capability,
357
+ candidates,
358
+ providerInput,
359
+ retriedFrom: before.id
360
+ });
361
+ return Object.freeze({
362
+ contractVersion: COMMAND_CONTRACT_VERSION,
363
+ command: name,
364
+ taskId: result.taskId,
365
+ retriedFrom: before.id,
366
+ task: result.task,
367
+ ...(result.localError ? { localError: result.localError } : {})
368
+ });
369
+ });
370
+ }
371
+ if (name === 'task.inspect') {
372
+ exactInput(input, ['task_id']);
373
+ return runtime.run('inspect', ({ dataRoot }) => Object.freeze({
374
+ contractVersion: COMMAND_CONTRACT_VERSION,
375
+ command: name,
376
+ task: inspectCoreTask(dataRoot, input.task_id)
377
+ }));
378
+ }
379
+ if (name === 'task.list') {
380
+ exactInput(input, ['offset', 'limit']);
381
+ return runtime.run('inspect', ({ dataRoot }) => Object.freeze({
382
+ contractVersion: COMMAND_CONTRACT_VERSION,
383
+ command: name,
384
+ ...listCoreTasks(dataRoot, input)
385
+ }));
386
+ }
387
+ if (name === 'artifact.inspect') {
388
+ exactInput(input, ['artifact_id']);
389
+ return runtime.run('inspect', ({ dataRoot }) => Object.freeze({
390
+ contractVersion: COMMAND_CONTRACT_VERSION,
391
+ command: name,
392
+ artifact: inspectCoreArtifact(dataRoot, input.artifact_id)
393
+ }));
394
+ }
395
+ if (name === 'artifact.list') {
396
+ exactInput(input, ['offset', 'limit']);
397
+ return runtime.run('inspect', ({ dataRoot }) => Object.freeze({
398
+ contractVersion: COMMAND_CONTRACT_VERSION,
399
+ command: name,
400
+ ...listCoreArtifacts(dataRoot, input)
401
+ }));
402
+ }
403
+ if (name === 'artifact.rebuild') {
404
+ exactInput(input, []);
405
+ return runtime.run('recover', ({ dataRoot }) => Object.freeze({
406
+ contractVersion: COMMAND_CONTRACT_VERSION,
407
+ command: name,
408
+ ...rebuildCoreArtifactIndex(dataRoot)
409
+ }));
410
+ }
411
+ exactInput(input, ['artifact_id', 'output_path']);
412
+ return runtime.run('execute', ({ dataRoot }) => Object.freeze({
413
+ contractVersion: COMMAND_CONTRACT_VERSION,
414
+ command: name,
415
+ ...exportCoreArtifact(dataRoot, input.artifact_id, input.output_path)
416
+ }));
417
+ }
418
+
419
+ return Object.freeze({ contractVersion: COMMAND_CONTRACT_VERSION, commands: CORE_COMMANDS, execute });
420
+ }