@harness-mix/cli 0.2.0 → 0.2.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 (84) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/NOTICE +14 -0
  3. package/README.md +4 -24
  4. package/docs/multi-agent-collaboration.md +1 -1
  5. package/licenses/anthropic-codex-theme-MIT.txt +21 -0
  6. package/licenses/codex-dream-skin-MIT.txt +21 -0
  7. package/output/native-build/renderer-extension.js +703 -58
  8. package/package.json +192 -189
  9. package/scripts/collaboration-test.cjs +262 -200
  10. package/scripts/collaboration-ui-smoke.cjs +85 -77
  11. package/scripts/core-concurrency-test.cjs +23 -1
  12. package/scripts/e2e-pi-cancel.cjs +99 -0
  13. package/scripts/native-file-change-test.cjs +42 -2
  14. package/scripts/native-sidebar-test.cjs +8 -0
  15. package/scripts/native-vendor-adapters-test.cjs +67 -1
  16. package/scripts/pets-test.cjs +63 -0
  17. package/scripts/pi-cancel-race-test.cjs +102 -0
  18. package/scripts/stuck-turn-test.cjs +73 -0
  19. package/scripts/workspace-test.cjs +9 -0
  20. package/src/assets/skins/dream-skin/NOTICE.md +13 -0
  21. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp +0 -0
  22. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/theme.json +34 -0
  23. package/src/main/adapters/claude.js +9 -6
  24. package/src/main/adapters/pi-family.js +41 -8
  25. package/src/main/harness-adapter/event-normalizer.js +270 -258
  26. package/src/main/host/collaboration-mcp.cjs +8 -1
  27. package/src/main/host/collaboration-tools.js +1 -1
  28. package/src/main/host/collaboration.js +81 -11
  29. package/src/main/host/runtime.js +1462 -1341
  30. package/src/main/native/pets.js +79 -2
  31. package/src/main/native/protocol.js +1164 -1145
  32. package/src/main/native/thread-list.js +6 -1
  33. package/src/main/workspace/core-review.js +75 -0
  34. package/src/main/workspace/diff.js +5 -1
  35. package/src/main/workspace/file-changes.js +9 -4
  36. package/src/main/workspace/review-controller.js +115 -102
  37. package/src/main/workspace/review.js +90 -88
  38. package/src/native-ui/renderer-extension/dist/contract-audit.js +16637 -0
  39. package/src/native-ui/renderer-extension/dist/index.js +34853 -0
  40. package/src/native-ui/renderer-extension/dist/production.js +34263 -0
  41. package/src/native-ui/renderer-extension/dist/renderer-binding-probe.js +34254 -0
  42. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  43. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +11 -0
  44. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  45. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts +73 -0
  46. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts.map +1 -0
  47. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  48. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  49. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  50. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts +12 -0
  51. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts.map +1 -0
  52. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
  53. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
  56. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +1 -1
  58. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +9 -0
  60. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -1
  61. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
  63. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  64. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3181 -3171
  65. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +14 -0
  66. package/src/native-ui/renderer-extension/src/renderer-pet-switcher.ts +308 -0
  67. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  68. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +76 -21
  69. package/src/native-ui/renderer-extension/src/settings/collaboration-page.ts +139 -0
  70. package/src/native-ui/renderer-extension/src/settings/icons.ts +155 -153
  71. package/src/native-ui/renderer-extension/src/settings/localization.ts +785 -783
  72. package/src/native-ui/renderer-extension/src/settings/pages.ts +694 -681
  73. package/src/native-ui/renderer-extension/src/settings/pet-market.css +110 -1
  74. package/src/native-ui/renderer-extension/src/settings/pet-market.ts +276 -22
  75. package/src/native-ui/renderer-extension/src/settings/pets-client.ts +32 -0
  76. package/src/native-ui/renderer-extension/src/settings/shell.css +2294 -2255
  77. package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +51 -1
  78. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
  79. package/src/native-ui/renderer-extension/test/renderer-pet-switcher.test.ts +400 -0
  80. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -79
  81. package/src/native-ui/renderer-extension/test/settings/pet-market.test.ts +378 -0
  82. package/src/native-ui/renderer-extension/test/settings/pets-client.test.ts +83 -0
  83. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  84. package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +15 -1
@@ -1,258 +1,270 @@
1
- // Event Normalizer:把 Runtime 现有的统一事件(legacy kind)标准化为 CoreEvent(§12)。
2
- // 第一阶段不重写任何 Adapter,只在公共入口处 normalize。
3
- // 本模块对 Harness 名称零感知:输入已经是 Adapter 转换后的统一事件。
4
- const { classifyError, codexErrorInfoKey } = require('./error-kind');
5
-
6
- class EventNormalizer {
7
- /**
8
- * @param {{ threadId: string, source?: string }} context source 仅为溯源标签,不参与逻辑分支
9
- */
10
- constructor({ threadId, source } = {}) {
11
- this.threadId = threadId;
12
- this.source = source ?? null;
13
- this.turnId = null;
14
- this.turnActive = false;
15
- this.currentMessageItemId = null; // 当前 streaming 的 agent_message
16
- this.currentReasoningItemId = null; // 当前 streaming 的 reasoning
17
- this.segmentSequence = 0;
18
- this.activeSlot = null;
19
- this.interactions = new Map();
20
- this.toolItems = new Map(); // toolCallId -> itemId
21
- }
22
-
23
- /** Turn 开始:先产出 user_message(已完成),后续 delta 归入本 Turn */
24
- beginTurn(turnId, userText, attachments = []) {
25
- this.turnId = turnId;
26
- this.currentNativeRef = undefined;
27
- this.turnActive = true;
28
- this.currentMessageItemId = null;
29
- this.currentReasoningItemId = null;
30
- this.toolItems.clear();
31
- this.interactions.clear();
32
- this.activeSlot = null;
33
- this.segmentSequence = 0;
34
- const itemId = `item_user_${turnId}`;
35
- return [
36
- this.#event({ type: 'item.started', itemId, payload: { type: 'user_message', content: userText, ...(attachments.length ? { attachments } : {}) } }),
37
- this.#event({ type: 'item.completed', itemId }),
38
- ];
39
- }
40
-
41
- /**
42
- * legacy 统一事件 → CoreEvent 序列(一个 legacy 事件可展开为多个 CoreEvent)
43
- * @returns Array<Partial<CoreEvent>>
44
- */
45
- normalize(legacy) {
46
- if (!legacy || typeof legacy !== 'object') return [];
47
- if (!this.turnActive && ['text-delta', 'thinking-delta', 'tool', 'artifact', 'approval', 'interaction-responded', 'plan', 'file-change'].includes(legacy.kind)) return [];
48
- this.currentNativeRef = legacy.nativeRef;
49
- switch (legacy.kind) {
50
- case 'text-delta':
51
- return this.#delta('agent_message', 'currentMessageItemId', legacy.text);
52
- case 'thinking-delta':
53
- return this.#delta('reasoning', 'currentReasoningItemId', legacy.text);
54
- case 'tool':
55
- return this.#tool(legacy);
56
- case 'artifact':
57
- return this.#artifact(legacy);
58
- case 'approval':
59
- if (this.interactions.has(legacy.requestId)) return [];
60
- this.interactions.set(legacy.requestId, `interaction_${this.turnId}_${legacy.requestId}`);
61
- return [this.#approval(legacy), this.#event({ type: 'turn.waiting' })];
62
- case 'interaction-responded': {
63
- const itemId = this.interactions.get(legacy.requestId);
64
- if (!itemId) return [];
65
- this.interactions.delete(legacy.requestId);
66
- return [this.#event({ type: 'item.updated', itemId, payload: { interactionStatus: 'responded' } }),
67
- this.#event({ type: 'item.completed', itemId }),
68
- ...(this.interactions.size ? [] : [this.#event({ type: 'turn.resumed' })])];
69
- }
70
- case 'compaction':
71
- case 'context-compaction':
72
- return this.#compaction(legacy);
73
- case 'plan':
74
- return [this.#event({ type: 'plan.updated', payload: { entries: legacy.entries ?? [] } })];
75
- case 'file-change':
76
- return [this.#event({ type: 'files.updated', payload: { source: legacy.source ?? 'native', changes: legacy.changes ?? [] } })];
77
- case 'usage':
78
- return [this.#event({ type: 'usage.updated', payload: { ...(legacy.usage ?? {}) } })];
79
- case 'session':
80
- return [this.#event({
81
- type: 'thread.updated',
82
- payload: {
83
- ...(legacy.nativeSessionId ? { nativeSessionRef: { sessionId: String(legacy.nativeSessionId) } } : {}),
84
- ...(legacy.model ? { metadata: { model: legacy.model } } : {}),
85
- },
86
- })];
87
- case 'status':
88
- case 'notice':
89
- return this.#notice(legacy);
90
- case 'completed':
91
- // 结算闸门:Turn 已结算后迟到的重复结算事件不再产生 CoreEvent(如 cancel 后
92
- // 原生流仍送达 agent_settled / result),防止影子侧出现非法状态迁移。
93
- if (!this.turnActive) return [];
94
- this.turnActive = false;
95
- return [
96
- ...(!legacy.finalAnswer || legacy.stopReason && legacy.stopReason !== 'completed' || !this.currentMessageItemId ? [] : [this.#event({ type: 'item.updated', itemId: this.currentMessageItemId, payload: { phase: 'final' } })]),
97
- this.#event({ type: 'turn.completed', payload: { stopReason: legacy.stopReason ?? 'completed' } }),
98
- ];
99
- case 'error':
100
- if (!this.turnActive) return [];
101
- this.turnActive = false;
102
- return [this.#event({ type: 'turn.failed', payload: {
103
- message: String(legacy.message ?? 'unknown error'),
104
- errorKind: classifyError(legacy),
105
- ...(codexErrorInfoKey(legacy.codexErrorInfo) ? { codexErrorInfo: codexErrorInfoKey(legacy.codexErrorInfo) } : {}),
106
- } })];
107
- default:
108
- return [];
109
- }
110
- }
111
-
112
- /* ---------------- 内部 ---------------- */
113
-
114
- #event({ type, itemId, payload, nativeRef }) {
115
- return {
116
- threadId: this.threadId,
117
- turnId: this.turnId,
118
- itemId: itemId ?? null,
119
- source: this.source,
120
- nativeRef: { ...this.currentNativeRef, ...nativeRef },
121
- type,
122
- payload: payload ?? {},
123
- };
124
- }
125
-
126
- #delta(itemType, slot, text) {
127
- if (typeof text !== 'string' || !text) return [];
128
- const closed = this.activeSlot && this.activeSlot !== slot ? this.#closeSegment() : [];
129
- if (!this[slot]) {
130
- const itemId = this.#newSegment(slot, itemType);
131
- return [
132
- ...closed,
133
- this.#event({ type: 'item.started', itemId, payload: { type: itemType, ...(itemType === 'agent_message' ? { phase: 'progress' } : {}) } }),
134
- this.#event({ type: 'item.delta', itemId, payload: { text } }),
135
- ];
136
- }
137
- return [this.#event({ type: 'item.delta', itemId: this[slot], payload: { text } })];
138
- }
139
-
140
- // 同一 Turn 可能有多段 message/reasoning(与工具调用交替),用唯一后缀区分
141
- #newSegment(slot, itemType) {
142
- this.activeSlot = slot;
143
- this[slot] = `item_${itemType}_${this.turnId}_${++this.segmentSequence}`;
144
- return this[slot];
145
- }
146
-
147
- #closeSegment() {
148
- const itemId = this.activeSlot ? this[this.activeSlot] : null;
149
- this.currentMessageItemId = null;
150
- this.currentReasoningItemId = null;
151
- this.activeSlot = null;
152
- return itemId ? [this.#event({ type: 'item.completed', itemId })] : [];
153
- }
154
-
155
- #tool(legacy) {
156
- const key = legacy.toolCallId ?? `anon:${legacy.title ?? 'tool'}`;
157
- let itemId = this.toolItems.get(key);
158
- const events = [];
159
- const state = legacy.state ?? 'running';
160
- if (!itemId) {
161
- itemId = `item_tool_${this.turnId}_${this.toolItems.size}`;
162
- this.toolItems.set(key, itemId);
163
- events.push(...this.#closeSegment());
164
- // 新工具出现意味着上一段 message/reasoning 已结束,后续 delta 开新段
165
- this.currentMessageItemId = null;
166
- this.currentReasoningItemId = null;
167
- events.push(this.#event({
168
- type: 'item.started',
169
- itemId,
170
- nativeRef: legacy.toolCallId ? { toolCallId: String(legacy.toolCallId) } : undefined,
171
- payload: { type: 'tool_call', title: legacy.title ?? '工具', state,
172
- ...(legacy.collaboration ? { collaboration: legacy.collaboration } : {}) },
173
- }));
174
- }
175
- const patch = {};
176
- if (legacy.collaboration) patch.collaboration = legacy.collaboration;
177
- for (const k of ['detail', 'input', 'output']) {
178
- if (typeof legacy[k] === 'string') patch[k] = legacy[k];
179
- else if (legacy[k] != null) patch[k] = JSON.stringify(legacy[k]);
180
- }
181
- if (legacy.title) patch.title = legacy.title;
182
- patch.state = state;
183
- events.push(this.#event({ type: 'item.updated', itemId, payload: patch }));
184
- if (state !== 'running') {
185
- events.push(this.#event({ type: 'item.completed', itemId, payload: { state } }));
186
- }
187
- return events;
188
- }
189
-
190
- #artifact(legacy) {
191
- const a = legacy.artifact;
192
- if (!a || typeof a !== 'object') return [];
193
- // 产物挂在当前 agent_message 上;若尚无消息段则先建段
194
- if (!this.currentMessageItemId) {
195
- const itemId = this.#newSegment('currentMessageItemId', 'agent_message');
196
- return [
197
- this.#event({ type: 'item.started', itemId, payload: { type: 'agent_message' } }),
198
- this.#event({
199
- type: 'item.updated',
200
- itemId,
201
- payload: { artifacts: [{ id: a.id, type: a.type, name: a.name, mime: a.mime, uri: a.uri, data: a.data }] },
202
- }),
203
- ];
204
- }
205
- return [this.#event({
206
- type: 'item.updated',
207
- itemId: this.currentMessageItemId,
208
- payload: { artifacts: [{ id: a.id, type: a.type, name: a.name, mime: a.mime, uri: a.uri, data: a.data }] },
209
- })];
210
- }
211
-
212
- #approval(legacy) {
213
- const itemType = ['input', 'editor'].includes(legacy.method) ? 'question' : 'approval';
214
- return this.#event({
215
- type: 'item.started',
216
- itemId: this.interactions.get(legacy.requestId),
217
- nativeRef: legacy.requestId ? { interactionId: String(legacy.nativeRef?.interactionId ?? legacy.requestId) } : undefined,
218
- payload: {
219
- type: itemType,
220
- requestId: legacy.requestId,
221
- method: legacy.method,
222
- title: legacy.title,
223
- message: legacy.message,
224
- options: legacy.options,
225
- placeholder: legacy.placeholder,
226
- interactionStatus: 'pending',
227
- },
228
- });
229
- }
230
-
231
- #notice(legacy) {
232
- const text = legacy.text;
233
- if (typeof text !== 'string' || !text) return [];
234
- const itemId = `item_notice_${this.turnId ?? this.threadId}_${++this.segmentSequence}`;
235
- return [
236
- this.#event({ type: 'item.started', itemId, payload: { type: 'notice', level: legacy.level ?? 'info', content: text } }),
237
- this.#event({ type: 'item.completed', itemId }),
238
- ];
239
- }
240
-
241
- #compaction(legacy) {
242
- const closed = this.activeSlot ? this.#closeSegment() : [];
243
- const itemId = `item_compaction_${this.turnId ?? this.threadId}_${++this.segmentSequence}`;
244
- const payload = {
245
- type: 'context_compaction',
246
- ...(legacy.tokensBefore != null ? { tokensBefore: legacy.tokensBefore } : {}),
247
- ...(legacy.tokensAfter != null ? { tokensAfter: legacy.tokensAfter } : {}),
248
- ...(legacy.summary ? { summary: legacy.summary } : {}),
249
- };
250
- return [
251
- ...closed,
252
- this.#event({ type: 'item.started', itemId, payload }),
253
- this.#event({ type: 'item.completed', itemId, payload }),
254
- ];
255
- }
256
- }
257
-
258
- module.exports = { EventNormalizer };
1
+ // Event Normalizer:把 Runtime 现有的统一事件(legacy kind)标准化为 CoreEvent(§12)。
2
+ // 第一阶段不重写任何 Adapter,只在公共入口处 normalize。
3
+ // 本模块对 Harness 名称零感知:输入已经是 Adapter 转换后的统一事件。
4
+ const { classifyError, codexErrorInfoKey } = require('./error-kind');
5
+
6
+ class EventNormalizer {
7
+ /**
8
+ * @param {{ threadId: string, source?: string }} context source 仅为溯源标签,不参与逻辑分支
9
+ */
10
+ constructor({ threadId, source } = {}) {
11
+ this.threadId = threadId;
12
+ this.source = source ?? null;
13
+ this.turnId = null;
14
+ this.turnActive = false;
15
+ this.currentMessageItemId = null; // 当前 streaming 的 agent_message
16
+ this.currentReasoningItemId = null; // 当前 streaming 的 reasoning
17
+ this.segmentSequence = 0;
18
+ this.activeSlot = null;
19
+ this.interactions = new Map();
20
+ this.toolItems = new Map(); // toolCallId -> itemId
21
+ }
22
+
23
+ /** Turn 开始:先产出 user_message(已完成),后续 delta 归入本 Turn */
24
+ beginTurn(turnId, userText, attachments = []) {
25
+ this.turnId = turnId;
26
+ this.currentNativeRef = undefined;
27
+ this.turnActive = true;
28
+ this.currentMessageItemId = null;
29
+ this.currentReasoningItemId = null;
30
+ this.toolItems.clear();
31
+ this.interactions.clear();
32
+ this.activeSlot = null;
33
+ this.segmentSequence = 0;
34
+ const itemId = `item_user_${turnId}`;
35
+ return [
36
+ this.#event({ type: 'item.started', itemId, payload: { type: 'user_message', content: userText, ...(attachments.length ? { attachments } : {}) } }),
37
+ this.#event({ type: 'item.completed', itemId }),
38
+ ];
39
+ }
40
+
41
+ /**
42
+ * legacy 统一事件 → CoreEvent 序列(一个 legacy 事件可展开为多个 CoreEvent)
43
+ * @returns Array<Partial<CoreEvent>>
44
+ */
45
+ normalize(legacy) {
46
+ if (!legacy || typeof legacy !== 'object') return [];
47
+ if (!this.turnActive && ['text-delta', 'thinking-delta', 'tool', 'artifact', 'approval', 'interaction-responded', 'plan', 'file-change'].includes(legacy.kind)) return [];
48
+ this.currentNativeRef = legacy.nativeRef;
49
+ switch (legacy.kind) {
50
+ case 'text-delta':
51
+ return this.#textDelta(legacy.text);
52
+ case 'thinking-delta':
53
+ return this.#delta('reasoning', 'currentReasoningItemId', legacy.text);
54
+ case 'tool':
55
+ return this.#tool(legacy);
56
+ case 'artifact':
57
+ return this.#artifact(legacy);
58
+ case 'approval':
59
+ if (this.interactions.has(legacy.requestId)) return [];
60
+ this.interactions.set(legacy.requestId, `interaction_${this.turnId}_${legacy.requestId}`);
61
+ return [this.#approval(legacy), this.#event({ type: 'turn.waiting' })];
62
+ case 'interaction-responded': {
63
+ const itemId = this.interactions.get(legacy.requestId);
64
+ if (!itemId) return [];
65
+ this.interactions.delete(legacy.requestId);
66
+ return [this.#event({ type: 'item.updated', itemId, payload: { interactionStatus: 'responded' } }),
67
+ this.#event({ type: 'item.completed', itemId }),
68
+ ...(this.interactions.size ? [] : [this.#event({ type: 'turn.resumed' })])];
69
+ }
70
+ case 'compaction':
71
+ case 'context-compaction':
72
+ return this.#compaction(legacy);
73
+ case 'plan':
74
+ return [this.#event({ type: 'plan.updated', payload: { entries: legacy.entries ?? [] } })];
75
+ case 'file-change':
76
+ return [this.#event({ type: 'files.updated', payload: { source: legacy.source ?? 'native', changes: legacy.changes ?? [] } })];
77
+ case 'usage':
78
+ return [this.#event({ type: 'usage.updated', payload: { ...(legacy.usage ?? {}) } })];
79
+ case 'session':
80
+ return [this.#event({
81
+ type: 'thread.updated',
82
+ payload: {
83
+ ...(legacy.nativeSessionId ? { nativeSessionRef: { sessionId: String(legacy.nativeSessionId) } } : {}),
84
+ ...(legacy.model ? { metadata: { model: legacy.model } } : {}),
85
+ },
86
+ })];
87
+ case 'status':
88
+ case 'notice':
89
+ return this.#notice(legacy);
90
+ case 'completed':
91
+ // 结算闸门:Turn 已结算后迟到的重复结算事件不再产生 CoreEvent(如 cancel 后
92
+ // 原生流仍送达 agent_settled / result),防止影子侧出现非法状态迁移。
93
+ if (!this.turnActive) return [];
94
+ this.turnActive = false;
95
+ // 最后一段 agent_message 标记 phase:'final':Desktop 回合结算后把此前所有
96
+ // progress 段与工具项折叠进「用时」栏,仅保留该段作为唯一正式结论。
97
+ return [
98
+ ...(!legacy.finalAnswer || legacy.stopReason && legacy.stopReason !== 'completed' || !this.currentMessageItemId ? [] : [this.#event({ type: 'item.updated', itemId: this.currentMessageItemId, payload: { phase: 'final' } })]),
99
+ this.#event({ type: 'turn.completed', payload: { stopReason: legacy.stopReason ?? 'completed' } }),
100
+ ];
101
+ case 'error':
102
+ if (!this.turnActive) return [];
103
+ this.turnActive = false;
104
+ return [
105
+ this.#event({ type: 'turn.failed', payload: {
106
+ message: String(legacy.message ?? 'unknown error'),
107
+ errorKind: classifyError(legacy),
108
+ ...(codexErrorInfoKey(legacy.codexErrorInfo) ? { codexErrorInfo: codexErrorInfoKey(legacy.codexErrorInfo) } : {}),
109
+ } }),
110
+ ];
111
+ default:
112
+ return [];
113
+ }
114
+ }
115
+
116
+ /* ---------------- 内部 ---------------- */
117
+
118
+ #event({ type, itemId, payload, nativeRef }) {
119
+ return {
120
+ threadId: this.threadId,
121
+ turnId: this.turnId,
122
+ itemId: itemId ?? null,
123
+ source: this.source,
124
+ nativeRef: { ...this.currentNativeRef, ...nativeRef },
125
+ type,
126
+ payload: payload ?? {},
127
+ };
128
+ }
129
+
130
+ #textDelta(text) {
131
+ // 正文 delta 一律即时流式发出:开场白与工具间解说段以 phase:'progress' 实时可见
132
+ // (回合结构 = progress 段与工具项交错),回合结算时仅最后一段升级为 'final',
133
+ // Codex Desktop 原生把 progress 段随执行区折叠进「用时 XXm XXs」栏。
134
+ return this.#delta('agent_message', 'currentMessageItemId', text);
135
+ }
136
+
137
+ #delta(itemType, slot, text) {
138
+ if (typeof text !== 'string' || !text) return [];
139
+ const closed = this.activeSlot && this.activeSlot !== slot ? this.#closeSegment() : [];
140
+ if (!this[slot]) {
141
+ const itemId = this.#newSegment(slot, itemType);
142
+ return [
143
+ ...closed,
144
+ this.#event({ type: 'item.started', itemId, payload: { type: itemType, ...(itemType === 'agent_message' ? { phase: 'progress' } : {}) } }),
145
+ this.#event({ type: 'item.delta', itemId, payload: { text } }),
146
+ ];
147
+ }
148
+ return [this.#event({ type: 'item.delta', itemId: this[slot], payload: { text } })];
149
+ }
150
+
151
+ // 同一 Turn 可能有多段 message/reasoning(与工具调用交替),用唯一后缀区分
152
+ #newSegment(slot, itemType) {
153
+ this.activeSlot = slot;
154
+ this[slot] = `item_${itemType}_${this.turnId}_${++this.segmentSequence}`;
155
+ return this[slot];
156
+ }
157
+
158
+ #closeSegment() {
159
+ const itemId = this.activeSlot ? this[this.activeSlot] : null;
160
+ this.currentMessageItemId = null;
161
+ this.currentReasoningItemId = null;
162
+ this.activeSlot = null;
163
+ return itemId ? [this.#event({ type: 'item.completed', itemId })] : [];
164
+ }
165
+
166
+ #tool(legacy) {
167
+ const key = legacy.toolCallId ?? `anon:${legacy.title ?? 'tool'}`;
168
+ let itemId = this.toolItems.get(key);
169
+ const events = [];
170
+ const state = legacy.state ?? 'running';
171
+ if (!itemId) {
172
+ itemId = `item_tool_${this.turnId}_${this.toolItems.size}`;
173
+ this.toolItems.set(key, itemId);
174
+ events.push(...this.#closeSegment());
175
+ // 新工具出现意味着上一段 message/reasoning 已结束,后续 delta 开新段
176
+ this.currentMessageItemId = null;
177
+ this.currentReasoningItemId = null;
178
+ events.push(this.#event({
179
+ type: 'item.started',
180
+ itemId,
181
+ nativeRef: legacy.toolCallId ? { toolCallId: String(legacy.toolCallId) } : undefined,
182
+ payload: { type: 'tool_call', title: legacy.title ?? '工具', state,
183
+ ...(legacy.collaboration ? { collaboration: legacy.collaboration } : {}) },
184
+ }));
185
+ }
186
+ const patch = {};
187
+ if (legacy.collaboration) patch.collaboration = legacy.collaboration;
188
+ if (typeof legacy.path === 'string' && legacy.path) patch.path = legacy.path;
189
+ for (const k of ['detail', 'input', 'output']) {
190
+ if (typeof legacy[k] === 'string') patch[k] = legacy[k];
191
+ else if (legacy[k] != null) patch[k] = JSON.stringify(legacy[k]);
192
+ }
193
+ if (legacy.title) patch.title = legacy.title;
194
+ patch.state = state;
195
+ events.push(this.#event({ type: 'item.updated', itemId, payload: patch }));
196
+ if (state !== 'running') {
197
+ events.push(this.#event({ type: 'item.completed', itemId, payload: { state } }));
198
+ }
199
+ return events;
200
+ }
201
+
202
+ #artifact(legacy) {
203
+ const a = legacy.artifact;
204
+ if (!a || typeof a !== 'object') return [];
205
+ // 产物挂在当前 agent_message 上;若尚无消息段则先建段
206
+ if (!this.currentMessageItemId) {
207
+ const itemId = this.#newSegment('currentMessageItemId', 'agent_message');
208
+ return [
209
+ this.#event({ type: 'item.started', itemId, payload: { type: 'agent_message' } }),
210
+ this.#event({
211
+ type: 'item.updated',
212
+ itemId,
213
+ payload: { artifacts: [{ id: a.id, type: a.type, name: a.name, mime: a.mime, uri: a.uri, data: a.data }] },
214
+ }),
215
+ ];
216
+ }
217
+ return [this.#event({
218
+ type: 'item.updated',
219
+ itemId: this.currentMessageItemId,
220
+ payload: { artifacts: [{ id: a.id, type: a.type, name: a.name, mime: a.mime, uri: a.uri, data: a.data }] },
221
+ })];
222
+ }
223
+
224
+ #approval(legacy) {
225
+ const itemType = ['input', 'editor'].includes(legacy.method) ? 'question' : 'approval';
226
+ return this.#event({
227
+ type: 'item.started',
228
+ itemId: this.interactions.get(legacy.requestId),
229
+ nativeRef: legacy.requestId ? { interactionId: String(legacy.nativeRef?.interactionId ?? legacy.requestId) } : undefined,
230
+ payload: {
231
+ type: itemType,
232
+ requestId: legacy.requestId,
233
+ method: legacy.method,
234
+ title: legacy.title,
235
+ message: legacy.message,
236
+ options: legacy.options,
237
+ placeholder: legacy.placeholder,
238
+ interactionStatus: 'pending',
239
+ },
240
+ });
241
+ }
242
+
243
+ #notice(legacy) {
244
+ const text = legacy.text;
245
+ if (typeof text !== 'string' || !text) return [];
246
+ const itemId = `item_notice_${this.turnId ?? this.threadId}_${++this.segmentSequence}`;
247
+ return [
248
+ this.#event({ type: 'item.started', itemId, payload: { type: 'notice', level: legacy.level ?? 'info', content: text } }),
249
+ this.#event({ type: 'item.completed', itemId }),
250
+ ];
251
+ }
252
+
253
+ #compaction(legacy) {
254
+ const closed = this.activeSlot ? this.#closeSegment() : [];
255
+ const itemId = `item_compaction_${this.turnId ?? this.threadId}_${++this.segmentSequence}`;
256
+ const payload = {
257
+ type: 'context_compaction',
258
+ ...(legacy.tokensBefore != null ? { tokensBefore: legacy.tokensBefore } : {}),
259
+ ...(legacy.tokensAfter != null ? { tokensAfter: legacy.tokensAfter } : {}),
260
+ ...(legacy.summary ? { summary: legacy.summary } : {}),
261
+ };
262
+ return [
263
+ ...closed,
264
+ this.#event({ type: 'item.started', itemId, payload }),
265
+ this.#event({ type: 'item.completed', itemId, payload }),
266
+ ];
267
+ }
268
+ }
269
+
270
+ module.exports = { EventNormalizer };
@@ -1,6 +1,13 @@
1
1
  const readline = require('node:readline');
2
2
  const { tools } = require('./collaboration-tools');
3
3
 
4
+ // Host 通过 HARNESS_MIX_COLLAB_TEAM=0 表达「Agent Team 已在设置中停用」:
5
+ // 此时对模型隐藏团队工具,只保留一次性委派类工具。
6
+ const TEAM_TOOLS = new Set(['create_agent_team', 'assign_team_task', 'get_team_state', 'update_team_task', 'send_team_message']);
7
+ const visibleTools = process.env.HARNESS_MIX_COLLAB_TEAM === '0'
8
+ ? tools.filter((tool) => !TEAM_TOOLS.has(tool.name))
9
+ : tools;
10
+
4
11
  async function call(name, args) {
5
12
  const response = await fetch(process.env.HARNESS_MIX_COLLAB_URL, {
6
13
  method: 'POST', headers: { Authorization: `Bearer ${process.env.HARNESS_MIX_COLLAB_KEY}`, 'Content-Type': 'application/json' },
@@ -14,7 +21,7 @@ async function call(name, args) {
14
21
  async function dispatch(message) {
15
22
  if (message.method === 'initialize') return { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'harness-mix', version: '0.1.0' } };
16
23
  if (message.method === 'ping') return {};
17
- if (message.method === 'tools/list') return { tools };
24
+ if (message.method === 'tools/list') return { tools: visibleTools };
18
25
  if (message.method === 'tools/call') {
19
26
  try { return { content: [{ type: 'text', text: JSON.stringify(await call(message.params.name, message.params.arguments ?? {})) }] }; }
20
27
  catch (error) { return { isError: true, content: [{ type: 'text', text: error.message }] }; }
@@ -5,7 +5,7 @@ const teamId = { type: 'string', minLength: 1 };
5
5
  const memberId = { type: 'string', minLength: 1 };
6
6
  const tools = [
7
7
  { name: 'list_agents', description: 'List native Harnesses available for delegation and their IDs.', inputSchema: object({}) },
8
- { name: 'delegate_to_agent', description: 'Start an independent native Harness session asynchronously. For Agent Team work, pass team_id, member_id and team_task_id so the native session becomes that durable teammate and can use the shared task graph and mailbox. An @Agent mention explicitly assigns that agent its associated work. Include all necessary context. By default workers share the lead working directory, so later reviewers see earlier edits. Wait for development before starting dependent review; do not edit overlapping files concurrently. Use isolation=worktree for independent isolated experiments (auto also isolates Git projects). Returns task_id; collect results before finishing.', inputSchema: object({ agent_type: taskId, task, isolation: { type: 'string', enum: ['auto', 'worktree', 'shared'] }, team_id: teamId, member_id: memberId, team_task_id: taskId }, ['agent_type', 'task']) },
8
+ { name: 'delegate_to_agent', description: 'Start an independent native Harness session asynchronously. For Agent Team work, pass team_id, member_id and team_task_id so the native session becomes that durable teammate and can use the shared task graph and mailbox. An @Agent mention explicitly assigns that agent its associated work. Include all necessary context. By default workers share the lead working directory, so later reviewers see earlier edits; whenever another session is actively running in that directory, new workers start isolated automatically instead. Wait for development before starting dependent review; do not edit overlapping files concurrently. Use isolation=worktree for independent isolated experiments (auto also isolates Git projects). Returns task_id; collect results before finishing.', inputSchema: object({ agent_type: taskId, task, isolation: { type: 'string', enum: ['auto', 'worktree', 'shared'] }, team_id: teamId, member_id: memberId, team_task_id: taskId }, ['agent_type', 'task']) },
9
9
  { name: 'create_agent_team', description: 'Create a durable multi-Harness Agent Team with one lead and up to six named members with explicit roles. This creates shared team state, not worker sessions; assign tasks and delegate them to members next.', inputSchema: object({ name: { type: 'string', minLength: 1, maxLength: 120 }, goal: task, members: { type: 'array', minItems: 1, maxItems: 6, items: object({ name: { type: 'string', minLength: 1, maxLength: 80 }, role: { type: 'string', minLength: 1, maxLength: 240 }, agent_type: taskId }, ['name', 'role', 'agent_type']) } }, ['name', 'goal', 'members']) },
10
10
  { name: 'assign_team_task', description: 'Add a task to the shared team task graph. Dependencies must already exist. The assignee is a team member id or unique member name.', inputSchema: object({ team_id: teamId, title: { type: 'string', minLength: 1, maxLength: 160 }, description: task, assignee: memberId, depends_on: { type: 'array', maxItems: 16, items: taskId } }, ['team_id', 'title', 'description', 'assignee']) },
11
11
  { name: 'get_team_state', description: 'Read the current team roster, shared task graph and recent mailbox messages. Available to the lead and native teammate sessions. The projection includes a coarse phase (forming/running/waiting/completed), per-status progress counters, and each task\'s dependency depth so parallel lanes and pipeline stages are visible without walking depends_on by hand.', inputSchema: object({ team_id: teamId }, ['team_id']) },